Compare commits
265 Commits
eager-exec
...
graph-stat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71401b1059 | ||
|
|
076acf1b31 | ||
|
|
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 |
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_AUTH_TOKEN=private-token # get from sentry
|
||||||
# SENTRY_ORG=comfy-org
|
# SENTRY_ORG=comfy-org
|
||||||
# SENTRY_PROJECT=cloud-frontend-staging
|
# 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'
|
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:
|
on:
|
||||||
workflow_dispatch:
|
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'
|
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:
|
on:
|
||||||
# Manual trigger
|
# 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'
|
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:
|
on:
|
||||||
# Manual trigger
|
# 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"
|
name: "CI: JSON Validation"
|
||||||
description: "Validates JSON syntax in all tracked .json files (excluding tsconfig*.json) using jq"
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
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"
|
name: "CI: Lint Format"
|
||||||
description: "Linting and code formatting validation for pull requests"
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
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"
|
name: "CI: Python Validation"
|
||||||
description: "Validates Python code in tools/devtools directory"
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
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)"
|
name: "CI: Tests E2E (Deploy for Forks)"
|
||||||
description: "Deploys test results from forked PRs (forks can't access deployment secrets)"
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_run:
|
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"
|
name: "CI: Tests E2E"
|
||||||
description: "End-to-end testing with Playwright across multiple browsers, deploys test reports to Cloudflare Pages"
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -124,12 +124,19 @@ jobs:
|
|||||||
- name: Run Playwright tests (${{ matrix.browser }})
|
- name: Run Playwright tests (${{ matrix.browser }})
|
||||||
id: playwright
|
id: playwright
|
||||||
run: |
|
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 \
|
PLAYWRIGHT_JSON_OUTPUT_NAME=playwright-report/report.json \
|
||||||
pnpm exec playwright test --project=${{ matrix.browser }} \
|
pnpm exec playwright merge-reports --reporter=json ./blob-report
|
||||||
--reporter=list \
|
|
||||||
--reporter=html \
|
|
||||||
--reporter=json
|
|
||||||
|
|
||||||
- name: Upload Playwright report
|
- name: Upload Playwright report
|
||||||
uses: actions/upload-artifact@v4
|
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)"
|
name: "CI: Tests Storybook (Deploy for Forks)"
|
||||||
description: "Deploys Storybook previews from forked PRs (forks can't access deployment secrets)"
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_run:
|
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"
|
name: "CI: Tests Storybook"
|
||||||
description: "Builds Storybook and runs visual regression testing via Chromatic, deploys previews to Cloudflare Pages"
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch: # Allow manual triggering
|
workflow_dispatch: # Allow manual triggering
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# Post starting comment for non-forked PRs
|
# 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"
|
name: "CI: Tests Unit"
|
||||||
description: "Unit and component testing with Vitest"
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
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"
|
name: "CI: YAML Validation"
|
||||||
description: "Validates YAML syntax and style using yamllint with relaxed rules"
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
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"
|
name: "i18n: Update Core"
|
||||||
description: "Generates and updates translations for core ComfyUI components using OpenAI"
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
# Manual dispatch for urgent translation updates
|
# Manual dispatch for urgent translation updates
|
||||||
|
|||||||
18
.github/workflows/pr-backport.yaml
vendored
@@ -16,6 +16,10 @@ on:
|
|||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: backport-${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
backport:
|
backport:
|
||||||
if: >
|
if: >
|
||||||
@@ -236,8 +240,8 @@ jobs:
|
|||||||
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
|
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
|
||||||
MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid')
|
MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid')
|
||||||
else
|
else
|
||||||
PR_TITLE="${{ github.event.pull_request.title }}"
|
PR_TITLE=$(jq -r '.pull_request.title' "$GITHUB_EVENT_PATH")
|
||||||
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
|
MERGE_COMMIT=$(jq -r '.pull_request.merge_commit_sha' "$GITHUB_EVENT_PATH")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for target in ${{ steps.filter-targets.outputs.pending-targets }}; do
|
for target in ${{ steps.filter-targets.outputs.pending-targets }}; do
|
||||||
@@ -326,8 +330,8 @@ jobs:
|
|||||||
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
|
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
|
||||||
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
|
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
|
||||||
else
|
else
|
||||||
PR_TITLE="${{ github.event.pull_request.title }}"
|
PR_TITLE=$(jq -r '.pull_request.title' "$GITHUB_EVENT_PATH")
|
||||||
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
|
PR_AUTHOR=$(jq -r '.pull_request.user.login' "$GITHUB_EVENT_PATH")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for backport in ${{ steps.backport.outputs.success }}; do
|
for backport in ${{ steps.backport.outputs.success }}; do
|
||||||
@@ -364,9 +368,9 @@ jobs:
|
|||||||
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
|
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
|
||||||
MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid')
|
MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid')
|
||||||
else
|
else
|
||||||
PR_NUMBER="${{ github.event.pull_request.number }}"
|
PR_NUMBER=$(jq -r '.pull_request.number' "$GITHUB_EVENT_PATH")
|
||||||
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
|
PR_AUTHOR=$(jq -r '.pull_request.user.login' "$GITHUB_EVENT_PATH")
|
||||||
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
|
MERGE_COMMIT=$(jq -r '.pull_request.merge_commit_sha' "$GITHUB_EVENT_PATH")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for failure in ${{ steps.backport.outputs.failed }}; do
|
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"
|
name: "PR: Claude Review"
|
||||||
description: "AI-powered code review triggered by adding the 'claude-review' label to a PR"
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|||||||
@@ -124,12 +124,16 @@ jobs:
|
|||||||
- name: Stage changed snapshot files
|
- name: Stage changed snapshot files
|
||||||
id: changed-snapshots
|
id: changed-snapshots
|
||||||
run: |
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
echo "=========================================="
|
echo "=========================================="
|
||||||
echo "STAGING CHANGED SNAPSHOTS (Shard ${{ matrix.shardIndex }})"
|
echo "STAGING CHANGED SNAPSHOTS (Shard ${{ matrix.shardIndex }})"
|
||||||
echo "=========================================="
|
echo "=========================================="
|
||||||
|
|
||||||
# Get list of changed snapshot files
|
# Get list of changed snapshot files (including untracked/new files)
|
||||||
changed_files=$(git diff --name-only browser_tests/ 2>/dev/null | grep -E '\-snapshots/' || echo "")
|
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
|
if [ -z "$changed_files" ]; then
|
||||||
echo "No snapshot changes in this shard"
|
echo "No snapshot changes in this shard"
|
||||||
@@ -151,6 +155,11 @@ jobs:
|
|||||||
# Strip 'browser_tests/' prefix to avoid double nesting
|
# Strip 'browser_tests/' prefix to avoid double nesting
|
||||||
echo "Copying changed files to staging directory..."
|
echo "Copying changed files to staging directory..."
|
||||||
while IFS= read -r file; do
|
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
|
# Remove 'browser_tests/' prefix
|
||||||
file_without_prefix="${file#browser_tests/}"
|
file_without_prefix="${file#browser_tests/}"
|
||||||
# Create parent directories
|
# Create parent directories
|
||||||
@@ -261,11 +270,19 @@ jobs:
|
|||||||
echo "CHANGES SUMMARY"
|
echo "CHANGES SUMMARY"
|
||||||
echo "=========================================="
|
echo "=========================================="
|
||||||
echo ""
|
echo ""
|
||||||
echo "Changed files in browser_tests:"
|
echo "Changed files in browser_tests (including untracked):"
|
||||||
git diff --name-only browser_tests/ | head -20 || echo "No changes"
|
CHANGES=$(git status --porcelain=v1 --untracked-files=all -- browser_tests/)
|
||||||
echo ""
|
if [ -z "$CHANGES" ]; then
|
||||||
echo "Total changes:"
|
echo "No changes"
|
||||||
git diff --name-only browser_tests/ | wc -l || echo "0"
|
echo ""
|
||||||
|
echo "Total changes:"
|
||||||
|
echo "0"
|
||||||
|
else
|
||||||
|
echo "$CHANGES" | head -50
|
||||||
|
echo ""
|
||||||
|
echo "Total changes:"
|
||||||
|
echo "$CHANGES" | wc -l
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Commit updated expectations
|
- name: Commit updated expectations
|
||||||
id: commit
|
id: commit
|
||||||
@@ -273,7 +290,7 @@ jobs:
|
|||||||
git config --global user.name 'github-actions'
|
git config --global user.name 'github-actions'
|
||||||
git config --global user.email 'github-actions@github.com'
|
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 "No changes to commit"
|
||||||
echo "has-changes=false" >> $GITHUB_OUTPUT
|
echo "has-changes=false" >> $GITHUB_OUTPUT
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
4
.github/workflows/release-branch-create.yaml
vendored
@@ -148,10 +148,10 @@ jobs:
|
|||||||
done
|
done
|
||||||
|
|
||||||
{
|
{
|
||||||
echo "results<<'EOF'"
|
echo "results<<EOF"
|
||||||
cat "$RESULTS_FILE"
|
cat "$RESULTS_FILE"
|
||||||
echo "EOF"
|
echo "EOF"
|
||||||
} >> $GITHUB_OUTPUT
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Ensure release labels
|
- name: Ensure release labels
|
||||||
if: steps.check_version.outputs.is_minor_bump == 'true'
|
if: steps.check_version.outputs.is_minor_bump == 'true'
|
||||||
|
|||||||
2
.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"
|
name: "Release: Version Bump"
|
||||||
description: "Manual workflow to increment package version with semantic versioning support"
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|||||||
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"
|
name: "Weekly Documentation Check"
|
||||||
description: "Automated weekly documentation accuracy check and update via Claude"
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ module.exports = defineConfig({
|
|||||||
entry: 'src/locales/en',
|
entry: 'src/locales/en',
|
||||||
entryLocale: 'en',
|
entryLocale: 'en',
|
||||||
output: 'src/locales',
|
output: 'src/locales',
|
||||||
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar', 'tr'],
|
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.
|
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream.
|
||||||
'latent' is the short form of 'latent space'.
|
'latent' is the short form of 'latent space'.
|
||||||
'mask' is in the context of image processing.
|
'mask' is in the context of image processing.
|
||||||
|
|||||||
@@ -2,25 +2,98 @@
|
|||||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||||
"ignorePatterns": [
|
"ignorePatterns": [
|
||||||
".i18nrc.cjs",
|
".i18nrc.cjs",
|
||||||
"components.d.ts",
|
".nx/*",
|
||||||
"lint-staged.config.js",
|
|
||||||
"vitest.setup.ts",
|
|
||||||
"**/vite.config.*.timestamp*",
|
"**/vite.config.*.timestamp*",
|
||||||
"**/vitest.config.*.timestamp*",
|
"**/vitest.config.*.timestamp*",
|
||||||
|
"components.d.ts",
|
||||||
|
"coverage/*",
|
||||||
|
"dist/*",
|
||||||
"packages/registry-types/src/comfyRegistryTypes.ts",
|
"packages/registry-types/src/comfyRegistryTypes.ts",
|
||||||
|
"playwright-report/*",
|
||||||
"src/extensions/core/*",
|
"src/extensions/core/*",
|
||||||
"src/scripts/*",
|
"src/scripts/*",
|
||||||
"src/types/generatedManagerTypes.ts",
|
"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": {
|
"rules": {
|
||||||
"no-async-promise-executor": "off",
|
"no-async-promise-executor": "off",
|
||||||
|
"no-console": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"allow": [
|
||||||
|
"warn",
|
||||||
|
"error"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"no-control-regex": "off",
|
"no-control-regex": "off",
|
||||||
"no-eval": "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-self-assign": "allow",
|
||||||
"no-unused-expressions": "off",
|
"no-unused-expressions": "off",
|
||||||
"no-unused-private-class-members": "off",
|
"no-unused-private-class-members": "off",
|
||||||
"no-useless-rename": "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-this-alias": "off",
|
||||||
"typescript/no-unnecessary-parameter-property-assignment": "off",
|
"typescript/no-unnecessary-parameter-property-assignment": "off",
|
||||||
"typescript/no-unsafe-declaration-merging": "off",
|
"typescript/no-unsafe-declaration-merging": "off",
|
||||||
@@ -38,6 +111,19 @@
|
|||||||
"typescript/no-redundant-type-constituents": "off",
|
"typescript/no-redundant-type-constituents": "off",
|
||||||
"typescript/restrict-template-expressions": "off",
|
"typescript/restrict-template-expressions": "off",
|
||||||
"typescript/unbound-method": "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,
|
deep: true,
|
||||||
extensions: ['vue']
|
extensions: ['vue']
|
||||||
})
|
})
|
||||||
// Note: Explicitly NOT including generateImportMapPlugin to avoid externalization
|
|
||||||
],
|
],
|
||||||
server: {
|
server: {
|
||||||
allowedHosts: true
|
allowedHosts: true
|
||||||
|
|||||||
@@ -12,6 +12,9 @@
|
|||||||
"declaration-property-value-no-unknown": [
|
"declaration-property-value-no-unknown": [
|
||||||
true,
|
true,
|
||||||
{
|
{
|
||||||
|
"typesSyntax": {
|
||||||
|
"radial-gradient()": "| <any-value>"
|
||||||
|
},
|
||||||
"ignoreProperties": {
|
"ignoreProperties": {
|
||||||
"speak": ["none"],
|
"speak": ["none"],
|
||||||
"app-region": ["drag", "no-drag"],
|
"app-region": ["drag", "no-drag"],
|
||||||
@@ -56,10 +59,7 @@
|
|||||||
"function-no-unknown": [
|
"function-no-unknown": [
|
||||||
true,
|
true,
|
||||||
{
|
{
|
||||||
"ignoreFunctions": [
|
"ignoreFunctions": ["theme", "v-bind"]
|
||||||
"theme",
|
|
||||||
"v-bind"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
254
AGENTS.md
@@ -1,38 +1,244 @@
|
|||||||
# Repository Guidelines
|
# Repository Guidelines
|
||||||
|
|
||||||
## Project Structure & Module Organization
|
## 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`.
|
- Source: `src/`
|
||||||
- Tests: unit/component in `tests-ui/` and `src/components/**/*.{test,spec}.ts`; E2E in `browser_tests/`.
|
- Vue 3.5+
|
||||||
- Public assets: `public/`. Build output: `dist/`.
|
- TypeScript
|
||||||
- Config: `vite.config.mts`, `vitest.config.ts`, `playwright.config.ts`, `eslint.config.ts`, `.prettierrc`.
|
- 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
|
## Build, Test, and Development Commands
|
||||||
|
|
||||||
- `pnpm dev`: Start Vite dev server.
|
- `pnpm dev`: Start Vite dev server.
|
||||||
- `pnpm dev:electron`: Dev server with Electron API mocks.
|
- `pnpm dev:electron`: Dev server with Electron API mocks
|
||||||
- `pnpm build`: Type-check then production build to `dist/`.
|
- `pnpm build`: Type-check then production build to `dist/`
|
||||||
- `pnpm preview`: Preview the production build locally.
|
- `pnpm preview`: Preview the production build locally
|
||||||
- `pnpm test:unit`: Run Vitest unit tests.
|
- `pnpm test:unit`: Run Vitest unit tests
|
||||||
- `pnpm test:browser`: Run Playwright E2E tests (`browser_tests/`).
|
- `pnpm test:browser`: Run Playwright E2E tests (`browser_tests/`)
|
||||||
- `pnpm lint` / `pnpm lint:fix`: Lint (ESLint). `pnpm format` / `format:check`: Prettier.
|
- `pnpm lint` / `pnpm lint:fix`: Lint (ESLint)
|
||||||
- `pnpm typecheck`: Vue TSC type checking.
|
- `pnpm format` / `pnpm format:check`: Prettier
|
||||||
|
- `pnpm typecheck`: Vue TSC type checking
|
||||||
|
|
||||||
## Coding Style & Naming Conventions
|
## 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
|
- Language:
|
||||||
- Frameworks: Vitest (unit/component, happy-dom) and Playwright (E2E).
|
- TypeScript (exclusive, no new JavaScript)
|
||||||
- Test files: `**/*.{test,spec}.{ts,tsx,js}` under `tests-ui/`, `src/components/`, and `src/lib/litegraph/test/`.
|
- Vue 3 SFCs (`.vue`)
|
||||||
- Coverage: text/json/html reporters enabled; aim to cover critical logic and new features.
|
- Composition API only
|
||||||
- Playwright: place tests in `browser_tests/`; optional tags like `@mobile`, `@2x` are respected by config.
|
- 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
|
## 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.
|
- PRs:
|
||||||
- Quality gates: `pnpm lint`, `pnpm typecheck`, and relevant tests must pass. Keep PRs focused and small.
|
- 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
|
## Security & Configuration Tips
|
||||||
|
|
||||||
- Secrets: Use `.env` (see `.env_example`); do not commit secrets.
|
- 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
|
## Repository Setup
|
||||||
|
|
||||||
For first-time setup, use the Claude command:
|
For first-time setup, use the Claude command:
|
||||||
```
|
|
||||||
|
```sh
|
||||||
/setup_repo
|
/setup_repo
|
||||||
```
|
```
|
||||||
|
|
||||||
This bootstraps the monorepo with dependencies, builds, tests, and dev server verification.
|
This bootstraps the monorepo with dependencies, builds, tests, and dev server verification.
|
||||||
|
|
||||||
**Prerequisites:** Node.js >= 24, Git repository, available ports (5173, 6006)
|
**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
|
## Development Workflow
|
||||||
|
|
||||||
1. **First-time setup**: Run `/setup_repo` Claude command
|
1. **First-time setup**: Run `/setup_repo` Claude command
|
||||||
@@ -50,87 +25,6 @@ Key Nx features:
|
|||||||
|
|
||||||
## Git Conventions
|
## Git Conventions
|
||||||
|
|
||||||
- Use [prefix] format: [feat], [bugfix], [docs]
|
- Use `prefix:` format: `feat:`, `fix:`, `test:`
|
||||||
- Add "Fixes #n" to PR descriptions
|
- Add "Fixes #n" to PR descriptions
|
||||||
- Never mention Claude/AI in commits
|
- 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
|
# Desktop/Electron
|
||||||
/apps/desktop-ui/ @webfiltered
|
/apps/desktop-ui/ @benceruleanlu
|
||||||
/src/stores/electronDownloadStore.ts @webfiltered
|
/src/stores/electronDownloadStore.ts @benceruleanlu
|
||||||
/src/extensions/core/electronAdapter.ts @webfiltered
|
/src/extensions/core/electronAdapter.ts @benceruleanlu
|
||||||
/vite.electron.config.mts @webfiltered
|
/vite.electron.config.mts @benceruleanlu
|
||||||
|
|
||||||
# Common UI Components
|
# Common UI Components
|
||||||
/src/components/chip/ @viva-jinyi
|
/src/components/chip/ @viva-jinyi
|
||||||
@@ -22,6 +25,9 @@
|
|||||||
# Link rendering
|
# Link rendering
|
||||||
/src/renderer/core/canvas/links/ @benceruleanlu
|
/src/renderer/core/canvas/links/ @benceruleanlu
|
||||||
|
|
||||||
|
# Partner Nodes
|
||||||
|
/src/composables/node/useNodePricing.ts @jojodecayz @bigcat88
|
||||||
|
|
||||||
# Node help system
|
# Node help system
|
||||||
/src/utils/nodeHelpUtil.ts @benceruleanlu
|
/src/utils/nodeHelpUtil.ts @benceruleanlu
|
||||||
/src/stores/workspace/nodeHelpStore.ts @benceruleanlu
|
/src/stores/workspace/nodeHelpStore.ts @benceruleanlu
|
||||||
@@ -31,10 +37,7 @@
|
|||||||
/src/components/graph/selectionToolbox/ @Myestery
|
/src/components/graph/selectionToolbox/ @Myestery
|
||||||
|
|
||||||
# Minimap
|
# Minimap
|
||||||
/src/renderer/extensions/minimap/ @jtydhr88
|
/src/renderer/extensions/minimap/ @jtydhr88 @Myestery
|
||||||
|
|
||||||
# Assets
|
|
||||||
/src/platform/assets/ @arjansingh
|
|
||||||
|
|
||||||
# Workflow Templates
|
# Workflow Templates
|
||||||
/src/platform/workflow/templates/ @Myestery @christian-byrne @comfyui-wiki
|
/src/platform/workflow/templates/ @Myestery @christian-byrne @comfyui-wiki
|
||||||
@@ -53,11 +56,12 @@
|
|||||||
/src/workbench/extensions/manager/ @viva-jinyi @christian-byrne @ltdrdata
|
/src/workbench/extensions/manager/ @viva-jinyi @christian-byrne @ltdrdata
|
||||||
|
|
||||||
# Translations
|
# 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)
|
# LLM Instructions (blank on purpose)
|
||||||
.claude/
|
.claude/
|
||||||
.cursor/
|
.cursor/
|
||||||
.cursorrules
|
.cursorrules
|
||||||
**/AGENTS.md
|
**/AGENTS.md
|
||||||
**/CLAUDE.md
|
**/CLAUDE.md
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@comfyorg/desktop-ui",
|
"name": "@comfyorg/desktop-ui",
|
||||||
"version": "0.0.3",
|
"version": "0.0.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"nx": {
|
"nx": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -87,6 +87,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"lint": "nx run @comfyorg/desktop-ui:lint",
|
||||||
|
"typecheck": "nx run @comfyorg/desktop-ui:typecheck",
|
||||||
"storybook": "storybook dev -p 6007",
|
"storybook": "storybook dev -p 6007",
|
||||||
"build-storybook": "storybook build -o dist/storybook"
|
"build-storybook": "storybook build -o dist/storybook"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ const LOCALES = [
|
|||||||
['fr', 'Français'],
|
['fr', 'Français'],
|
||||||
['es', 'Español'],
|
['es', 'Español'],
|
||||||
['ar', 'عربي'],
|
['ar', 'عربي'],
|
||||||
['tr', 'Türkçe']
|
['tr', 'Türkçe'],
|
||||||
|
['pt-BR', 'Português (BR)']
|
||||||
] as const satisfies ReadonlyArray<[string, string]>
|
] as const satisfies ReadonlyArray<[string, string]>
|
||||||
|
|
||||||
type SupportedLocale = (typeof LOCALES)[number][0]
|
type SupportedLocale = (typeof LOCALES)[number][0]
|
||||||
|
|||||||
@@ -22,7 +22,11 @@
|
|||||||
<h1 v-if="title" class="font-inter font-bold text-3xl text-neutral-300">
|
<h1 v-if="title" class="font-inter font-bold text-3xl text-neutral-300">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</h1>
|
</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 }}
|
{{ statusText }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,7 +40,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { PassThrough } from '@primevue/core'
|
import type { PassThrough } from '@primevue/core'
|
||||||
import Button from 'primevue/button'
|
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'
|
import StepList from 'primevue/steplist'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
// Import only English locale eagerly as the default/fallback
|
// Import only English locale eagerly as the default/fallback
|
||||||
// ESLint cannot statically resolve dynamic imports with path aliases (@frontend-locales/*),
|
// 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.
|
// 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' }
|
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' }
|
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' }
|
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 enSettings from '@frontend-locales/en/settings.json' with { type: 'json' }
|
||||||
import { createI18n } from 'vue-i18n'
|
import { createI18n } from 'vue-i18n'
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ function buildLocale<
|
|||||||
|
|
||||||
// Locale loader map - dynamically import locales only when needed
|
// Locale loader map - dynamically import locales only when needed
|
||||||
// ESLint cannot statically resolve these dynamic imports, but they are valid at build time
|
// ESLint cannot statically resolve these dynamic imports, but they are valid at build time
|
||||||
/* eslint-disable import-x/no-unresolved */
|
|
||||||
const localeLoaders: Record<
|
const localeLoaders: Record<
|
||||||
string,
|
string,
|
||||||
() => Promise<{ default: Record<string, unknown> }>
|
() => Promise<{ default: Record<string, unknown> }>
|
||||||
@@ -40,7 +40,8 @@ const localeLoaders: Record<
|
|||||||
ru: () => import('@frontend-locales/ru/main.json'),
|
ru: () => import('@frontend-locales/ru/main.json'),
|
||||||
tr: () => import('@frontend-locales/tr/main.json'),
|
tr: () => import('@frontend-locales/tr/main.json'),
|
||||||
zh: () => import('@frontend-locales/zh/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<
|
const nodeDefsLoaders: Record<
|
||||||
@@ -55,7 +56,8 @@ const nodeDefsLoaders: Record<
|
|||||||
ru: () => import('@frontend-locales/ru/nodeDefs.json'),
|
ru: () => import('@frontend-locales/ru/nodeDefs.json'),
|
||||||
tr: () => import('@frontend-locales/tr/nodeDefs.json'),
|
tr: () => import('@frontend-locales/tr/nodeDefs.json'),
|
||||||
zh: () => import('@frontend-locales/zh/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<
|
const commandsLoaders: Record<
|
||||||
@@ -70,7 +72,8 @@ const commandsLoaders: Record<
|
|||||||
ru: () => import('@frontend-locales/ru/commands.json'),
|
ru: () => import('@frontend-locales/ru/commands.json'),
|
||||||
tr: () => import('@frontend-locales/tr/commands.json'),
|
tr: () => import('@frontend-locales/tr/commands.json'),
|
||||||
zh: () => import('@frontend-locales/zh/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<
|
const settingsLoaders: Record<
|
||||||
@@ -85,7 +88,8 @@ const settingsLoaders: Record<
|
|||||||
ru: () => import('@frontend-locales/ru/settings.json'),
|
ru: () => import('@frontend-locales/ru/settings.json'),
|
||||||
tr: () => import('@frontend-locales/tr/settings.json'),
|
tr: () => import('@frontend-locales/tr/settings.json'),
|
||||||
zh: () => import('@frontend-locales/zh/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
|
// 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
|
// Only include English in the initial bundle
|
||||||
const messages = {
|
const enMessages = buildLocale(en, enNodes, enCommands, enSettings)
|
||||||
en: buildLocale(en, enNodes, enCommands, enSettings)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type for locale messages - inferred from the English locale structure
|
// 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({
|
export const i18n = createI18n({
|
||||||
// Must set `false`, as Vue I18n Legacy API is for Vue 2
|
// Must set `false`, as Vue I18n Legacy API is for Vue 2
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useTimeout } from '@vueuse/core'
|
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}.
|
* 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 Button from 'primevue/button'
|
||||||
import { useRoute } from 'vue-router'
|
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 { t } from '@/i18n'
|
||||||
import { electronAPI } from '@/utils/envUtil'
|
import { electronAPI } from '@/utils/envUtil'
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<img
|
<img
|
||||||
class="sad-girl"
|
class="sad-girl"
|
||||||
src="/assets/images/sad_girl.png"
|
src="/assets/images/sad_girl.png"
|
||||||
alt="Sad girl illustration"
|
:alt="$t('notSupported.illustrationAlt')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="no-drag sad-text flex items-center">
|
<div class="no-drag sad-text flex items-center">
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
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 |
@@ -27,18 +27,32 @@ dotenv.config()
|
|||||||
|
|
||||||
type WorkspaceStore = ReturnType<typeof useWorkspaceStore>
|
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 {
|
class ComfyMenu {
|
||||||
private _nodeLibraryTab: NodeLibrarySidebarTab | null = null
|
private _nodeLibraryTab: NodeLibrarySidebarTab | null = null
|
||||||
private _workflowsTab: WorkflowsSidebarTab | null = null
|
private _workflowsTab: WorkflowsSidebarTab | null = null
|
||||||
private _topbar: Topbar | null = null
|
private _topbar: Topbar | null = null
|
||||||
|
|
||||||
public readonly sideToolbar: Locator
|
public readonly sideToolbar: Locator
|
||||||
|
public readonly propertiesPanel: ComfyPropertiesPanel
|
||||||
public readonly themeToggleButton: Locator
|
public readonly themeToggleButton: Locator
|
||||||
public readonly saveButton: Locator
|
public readonly saveButton: Locator
|
||||||
|
|
||||||
constructor(public readonly page: Page) {
|
constructor(public readonly page: Page) {
|
||||||
this.sideToolbar = page.locator('.side-tool-bar-container')
|
this.sideToolbar = page.locator('.side-tool-bar-container')
|
||||||
this.themeToggleButton = page.locator('.comfy-vue-theme-toggle')
|
this.themeToggleButton = page.locator('.comfy-vue-theme-toggle')
|
||||||
|
this.propertiesPanel = new ComfyPropertiesPanel(page)
|
||||||
this.saveButton = page
|
this.saveButton = page
|
||||||
.locator('button[title="Save the current workflow"]')
|
.locator('button[title="Save the current workflow"]')
|
||||||
.nth(0)
|
.nth(0)
|
||||||
@@ -112,6 +126,20 @@ class ConfirmDialog {
|
|||||||
const loc = this[locator]
|
const loc = this[locator]
|
||||||
await expect(loc).toBeVisible()
|
await expect(loc).toBeVisible()
|
||||||
await loc.click()
|
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 }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,6 +256,9 @@ export class ComfyPage {
|
|||||||
await this.page.evaluate(async () => {
|
await this.page.evaluate(async () => {
|
||||||
await window['app'].extensionManager.workflow.syncWorkflows()
|
await window['app'].extensionManager.workflow.syncWorkflows()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Wait for Vue to re-render the workflow list
|
||||||
|
await this.nextFrame()
|
||||||
}
|
}
|
||||||
|
|
||||||
async setupUser(username: string) {
|
async setupUser(username: string) {
|
||||||
@@ -310,19 +341,6 @@ export class ComfyPage {
|
|||||||
}
|
}
|
||||||
await this.goto()
|
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(() => document.fonts.ready)
|
||||||
await this.page.waitForFunction(
|
await this.page.waitForFunction(
|
||||||
() =>
|
() =>
|
||||||
@@ -567,9 +585,15 @@ export class ComfyPage {
|
|||||||
fileName?: string
|
fileName?: string
|
||||||
url?: string
|
url?: string
|
||||||
dropPosition?: Position
|
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)
|
if (!fileName && !url)
|
||||||
throw new Error('Must provide either fileName or url')
|
throw new Error('Must provide either fileName or url')
|
||||||
@@ -606,6 +630,14 @@ export class ComfyPage {
|
|||||||
// Dropping a URL (e.g., dropping image across browser tabs in Firefox)
|
// Dropping a URL (e.g., dropping image across browser tabs in Firefox)
|
||||||
if (url) evaluateParams.url = url
|
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
|
// Execute the drag and drop in the browser
|
||||||
await this.page.evaluate(async (params) => {
|
await this.page.evaluate(async (params) => {
|
||||||
const dataTransfer = new DataTransfer()
|
const dataTransfer = new DataTransfer()
|
||||||
@@ -672,12 +704,17 @@ export class ComfyPage {
|
|||||||
}
|
}
|
||||||
}, evaluateParams)
|
}, evaluateParams)
|
||||||
|
|
||||||
|
// Wait for file upload to complete
|
||||||
|
if (uploadResponsePromise) {
|
||||||
|
await uploadResponsePromise
|
||||||
|
}
|
||||||
|
|
||||||
await this.nextFrame()
|
await this.nextFrame()
|
||||||
}
|
}
|
||||||
|
|
||||||
async dragAndDropFile(
|
async dragAndDropFile(
|
||||||
fileName: string,
|
fileName: string,
|
||||||
options: { dropPosition?: Position } = {}
|
options: { dropPosition?: Position; waitForUpload?: boolean } = {}
|
||||||
) {
|
) {
|
||||||
return this.dragAndDropExternalResource({ fileName, ...options })
|
return this.dragAndDropExternalResource({ fileName, ...options })
|
||||||
}
|
}
|
||||||
@@ -1258,9 +1295,6 @@ export class ComfyPage {
|
|||||||
}, 'image/png')
|
}, 'image/png')
|
||||||
})
|
})
|
||||||
}, filename)
|
}, filename)
|
||||||
|
|
||||||
// Wait a bit for the download to process
|
|
||||||
await this.page.waitForTimeout(500)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1651,7 +1685,10 @@ export const comfyPageFixture = base.extend<{
|
|||||||
// Set tutorial completed to true to avoid loading the tutorial workflow.
|
// Set tutorial completed to true to avoid loading the tutorial workflow.
|
||||||
'Comfy.TutorialCompleted': true,
|
'Comfy.TutorialCompleted': true,
|
||||||
'Comfy.SnapToGrid.GridSize': testComfySnapToGridGridSize,
|
'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) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
|
|||||||
@@ -60,9 +60,6 @@ export class ComfyNodeSearchBox {
|
|||||||
await this.input.waitFor({ state: 'visible' })
|
await this.input.waitFor({ state: 'visible' })
|
||||||
await this.input.fill(nodeName)
|
await this.input.fill(nodeName)
|
||||||
await this.dropdown.waitFor({ state: 'visible' })
|
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
|
await this.dropdown
|
||||||
.locator('li')
|
.locator('li')
|
||||||
.nth(options?.suggestionIndex || 0)
|
.nth(options?.suggestionIndex || 0)
|
||||||
|
|||||||
@@ -116,7 +116,6 @@ export class WorkflowsSidebarTab extends SidebarTab {
|
|||||||
async switchToWorkflow(workflowName: string) {
|
async switchToWorkflow(workflowName: string) {
|
||||||
const workflowLocator = this.getOpenedItem(workflowName)
|
const workflowLocator = this.getOpenedItem(workflowName)
|
||||||
await workflowLocator.click()
|
await workflowLocator.click()
|
||||||
await this.page.waitForTimeout(300)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getOpenedItem(name: string) {
|
getOpenedItem(name: string) {
|
||||||
@@ -138,7 +137,13 @@ export class WorkflowsSidebarTab extends SidebarTab {
|
|||||||
.click()
|
.click()
|
||||||
await this.page.keyboard.type(newName)
|
await this.page.keyboard.type(newName)
|
||||||
await this.page.keyboard.press('Enter')
|
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) {
|
async insertWorkflow(locator: Locator) {
|
||||||
|
|||||||
@@ -92,10 +92,26 @@ export class Topbar {
|
|||||||
)
|
)
|
||||||
// Wait for the dialog to close.
|
// Wait for the dialog to close.
|
||||||
await this.getSaveDialog().waitFor({ state: 'hidden', timeout: 500 })
|
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() {
|
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.menuTrigger.click()
|
||||||
await this.menuLocator.waitFor({ state: 'visible' })
|
await this.menuLocator.waitFor({ state: 'visible' })
|
||||||
return this.menuLocator
|
return this.menuLocator
|
||||||
@@ -163,15 +179,36 @@ export class Topbar {
|
|||||||
|
|
||||||
await topLevelMenu.hover()
|
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
|
let currentMenu = topLevelMenu
|
||||||
for (let i = 1; i < path.length; i++) {
|
for (let i = 1; i < path.length; i++) {
|
||||||
const commandName = path[i]
|
const commandName = path[i]
|
||||||
const menuItem = currentMenu
|
const menuItem = submenu
|
||||||
.locator(
|
.locator(`.p-tieredmenu-item:has-text("${commandName}")`)
|
||||||
`.p-tieredmenu-submenu .p-tieredmenu-item:has-text("${commandName}")`
|
|
||||||
)
|
|
||||||
.first()
|
.first()
|
||||||
await menuItem.waitFor({ state: 'visible' })
|
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()
|
await menuItem.hover()
|
||||||
currentMenu = menuItem
|
currentMenu = menuItem
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -462,7 +462,6 @@ export class NodeReference {
|
|||||||
async convertToSubgraph() {
|
async convertToSubgraph() {
|
||||||
await this.clickContextMenuOption('Convert to Subgraph')
|
await this.clickContextMenuOption('Convert to Subgraph')
|
||||||
await this.comfyPage.nextFrame()
|
await this.comfyPage.nextFrame()
|
||||||
await this.comfyPage.page.waitForTimeout(256)
|
|
||||||
const nodes = await this.comfyPage.getNodeRefsByTitle('New Subgraph')
|
const nodes = await this.comfyPage.getNodeRefsByTitle('New Subgraph')
|
||||||
if (nodes.length !== 1) {
|
if (nodes.length !== 1) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -511,7 +510,6 @@ export class NodeReference {
|
|||||||
// Double-click to enter subgraph
|
// Double-click to enter subgraph
|
||||||
await this.comfyPage.canvas.dblclick({ position, force: true })
|
await this.comfyPage.canvas.dblclick({ position, force: true })
|
||||||
await this.comfyPage.nextFrame()
|
await this.comfyPage.nextFrame()
|
||||||
await this.comfyPage.page.waitForTimeout(500)
|
|
||||||
|
|
||||||
// Check if we successfully entered the subgraph
|
// Check if we successfully entered the subgraph
|
||||||
isInSubgraph = await this.comfyPage.page.evaluate(() => {
|
isInSubgraph = await this.comfyPage.page.evaluate(() => {
|
||||||
|
|||||||
@@ -5,14 +5,18 @@ import type { AutoQueueMode } from '../../src/stores/queueStore'
|
|||||||
export class ComfyActionbar {
|
export class ComfyActionbar {
|
||||||
public readonly root: Locator
|
public readonly root: Locator
|
||||||
public readonly queueButton: ComfyQueueButton
|
public readonly queueButton: ComfyQueueButton
|
||||||
|
public readonly propertiesButton: Locator
|
||||||
|
|
||||||
constructor(public readonly page: Page) {
|
constructor(public readonly page: Page) {
|
||||||
this.root = page.locator('.actionbar')
|
this.root = page.locator('.actionbar-container')
|
||||||
this.queueButton = new ComfyQueueButton(this)
|
this.queueButton = new ComfyQueueButton(this)
|
||||||
|
this.propertiesButton = this.root.getByLabel('Toggle properties panel')
|
||||||
}
|
}
|
||||||
|
|
||||||
async isDocked() {
|
async isDocked() {
|
||||||
const className = await this.root.getAttribute('class')
|
const className = await this.root
|
||||||
|
.locator('.actionbar')
|
||||||
|
.getAttribute('class')
|
||||||
return className?.includes('static') ?? false
|
return className?.includes('static') ?? false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Locator, Page } from '@playwright/test'
|
import type { Locator, Page } from '@playwright/test'
|
||||||
|
import { expect } from '@playwright/test'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@@ -8,9 +9,20 @@ import type {
|
|||||||
|
|
||||||
export class ComfyTemplates {
|
export class ComfyTemplates {
|
||||||
readonly content: Locator
|
readonly content: Locator
|
||||||
|
readonly allTemplateCards: Locator
|
||||||
|
|
||||||
constructor(readonly page: Page) {
|
constructor(readonly page: Page) {
|
||||||
this.content = page.getByTestId('template-workflows-content')
|
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) {
|
async loadTemplate(id: string) {
|
||||||
|
|||||||
@@ -75,13 +75,9 @@ test.describe('Background Image Upload', () => {
|
|||||||
// Upload the test image
|
// Upload the test image
|
||||||
await fileChooser.setFiles(comfyPage.assetPath('image32x32.webp'))
|
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
|
// Verify the URL input now has an API URL
|
||||||
const urlInput = backgroundImageSetting.locator('input[type="text"]')
|
const urlInput = backgroundImageSetting.locator('input[type="text"]')
|
||||||
const inputValue = await urlInput.inputValue()
|
await expect(urlInput).toHaveValue(/^\/api\/view\?.*subfolder=backgrounds/)
|
||||||
expect(inputValue).toMatch(/^\/api\/view\?.*subfolder=backgrounds/)
|
|
||||||
|
|
||||||
// Verify clear button is now enabled
|
// Verify clear button is now enabled
|
||||||
const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)')
|
const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)')
|
||||||
@@ -191,14 +187,11 @@ test.describe('Background Image Upload', () => {
|
|||||||
)
|
)
|
||||||
await uploadButton.hover()
|
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')
|
const uploadTooltip = comfyPage.page.locator('.p-tooltip:visible')
|
||||||
await expect(uploadTooltip).toBeVisible()
|
await expect(uploadTooltip).toBeVisible()
|
||||||
|
|
||||||
// Move away to hide tooltip
|
// Move away to hide tooltip
|
||||||
await comfyPage.page.locator('body').hover()
|
await comfyPage.page.locator('body').hover()
|
||||||
await comfyPage.page.waitForTimeout(100)
|
|
||||||
|
|
||||||
// Set a background to enable clear button
|
// Set a background to enable clear button
|
||||||
const urlInput = backgroundImageSetting.locator('input[type="text"]')
|
const urlInput = backgroundImageSetting.locator('input[type="text"]')
|
||||||
@@ -209,8 +202,6 @@ test.describe('Background Image Upload', () => {
|
|||||||
const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)')
|
const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)')
|
||||||
await clearButton.hover()
|
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')
|
const clearTooltip = comfyPage.page.locator('.p-tooltip:visible')
|
||||||
await expect(clearTooltip).toBeVisible()
|
await expect(clearTooltip).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -203,7 +203,6 @@ test.describe('Node Color Adjustments', () => {
|
|||||||
comfyPage
|
comfyPage
|
||||||
}) => {
|
}) => {
|
||||||
await comfyPage.setSetting('Comfy.Node.Opacity', 0.5)
|
await comfyPage.setSetting('Comfy.Node.Opacity', 0.5)
|
||||||
await comfyPage.page.waitForTimeout(128)
|
|
||||||
|
|
||||||
// Drag mouse to force canvas to redraw
|
// Drag mouse to force canvas to redraw
|
||||||
await comfyPage.page.mouse.move(0, 0)
|
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 expect(comfyPage.canvas).toHaveScreenshot('node-opacity-0.5.png')
|
||||||
|
|
||||||
await comfyPage.setSetting('Comfy.Node.Opacity', 1.0)
|
await comfyPage.setSetting('Comfy.Node.Opacity', 1.0)
|
||||||
await comfyPage.page.waitForTimeout(128)
|
|
||||||
|
|
||||||
await comfyPage.page.mouse.move(8, 8)
|
await comfyPage.page.mouse.move(8, 8)
|
||||||
await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-1.png')
|
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.Node.Opacity', 0.5)
|
||||||
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
|
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
|
||||||
const saveWorkflowInterval = 1000
|
const saveWorkflowInterval = 1000
|
||||||
await comfyPage.page.waitForTimeout(saveWorkflowInterval)
|
|
||||||
const workflow = await comfyPage.page.evaluate(() => {
|
const workflow = await comfyPage.page.evaluate(() => {
|
||||||
return localStorage.getItem('workflow')
|
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
|
// Wait for any async operations to complete after dialog closes
|
||||||
await comfyPage.nextFrame()
|
await comfyPage.nextFrame()
|
||||||
await comfyPage.page.waitForTimeout(100)
|
|
||||||
|
|
||||||
// Make a change to the graph
|
// Make a change to the graph
|
||||||
await comfyPage.doubleClickCanvas()
|
await comfyPage.doubleClickCanvas()
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 86 KiB |
@@ -36,13 +36,10 @@ test.describe('Execute to selected output nodes', () => {
|
|||||||
await output1.click('title')
|
await output1.click('title')
|
||||||
|
|
||||||
await comfyPage.executeCommand('Comfy.QueueSelectedOutputNodes')
|
await comfyPage.executeCommand('Comfy.QueueSelectedOutputNodes')
|
||||||
// @note: Wait for the execution to finish. We might want to move to a more
|
await expect(async () => {
|
||||||
// reliable way to wait for the execution to finish. Workflow in this test
|
expect(await (await input.getWidget(0)).getValue()).toBe('foo')
|
||||||
// is simple enough that this is fine for now.
|
expect(await (await output1.getWidget(0)).getValue()).toBe('foo')
|
||||||
await comfyPage.page.waitForTimeout(200)
|
expect(await (await output2.getWidget(0)).getValue()).toBe('')
|
||||||
|
}).toPass({ timeout: 2_000 })
|
||||||
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('')
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 97 KiB |
@@ -94,9 +94,6 @@ test.describe('Feature Flags', () => {
|
|||||||
test('Server feature flags are received and accessible', async ({
|
test('Server feature flags are received and accessible', async ({
|
||||||
comfyPage
|
comfyPage
|
||||||
}) => {
|
}) => {
|
||||||
// Wait for connection to establish
|
|
||||||
await comfyPage.page.waitForTimeout(1000)
|
|
||||||
|
|
||||||
// Get the actual server feature flags from the backend
|
// Get the actual server feature flags from the backend
|
||||||
const serverFlags = await comfyPage.page.evaluate(() => {
|
const serverFlags = await comfyPage.page.evaluate(() => {
|
||||||
return window['app'].api.serverFeatureFlags
|
return window['app'].api.serverFeatureFlags
|
||||||
@@ -116,9 +113,6 @@ test.describe('Feature Flags', () => {
|
|||||||
test('serverSupportsFeature method works with real backend flags', async ({
|
test('serverSupportsFeature method works with real backend flags', async ({
|
||||||
comfyPage
|
comfyPage
|
||||||
}) => {
|
}) => {
|
||||||
// Wait for connection
|
|
||||||
await comfyPage.page.waitForTimeout(1000)
|
|
||||||
|
|
||||||
// Test serverSupportsFeature with real backend flags
|
// Test serverSupportsFeature with real backend flags
|
||||||
const supportsPreviewMetadata = await comfyPage.page.evaluate(() => {
|
const supportsPreviewMetadata = await comfyPage.page.evaluate(() => {
|
||||||
return window['app'].api.serverSupportsFeature(
|
return window['app'].api.serverSupportsFeature(
|
||||||
@@ -170,9 +164,6 @@ test.describe('Feature Flags', () => {
|
|||||||
test('getServerFeature method works with real backend data', async ({
|
test('getServerFeature method works with real backend data', async ({
|
||||||
comfyPage
|
comfyPage
|
||||||
}) => {
|
}) => {
|
||||||
// Wait for connection
|
|
||||||
await comfyPage.page.waitForTimeout(1000)
|
|
||||||
|
|
||||||
// Test getServerFeature method
|
// Test getServerFeature method
|
||||||
const previewMetadataValue = await comfyPage.page.evaluate(() => {
|
const previewMetadataValue = await comfyPage.page.evaluate(() => {
|
||||||
return window['app'].api.getServerFeature('supports_preview_metadata')
|
return window['app'].api.getServerFeature('supports_preview_metadata')
|
||||||
@@ -199,9 +190,6 @@ test.describe('Feature Flags', () => {
|
|||||||
test('getServerFeatures returns all backend feature flags', async ({
|
test('getServerFeatures returns all backend feature flags', async ({
|
||||||
comfyPage
|
comfyPage
|
||||||
}) => {
|
}) => {
|
||||||
// Wait for connection
|
|
||||||
await comfyPage.page.waitForTimeout(1000)
|
|
||||||
|
|
||||||
// Test getServerFeatures returns all flags
|
// Test getServerFeatures returns all flags
|
||||||
const allFeatures = await comfyPage.page.evaluate(() => {
|
const allFeatures = await comfyPage.page.evaluate(() => {
|
||||||
return window['app'].api.getServerFeatures()
|
return window['app'].api.getServerFeatures()
|
||||||
@@ -248,9 +236,6 @@ test.describe('Feature Flags', () => {
|
|||||||
test('Server features are immutable when accessed via getServerFeatures', async ({
|
test('Server features are immutable when accessed via getServerFeatures', async ({
|
||||||
comfyPage
|
comfyPage
|
||||||
}) => {
|
}) => {
|
||||||
// Wait for connection to establish
|
|
||||||
await comfyPage.page.waitForTimeout(1000)
|
|
||||||
|
|
||||||
const immutabilityTest = await comfyPage.page.evaluate(() => {
|
const immutabilityTest = await comfyPage.page.evaluate(() => {
|
||||||
// Get a copy of server features
|
// Get a copy of server features
|
||||||
const features1 = window['app'].api.getServerFeatures()
|
const features1 = window['app'].api.getServerFeatures()
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 100 KiB |
@@ -104,8 +104,6 @@ test.describe('Group Node', () => {
|
|||||||
await comfyPage.setSetting('Comfy.EnableTooltips', true)
|
await comfyPage.setSetting('Comfy.EnableTooltips', true)
|
||||||
await comfyPage.convertAllNodesToGroupNode('Group Node')
|
await comfyPage.convertAllNodesToGroupNode('Group Node')
|
||||||
await comfyPage.page.mouse.move(47, 173)
|
await comfyPage.page.mouse.move(47, 173)
|
||||||
const tooltipTimeout = 500
|
|
||||||
await comfyPage.page.waitForTimeout(tooltipTimeout + 16)
|
|
||||||
await expect(comfyPage.page.locator('.node-tooltip')).toBeVisible()
|
await expect(comfyPage.page.locator('.node-tooltip')).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -320,14 +318,12 @@ test.describe('Group Node', () => {
|
|||||||
test('Convert to group node, no selection', async ({ comfyPage }) => {
|
test('Convert to group node, no selection', async ({ comfyPage }) => {
|
||||||
expect(await comfyPage.getVisibleToastCount()).toBe(0)
|
expect(await comfyPage.getVisibleToastCount()).toBe(0)
|
||||||
await comfyPage.page.keyboard.press('Alt+g')
|
await comfyPage.page.keyboard.press('Alt+g')
|
||||||
await comfyPage.page.waitForTimeout(300)
|
|
||||||
expect(await comfyPage.getVisibleToastCount()).toBe(1)
|
expect(await comfyPage.getVisibleToastCount()).toBe(1)
|
||||||
})
|
})
|
||||||
test('Convert to group node, selected 1 node', async ({ comfyPage }) => {
|
test('Convert to group node, selected 1 node', async ({ comfyPage }) => {
|
||||||
expect(await comfyPage.getVisibleToastCount()).toBe(0)
|
expect(await comfyPage.getVisibleToastCount()).toBe(0)
|
||||||
await comfyPage.clickTextEncodeNode1()
|
await comfyPage.clickTextEncodeNode1()
|
||||||
await comfyPage.page.keyboard.press('Alt+g')
|
await comfyPage.page.keyboard.press('Alt+g')
|
||||||
await comfyPage.page.waitForTimeout(300)
|
|
||||||
expect(await comfyPage.getVisibleToastCount()).toBe(1)
|
expect(await comfyPage.getVisibleToastCount()).toBe(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -306,16 +306,16 @@ test.describe('Node Interaction', () => {
|
|||||||
await comfyPage.canvas.click({
|
await comfyPage.canvas.click({
|
||||||
position: numberWidgetPos
|
position: numberWidgetPos
|
||||||
})
|
})
|
||||||
await expect(comfyPage.canvas).toHaveScreenshot('prompt-dialog-opened.png')
|
const legacyPrompt = comfyPage.page.locator('.graphdialog')
|
||||||
// Wait for 1s so that it does not trigger the search box by double click.
|
await expect(legacyPrompt).toBeVisible()
|
||||||
await comfyPage.page.waitForTimeout(1000)
|
await comfyPage.delay(300)
|
||||||
await comfyPage.canvas.click({
|
await comfyPage.canvas.click({
|
||||||
position: {
|
position: {
|
||||||
x: 10,
|
x: 10,
|
||||||
y: 10
|
y: 10
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
await expect(comfyPage.canvas).toHaveScreenshot('prompt-dialog-closed.png')
|
await expect(legacyPrompt).toBeHidden()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Can close prompt dialog with canvas click (text widget)', async ({
|
test('Can close prompt dialog with canvas click (text widget)', async ({
|
||||||
@@ -329,19 +329,16 @@ test.describe('Node Interaction', () => {
|
|||||||
await comfyPage.canvas.click({
|
await comfyPage.canvas.click({
|
||||||
position: textWidgetPos
|
position: textWidgetPos
|
||||||
})
|
})
|
||||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
const legacyPrompt = comfyPage.page.locator('.graphdialog')
|
||||||
'prompt-dialog-opened-text.png'
|
await expect(legacyPrompt).toBeVisible()
|
||||||
)
|
await comfyPage.delay(300)
|
||||||
await comfyPage.page.waitForTimeout(1000)
|
|
||||||
await comfyPage.canvas.click({
|
await comfyPage.canvas.click({
|
||||||
position: {
|
position: {
|
||||||
x: 10,
|
x: 10,
|
||||||
y: 10
|
y: 10
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
await expect(legacyPrompt).toBeHidden()
|
||||||
'prompt-dialog-closed-text.png'
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Can double click node title to edit', async ({ comfyPage }) => {
|
test('Can double click node title to edit', async ({ comfyPage }) => {
|
||||||
@@ -663,9 +660,6 @@ test.describe('Load workflow', () => {
|
|||||||
await comfyPage.loadWorkflow('nodes/single_ksampler')
|
await comfyPage.loadWorkflow('nodes/single_ksampler')
|
||||||
const node = (await comfyPage.getFirstNodeRef())!
|
const node = (await comfyPage.getFirstNodeRef())!
|
||||||
await node.click('collapse')
|
await node.click('collapse')
|
||||||
// Wait 300ms between 2 clicks so that it is not treated as a double click
|
|
||||||
// by litegraph.
|
|
||||||
await comfyPage.page.waitForTimeout(300)
|
|
||||||
await comfyPage.clickEmptySpace()
|
await comfyPage.clickEmptySpace()
|
||||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||||
'single_ksampler_modified.png'
|
'single_ksampler_modified.png'
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 98 KiB |