Compare commits

..

2 Commits

Author SHA1 Message Date
Chenlei Hu
702458acbd Refactor + error handling 2025-05-07 14:52:37 -04:00
Chenlei Hu
fd01f13fbf Update link_fixer from rgthree 2025-05-07 14:44:28 -04:00
378 changed files with 4773 additions and 63140 deletions

View File

@@ -1,43 +0,0 @@
# Add Missing i18n Translations
## Task: Add English translations for all new localized strings
### Step 1: Identify new translation keys
Find all translation keys that were added in the current branch's changes. These keys appear as arguments to translation functions: `t()`, `st()`, `$t()`, or similar i18n functions.
### Step 2: Add translations to English locale file
For each new translation key found, add the corresponding English text to the file `src/locales/en/main.json`.
### Key-to-JSON mapping rules:
- Translation keys use dot notation to represent nested JSON structure
- Convert dot notation to nested JSON objects when adding to the locale file
- Example: The key `g.user.name` maps to:
```json
{
"g": {
"user": {
"name": "User Name"
}
}
}
```
### Important notes:
1. **Only modify the English locale file** (`src/locales/en/main.json`)
2. **Do not modify other locale files** - translations for other languages are automatically generated by the `i18n.yaml` workflow
3. **Exception for manual translations**: Only add translations to non-English locale files if:
- You have specific domain knowledge that would produce a more accurate translation than the automated system
- The automated translation would likely be incorrect due to technical terminology or context-specific meaning
### Example workflow:
1. If you added `t('settings.advanced.enable')` in a Vue component
2. Add to `src/locales/en/main.json`:
```json
{
"settings": {
"advanced": {
"enable": "Enable advanced settings"
}
}
}
```

View File

@@ -1,111 +0,0 @@
Apply performance monitoring concepts from performance-test-guide.md to the specified test file: $ARGUMENTS
## Task Overview
Transform browser tests to include performance monitoring for canvas, node, and widget operations following the established performance testing patterns.
## Instructions
<analysis_phase>
1. **Read the target test file** specified in $ARGUMENTS
2. **Analyze test operations** to identify which ones should have performance monitoring based on the guide criteria:
-**Monitor**: Node operations, widget interactions, canvas operations, graph operations, background operations
-**Skip**: UI chrome elements, dialogs/modals, floating menus, gallery/template views
3. **Review existing test structure** to understand the test flow and key operations
</analysis_phase>
<implementation_phase>
4. **Add performance monitoring** following these steps:
**a. Import and setup:**
- Add `import { PerformanceMonitor } from '../helpers/performanceMonitor'`
- Add `@perf` tag to test name
- Initialize PerformanceMonitor with `comfyPage.page`
- Create descriptive kebab-case test name
- Call `startMonitoring(testName)`
**b. Wrap appropriate operations:**
- Use `measureOperation()` for node operations (creating, selecting, dragging, copying, deleting)
- Use `measureOperation()` for widget interactions (input changes, clicks, value modifications)
- Use `measureOperation()` for canvas operations (panning, zooming, selections, connections)
- Use `measureOperation()` for graph operations (loading workflows, undo/redo, batch operations)
- Use `markEvent()` for logical boundaries and state transitions
- Group related operations when they represent a single user action
- Keep assertions and expectations outside performance measurements
**c. Apply appropriate patterns:**
- **User Interaction Sequence**: Separate click, type, submit operations
- **Copy/Paste Operations**: Separate select, copy, paste with before/after marks
- **Drag Operations**: Separate start-drag, drag-to-position, drop
**d. Finalize:**
- Call `finishMonitoring(testName)` at the end
- Ensure all async operations are properly wrapped
</implementation_phase>
<naming_conventions>
- **Test names**: kebab-case, descriptive (e.g., 'copy-paste-multiple-nodes')
- **Operation names**: kebab-case, action-focused (e.g., 'click-node', 'drag-to-position')
- **Event marks**: kebab-case, state-focused (e.g., 'before-paste', 'after-render')
</naming_conventions>
<quality_guidelines>
- **Balance granularity**: Don't wrap every line, focus on meaningful operations
- **Maintain readability**: Wrapped code should remain clear and understandable
- **Preserve test logic**: Don't change test functionality, only add monitoring
- **Keep consistency**: Use similar operation names across similar tests
- **Group intelligently**: Combine related operations that represent single user actions
</quality_guidelines>
## Expected Output
Transform the test file to include:
1. Performance monitor import and initialization
2. `@perf` tag in test name
3. Appropriate `measureOperation()` wrapping for qualifying operations
4. `markEvent()` calls for logical boundaries
5. `finishMonitoring()` call at the end
6. Preserved test assertions and expectations outside performance measurements
Show the complete transformed test file with clear before/after comparison if the changes are substantial.
## Example Transformation Reference
Follow this pattern for transformation:
**Before:**
```typescript
test('Can copy and paste node', async ({ comfyPage }) => {
await comfyPage.clickEmptyLatentNode()
await comfyPage.ctrlC()
await comfyPage.ctrlV()
await expect(comfyPage.canvas).toHaveScreenshot('copied-node.png')
})
```
**After:**
```typescript
test('@perf Can copy and paste node', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'copy-paste-node'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('click-node', async () => {
await comfyPage.clickEmptyLatentNode()
})
await perfMonitor.measureOperation('copy-node', async () => {
await comfyPage.ctrlC()
})
await perfMonitor.measureOperation('paste-node', async () => {
await comfyPage.ctrlV()
})
await expect(comfyPage.canvas).toHaveScreenshot('copied-node.png')
await perfMonitor.finishMonitoring(testName)
})
```
Now apply these concepts to the test file: $ARGUMENTS

View File

@@ -1,53 +0,0 @@
# Create a Vue Widget for ComfyUI
Your task is to create a new Vue widget for ComfyUI based on the widget specification: $ARGUMENTS
## Instructions
Follow the comprehensive guide in `vue-widget-conversion/vue-widget-guide.md` to create the widget. This guide contains step-by-step instructions, examples from actual PRs, and best practices.
### Key Steps to Follow:
1. **Understand the Widget Type**
- Analyze what type of widget is needed: $ARGUMENTS
- Identify the data type (string, number, array, object, etc.)
- Determine if it needs special behaviors (execution state awareness, dynamic management, etc.)
2. **Component Creation**
- Create Vue component in `src/components/graph/widgets/`
- REQUIRED: Use PrimeVue components (reference `vue-widget-conversion/primevue-components.md`)
- Use Composition API with `<script setup>`
- Implement proper v-model binding with `defineModel`
3. **Composable Pattern**
- Always create widget constructor composable in `src/composables/widgets/`
- Only create node-level composable in `src/composables/node/` if the widget needs dynamic management
- Follow the dual composable pattern explained in the guide
4. **Registration**
- Register in `src/scripts/widgets.ts`
- Use appropriate widget type name
5. **Testing**
- Create unit tests for composables
- Test with actual nodes that use the widget
### Important Requirements:
- **Always use PrimeVue components** - Check `vue-widget-conversion/primevue-components.md` for available components
- Use TypeScript with proper types
- Follow Vue 3 Composition API patterns
- Use Tailwind CSS for styling (no custom CSS unless absolutely necessary)
- Implement proper error handling and validation
- Consider performance (use v-show vs v-if appropriately)
### Before Starting:
1. First read through the entire guide at `vue-widget-conversion/vue-widget-guide.md`
2. Check existing widget implementations for similar patterns
3. Identify which PrimeVue component(s) best fit the widget requirements
### Widget Specification to Implement:
$ARGUMENTS
Begin by analyzing the widget requirements and proposing an implementation plan based on the guide.

View File

@@ -1,57 +0,0 @@
Your task is to perform visual verification of our recent changes to ensure they display correctly in the browser. This verification is critical for catching visual regressions, layout issues, and ensuring our UI changes render properly for end users.
<instructions>
Follow these steps systematically to verify our changes:
1. **Server Setup**
- Check if the dev server is running on port 5173 using browser navigation or port checking
- If not running, start it with `npm run dev` from the root directory
- If the server fails to start, provide detailed troubleshooting steps by reading package.json and README.md for accurate instructions
- Wait for the server to be fully ready before proceeding
2. **Visual Testing Process**
- Navigate to http://localhost:5173/
- For each target page (specified in arguments or recently changed files):
* Navigate to the page using direct URL or site navigation
* Take a high-quality screenshot
* Analyze the screenshot for the specific changes we implemented
* Document any visual issues or improvements needed
3. **Quality Verification**
Check each page for:
- Content accuracy and completeness
- Proper styling and layout alignment
- Responsive design elements
- Navigation functionality
- Image loading and display
- Typography and readability
- Color scheme consistency
- Interactive elements (buttons, links, forms)
</instructions>
<examples>
Common issues to watch for:
- Broken layouts or overlapping elements
- Missing images or broken image links
- Inconsistent styling or spacing
- Navigation menu problems
- Mobile responsiveness issues
- Text overflow or truncation
- Color contrast problems
</examples>
<reporting>
For each page tested, provide:
1. Page URL and screenshot
2. Confirmation that changes display correctly OR detailed description of issues found
3. Any design improvement suggestions
4. Overall assessment of visual quality
If you find issues, be specific about:
- Exact location of the problem
- Expected vs actual behavior
- Severity level (critical, important, minor)
- Suggested fix if obvious
</reporting>
Remember: Take your time with each screenshot and analysis. Visual quality directly impacts user experience and our project's professional appearance.

View File

@@ -1,25 +1,26 @@
# Vue 3 Composition API Project Rules
// Vue 3 Composition API .cursorrules
## 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:
// Vue 3 Composition API best practices
const vue3CompositionApiBestPractices = [
"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
",
"Organize vue component in <template> <script> <style> order",
]
## Project Structure
```
// Folder structure
const folderStructure = `
src/
components/
constants/
@@ -29,25 +30,16 @@ src/
services/
App.vue
main.ts
```
`;
## Styling Guidelines
- Use Tailwind CSS for styling
- Implement responsive design with Tailwind CSS
// Tailwind CSS best practices
const tailwindCssBestPractices = [
"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
// Additional instructions
const additionalInstructions = `
1. Leverage VueUse functions for performance-enhancing styles
2. Use lodash for utility functions
3. Use TypeScript for type safety
@@ -57,5 +49,6 @@ DO NOT use deprecated PrimeVue components. Use these replacements instead:
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
10. Use vue-i18n in composition API for any string literals. Place new translation
entries in src/locales/en/main.json.
`;

View File

@@ -25,11 +25,3 @@ ENABLE_MINIFY=true
# templates are served via the normal method from the server's python site
# packages.
DISABLE_TEMPLATES_PROXY=false
# If playwright tests are being run via vite dev server, Vue plugins will
# invalidate screenshots. When `true`, vite plugins will not be loaded.
DISABLE_VUE_PLUGINS=false
# Algolia credentials required for developing with the new custom node manager.
ALGOLIA_APP_ID=4E0RO38HS8
ALGOLIA_API_KEY=684d998c36b67a9a9fce8fc2d8860579

View File

@@ -1,9 +1,7 @@
name: Bug Report
description: 'Something is not behaving as expected.'
title: '[Bug]: '
description: "Something is not behaving as expected."
title: "[Bug]: "
labels: ['Potential Bug']
type: Bug
body:
- type: markdown
attributes:
@@ -12,15 +10,8 @@ body:
- **1:** You are running the latest version of ComfyUI.
- **2:** You have looked at the existing bug reports and made sure this isn't already reported.
- type: checkboxes
id: custom-nodes-test
attributes:
label: Custom Node Testing
description: Please confirm you have tried to reproduce the issue with all custom nodes disabled.
options:
- label: I have tried disabling custom nodes and the issue persists (see [how to disable custom nodes](https://docs.comfy.org/troubleshooting/custom-node-issues#step-1%3A-test-with-all-custom-nodes-disabled) if you need help)
required: true
- **3:** You confirmed that the bug is not caused by a custom node. You can disable all custom nodes by passing
`--disable-all-custom-nodes` command line argument.
- type: textarea
attributes:

View File

@@ -1,8 +1,7 @@
name: Feature Request
description: Suggest an idea for this project
title: '[Feature Request]: '
labels: ['enhancement']
type: Feature
title: "[Feature Request]: "
labels: ["enhancement"]
body:
- type: checkboxes

View File

@@ -1,72 +0,0 @@
name: Create Dev PyPI Package
on:
workflow_dispatch:
inputs:
devVersion:
description: 'Dev version'
required: true
type: number
jobs:
build:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.current_version.outputs.version }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 'lts/*'
- name: Get current version
id: current_version
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
- name: Build project
env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
USE_PROD_CONFIG: 'true'
run: |
npm ci
npm run build
npm run zipdist
- name: Upload dist artifact
uses: actions/upload-artifact@v4
with:
name: dist-files
path: |
dist/
dist.zip
publish_pypi:
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download dist artifact
uses: actions/download-artifact@v4
with:
name: dist-files
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: Install build dependencies
run: python -m pip install build
- name: Setup pypi package
run: |
mkdir -p comfyui_frontend_package/comfyui_frontend_package/static/
cp -r dist/* comfyui_frontend_package/comfyui_frontend_package/static/
- name: Build pypi package
run: python -m build
working-directory: comfyui_frontend_package
env:
COMFYUI_FRONTEND_VERSION: ${{ format('{0}.dev{1}', needs.build.outputs.version, inputs.devVersion) }}
- name: Publish pypi package
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc
with:
password: ${{ secrets.PYPI_TOKEN }}
packages-dir: comfyui_frontend_package/dist

View File

@@ -136,7 +136,7 @@ jobs:
git commit -m "Update locales"
- name: Install SSH key For PUSH
uses: shimataro/ssh-key-action@d4fffb50872869abe2d9a9098a6d9c5aa7d16be4
uses: shimataro/ssh-key-action@v2
with:
# PR private key from action server
key: ${{ secrets.PR_SSH_PRIVATE_KEY }}

View File

@@ -33,7 +33,7 @@ jobs:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
working-directory: ComfyUI_frontend
- name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: "Update locales for node definitions"

View File

@@ -54,7 +54,7 @@ jobs:
name: dist-files
- name: Create release
id: create_release
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@@ -93,7 +93,7 @@ jobs:
env:
COMFYUI_FRONTEND_VERSION: ${{ needs.build.outputs.version }}
- name: Publish pypi package
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_TOKEN }}
packages-dir: comfyui_frontend_package/dist

View File

@@ -30,7 +30,7 @@ jobs:
with:
repository: 'Comfy-Org/ComfyUI_devtools'
path: 'ComfyUI/custom_nodes/ComfyUI_devtools'
ref: 'd05fd48dd787a4192e16802d4244cfcc0e2f9684'
ref: '9d2421fd3208a310e4d0f71fca2ea0c985759c33'
- uses: actions/setup-node@v4
with:

View File

@@ -30,7 +30,7 @@ jobs:
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
- name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: '[chore] Update electron-types to ${{ steps.get-version.outputs.NEW_VERSION }}'

View File

@@ -29,7 +29,7 @@ jobs:
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
- name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: '[chore] Update litegraph to ${{ steps.get-version.outputs.NEW_VERSION }}'

View File

@@ -1,92 +0,0 @@
name: Update ComfyUI-Manager API Types
on:
# Manual trigger
workflow_dispatch:
jobs:
update-manager-types:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Checkout ComfyUI-Manager repository
uses: actions/checkout@v4
with:
repository: Comfy-Org/ComfyUI-Manager
path: ComfyUI-Manager
clean: true
- name: Get Manager commit information
id: manager-info
run: |
cd ComfyUI-Manager
MANAGER_COMMIT=$(git rev-parse --short HEAD)
echo "commit=${MANAGER_COMMIT}" >> $GITHUB_OUTPUT
cd ..
- name: Generate Manager API types
run: |
echo "Generating TypeScript types from ComfyUI-Manager@${{ steps.manager-info.outputs.commit }}..."
npx openapi-typescript ./ComfyUI-Manager/openapi.yaml --output ./src/types/generatedManagerTypes.ts
- name: Validate generated types
run: |
if [ ! -f ./src/types/generatedManagerTypes.ts ]; then
echo "Error: Types file was not generated."
exit 1
fi
# Check if file is not empty
if [ ! -s ./src/types/generatedManagerTypes.ts ]; then
echo "Error: Generated types file is empty."
exit 1
fi
- name: Check for changes
id: check-changes
run: |
if [[ -z $(git status --porcelain ./src/types/generatedManagerTypes.ts) ]]; then
echo "No changes to ComfyUI-Manager API types detected."
echo "changed=false" >> $GITHUB_OUTPUT
exit 0
else
echo "Changes detected in ComfyUI-Manager API types."
echo "changed=true" >> $GITHUB_OUTPUT
fi
- name: Create Pull Request
if: steps.check-changes.outputs.changed == 'true'
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: '[chore] Update ComfyUI-Manager API types from ComfyUI-Manager@${{ steps.manager-info.outputs.commit }}'
title: '[chore] Update ComfyUI-Manager API types from ComfyUI-Manager@${{ steps.manager-info.outputs.commit }}'
body: |
## Automated API Type Update
This PR updates the ComfyUI-Manager API types from the latest ComfyUI-Manager OpenAPI specification.
- Manager commit: ${{ steps.manager-info.outputs.commit }}
- Generated on: ${{ github.event.repository.updated_at }}
These types are automatically generated using openapi-typescript.
branch: update-manager-types-${{ steps.manager-info.outputs.commit }}
base: main
labels: Manager
delete-branch: true
add-paths: |
src/types/generatedManagerTypes.ts

View File

@@ -75,7 +75,7 @@ jobs:
- name: Create Pull Request
if: steps.check-changes.outputs.changed == 'true'
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: '[chore] Update Comfy Registry API types from comfy-api@${{ steps.api-info.outputs.commit }}'

View File

@@ -38,7 +38,7 @@ jobs:
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
- name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: '[release] Bump version to ${{ steps.bump-version.outputs.NEW_VERSION }}'

View File

@@ -1,8 +0,0 @@
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["-y", "@executeautomation/playwright-mcp-server"]
}
}
}

View File

@@ -1,56 +0,0 @@
- use npm run to see what commands are available
- After making code changes, follow this general process: (1) Create unit tests, component tests, browser tests (if appropriate for each), (2) run unit tests, component tests, and browser tests until passing, (3) run typecheck, lint, format (with prettier) -- you can use `npm run` command to see the scripts available, (4) check if any READMEs (including nested) or documentation needs to be updated, (5) Decide whether the changes are worth adding new content to the external documentation for (or would requires changes to the external documentation) at https://docs.comfy.org, then present your suggestion
- When referencing PrimeVue, you can get all the docs here: https://primevue.org. Do this instead of making up or inferring names of Components
- When trying to set tailwind classes for dark theme, use "dark-theme:" prefix rather than "dark:"
- Never add lines to PR descriptions that say "Generated with Claude Code"
- When making PR names and commit messages, if you are going to add a prefix like "docs:", "feat:", "bugfix:", use square brackets around the prefix term and do not use a colon (e.g., should be "[docs]" rather than "docs:").
- When I reference GitHub Repos related to Comfy-Org, you should proactively fetch or read the associated information in the repo. To do so, you should exhaust all options: (1) Check if we have a local copy of the repo, (2) Use the GitHub API to fetch the information; you may want to do this IN ADDITION to the other options, especially for reading specific branches/PRs/comments/reviews/metadata, and (3) curl the GitHub website and parse the html or json responses
- For information about ComfyUI, ComfyUI_frontend, or ComfyUI-Manager, you can web search or download these wikis: https://deepwiki.com/Comfy-Org/ComfyUI-Manager, https://deepwiki.com/Comfy-Org/ComfyUI_frontend/1-overview, https://deepwiki.com/comfyanonymous/ComfyUI/2-core-architecture
- If a question/project is related to Comfy-Org, Comfy, or ComfyUI ecosystem, you should proactively use the Comfy docs to answer the question. The docs may be referenced with URLs like https://docs.comfy.org
- When operating inside a repo, check for README files at key locations in the repo detailing info about the contents of that folder. E.g., top-level key folders like tests-ui, browser_tests, composables, extensions/core, stores, services often have their own README.md files. When writing code, make sure to frequently reference these README files to understand the overall architecture and design of the project. Pay close attention to the snippets to learn particular patterns that seem to be there for a reason, as you should emulate those.
- Prefer running single tests, and not the whole test suite, for performance
- If using a lesser known or complex CLI tool, run the --help to see the documentation before deciding what to run, even if just for double-checking or verifying things.
- IMPORTANT: the most important goal when writing code is to create clean, best-practices, sustainable, and scalable public APIs and interfaces. Our app is used by thousands of users and we have thousands of mods/extensions that are constantly changing and updating; and we are also always updating. That's why it is IMPORTANT that we design systems and write code that follows practices of domain-driven design, object-oriented design, and design patterns (such that you can assure stability while allowing for all components around you to change and evolve). We ABSOLUTELY prioritize clean APIs and public interfaces that clearly define and restrict how/what the mods/extensions can access.
- If any of these technologies are referenced, you can proactively read their docs at these locations: https://primevue.org/theming, https://primevue.org/forms/, https://www.electronjs.org/docs/latest/api/browser-window, https://vitest.dev/guide/browser/, https://atlassian.design/components/pragmatic-drag-and-drop/core-package/drop-targets/, https://playwright.dev/docs/api/class-test, https://playwright.dev/docs/api/class-electron, https://www.algolia.com/doc/api-reference/rest-api/, https://pyav.org/docs/develop/cookbook/basics.html
- IMPORTANT: Never add Co-Authored by Claude or any reference to Claude or Claude Code in commit messages, PR descriptions, titles, or any documentation whatsoever
- The npm script to type check is called "typecheck" NOT "type check"
- Use the Vue 3 Composition API instead of the Options API when writing Vue components. An exception is when overriding or extending a PrimeVue component for compatibility, you may use the Options API.
- when we are solving an issue we know the link/number for, we should add "Fixes #n" (where n is the issue number) to the PR description.
- Never write css if you can accomplish the same thing with tailwind utility classes
- 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. Do not define a `props` variable; instead, destructure props. Since vue 3.5, destructuring props does not strip them of reactivity.
- Use Tailwind CSS for styling
- Leverage VueUse functions for performance-enhancing styles
- Use lodash for utility functions
- Use TypeScript for type safety
- Implement proper props and emits definitions
- Utilize Vue 3's Teleport component when needed
- Use Suspense for async components
- Implement proper error handling
- Follow Vue 3 style guide and naming conventions
- Use Vite for fast development and building
- Use vue-i18n in composition API for any string literals. Place new translation entries in src/locales/en/main.json.
- Avoid using `@ts-expect-error` to work around type issues. We needed to employ it to migrate to TypeScript, but it should not be viewed as an accepted practice or standard.
- 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
* Use `api.apiURL()` for all backend API calls and routes
- Actual API endpoints like /prompt, /queue, /view, etc.
- Image previews: `api.apiURL('/view?...')`
- Any backend-generated content or dynamic routes
* Use `api.fileURL()` for static files served from the public folder:
- Templates: `api.fileURL('/templates/default.json')`
- Extensions: `api.fileURL(extensionPath)` for loading JS modules
- Any static assets that exist in the public directory

118
README.md
View File

@@ -526,46 +526,14 @@ Have another idea? Drop into Discord or open an issue, and let's chat!
## Development
### Prerequisites & Technology Stack
### Tech Stack
- **Required Software**:
- Node.js (v16 or later) and npm
- Git for version control
- A running ComfyUI backend instance
- **Tech Stack**:
- [Vue 3](https://vuejs.org/) with [TypeScript](https://www.typescriptlang.org/)
- [Pinia](https://pinia.vuejs.org/) for state management
- [PrimeVue](https://primevue.org/) with [TailwindCSS](https://tailwindcss.com/) for UI
- [litegraph.js](https://github.com/Comfy-Org/litegraph.js) for node editor
- [zod](https://zod.dev/) for schema validation
- [vue-i18n](https://github.com/intlify/vue-i18n) for internationalization
### Initial Setup
1. Clone the repository:
```bash
git clone https://github.com/Comfy-Org/ComfyUI_frontend.git
cd ComfyUI_frontend
```
2. Install dependencies:
```bash
npm install
```
3. Configure environment (optional):
Create a `.env` file in the project root based on the provided [.env.example](.env.example) file.
**Note about ports**: By default, the dev server expects the ComfyUI backend at `localhost:8188`. If your ComfyUI instance runs on a different port, update this in your `.env` file.
### Dev Server Configuration
To launch ComfyUI and have it connect to your development server:
```bash
python main.py --port 8188
```
- [Vue 3](https://vuejs.org/) with [TypeScript](https://www.typescriptlang.org/)
- [Pinia](https://pinia.vuejs.org/) for state management
- [PrimeVue](https://primevue.org/) with [TailwindCSS](https://tailwindcss.com/) for UI
- [litegraph.js](https://github.com/Comfy-Org/litegraph.js) for node editor
- [zod](https://zod.dev/) for schema validation
- [vue-i18n](https://github.com/intlify/vue-i18n) for internationalization
### Git pre-commit hooks
@@ -607,69 +575,7 @@ navigate to `http://<server_ip>:5173` (e.g. `http://192.168.2.20:5173` here), to
This project includes `.vscode/launch.json.default` and `.vscode/settings.json.default` files with recommended launch and workspace settings for editors that use the `.vscode` directory (e.g., VS Code, Cursor, etc.).
We've also included a list of recommended extensions in `.vscode/extensions.json`. Your editor should detect this file and show a human friendly list in the Extensions panel, linking each entry to its marketplace page.
### Recommended MCP Integrations
#### Playwright Browser Automation
The Playwright MCP server enables LLM coding assistants (like Copilot, Claude Code, etc.) to control and inspect web browsers programmatically. This allows assistants to navigate websites, take screenshots, and interact with web pages on your behalf.
For ComfyUI_frontend development, you can ask coding assistants to use Playwright screenshots to continuously verify that your code changes produce the expected visual results.
##### Setup for Claude Code
After installing dependencies with `npm i`, the Playwright MCP server will be automatically available when you start Claude Code locally.
Here's how Claude Code can use the Playwright MCP server to inspect the interface of the local development server (assuming you're running the dev server at `localhost:5173`):
```
> navigate to localhost:5173 and take a screenshot.
tell me the items in the menubar
● I'll navigate to localhost:5173 and take a
screenshot to see the menubar items.
● playwright:playwright_navigate (MCP)(url: …
"http://local
host:5173")
⎿ Navigated to http://localhost:5173
● playwright:playwright_screenshot (MCP)(name: "lo…
alhost_5173
_menubar")
⎿ Screenshot saved to: ../../../Downloads/localho
st_5173_menubar-2025-05-30T17-23-00-942Z.png
⎿ Screenshot also stored in memory with name:
'localhost_5173_menubar'
● Read(../../../Downloads/localhost_5173_menubar-2…
25-05-30T17-23-00-942Z.png)
⎿ Read image (94.3KB)
● Based on the screenshot, I can see the menubar at
the top of the ComfyUI interface contains the
following items:
Main Menubar Items:
- Workflow - Menu for workflow-related actions
- Edit - Menu for editing operations
- Help - Menu for help and documentation
Additional UI Elements:
- ComfyUI Logo - On the far left
- Workflow Tab - Shows "Unsaved Workflow" with a
dropdown and close button
- Layout Controls - On the far right (grid view
and hamburger menu icons)
The interface shows a typical ComfyUI workflow
graph with nodes like "Load Checkpoint", "CLIP
Text Encode (Prompt)", "KSampler", and "Empty
Latent Image" connected with colored cables.
```
Weve also included a list of recommended extensions in `.vscode/extensions.json`. Your editor should detect this file and show a human friendly list in the Extensions panel, linking each entry to its marketplace page.
### Unit Test
@@ -700,11 +606,3 @@ This will replace the litegraph package in this repo with the local litegraph re
### i18n
See [locales/README.md](src/locales/README.md) for details.
## Troubleshooting
For comprehensive troubleshooting and technical support, please refer to our official documentation:
- **[General Troubleshooting Guide](https://docs.comfy.org/troubleshooting/overview)** - Common issues, performance optimization, and reporting bugs
- **[Custom Node Issues](https://docs.comfy.org/troubleshooting/custom-node-issues)** - Debugging custom node problems and conflicts
- **[Desktop Installation Guide](https://docs.comfy.org/installation/desktop/windows)** - Desktop-specific installation and troubleshooting

View File

@@ -1,6 +1,6 @@
# Playwright Testing for ComfyUI_frontend
This document outlines the setup, usage, and common patterns for Playwright browser tests in the ComfyUI_frontend project.
This document outlines the setup and usage of Playwright for testing the ComfyUI_frontend project.
## WARNING
@@ -31,7 +31,7 @@ If you are running Playwright tests in parallel or running the same test multipl
## Running Tests
There are multiple ways to run the tests:
There are two ways to run the tests:
1. **Headless mode with report generation:**
```bash
@@ -47,239 +47,14 @@ There are multiple ways to run the tests:
![Playwright UI Mode](https://github.com/user-attachments/assets/6a1ebef0-90eb-4157-8694-f5ee94d03755)
3. **Running specific tests:**
```bash
npx playwright test widget.spec.ts
```
## Test Structure
Browser tests in this project follow a specific organization pattern:
- **Fixtures**: Located in `fixtures/` - These provide test setup and utilities
- `ComfyPage.ts` - The main fixture for interacting with ComfyUI
- `ComfyMouse.ts` - Utility for mouse interactions with the canvas
- Components fixtures in `fixtures/components/` - Page object models for UI components
- **Tests**: Located in `tests/` - The actual test specifications
- Organized by functionality (e.g., `widget.spec.ts`, `interaction.spec.ts`)
- Snapshot directories (e.g., `widget.spec.ts-snapshots/`) contain reference screenshots
- **Utilities**: Located in `utils/` - Common utility functions
- `litegraphUtils.ts` - Utilities for working with LiteGraph nodes
## Writing Effective Tests
When writing new tests, follow these patterns:
### Test Structure
```typescript
// Import the test fixture
import { comfyPageFixture as test } from '../fixtures/ComfyPage';
test.describe('Feature Name', () => {
// Set up test environment if needed
test.beforeEach(async ({ comfyPage }) => {
// Common setup
});
test('should do something specific', async ({ comfyPage }) => {
// Test implementation
});
});
```
### Leverage Existing Fixtures and Helpers
Always check for existing helpers and fixtures before implementing new ones:
- **ComfyPage**: Main fixture with methods for canvas interaction and node management
- **ComfyMouse**: Helper for precise mouse operations on the canvas
- **Helpers**: Check `browser_tests/helpers/` for specialized helpers like:
- `actionbar.ts`: Interact with the action bar
- `manageGroupNode.ts`: Group node management operations
- `templates.ts`: Template workflows operations
- **Component Fixtures**: Check `browser_tests/fixtures/components/` for UI component helpers
- **Utility Functions**: Check `browser_tests/utils/` and `browser_tests/fixtures/utils/` for shared utilities
Most common testing needs are already addressed by these helpers, which will make your tests more consistent and reliable.
### Key Testing Patterns
1. **Focus elements explicitly**:
Canvas-based elements often need explicit focus before interaction:
```typescript
// Click the canvas first to focus it before pressing keys
await comfyPage.canvas.click();
await comfyPage.page.keyboard.press('a');
```
2. **Mark canvas as dirty if needed**:
Some interactions need explicit canvas updates:
```typescript
// After programmatically changing node state, mark canvas dirty
await comfyPage.page.evaluate(() => {
window['app'].graph.setDirtyCanvas(true, true);
});
```
3. **Use node references over coordinates**:
Node references from `fixtures/utils/litegraphUtils.ts` provide stable ways to interact with nodes:
```typescript
// Prefer this:
const node = await comfyPage.getNodeRefsByType('LoadImage')[0];
await node.click('title');
// Over this:
await comfyPage.canvas.click({ position: { x: 100, y: 100 } });
```
4. **Wait for canvas to render after UI interactions**:
```typescript
await comfyPage.nextFrame();
```
5. **Clean up persistent server state**:
While most state is reset between tests, anything stored on the server persists:
```typescript
// Reset settings that affect other tests (these are stored on server)
await comfyPage.setSetting('Comfy.ColorPalette', 'dark');
await comfyPage.setSetting('Comfy.NodeBadge.NodeIdBadgeMode', 'None');
// Clean up uploaded files if needed
await comfyPage.request.delete(`${comfyPage.url}/api/delete/image.png`);
```
6. **Prefer functional assertions over screenshots**:
Use screenshots only when visual verification is necessary:
```typescript
// Prefer this:
expect(await node.isPinned()).toBe(true);
expect(await node.getProperty('title')).toBe('Expected Title');
// Over this - only use when needed:
await expect(comfyPage.canvas).toHaveScreenshot('state.png');
```
7. **Use minimal test workflows**:
When creating test workflows, keep them as minimal as possible:
```typescript
// Include only the components needed for the test
await comfyPage.loadWorkflow('single_ksampler');
```
## Common Patterns and Utilities
### Page Object Pattern
Tests use the Page Object pattern to create abstractions over the UI:
```typescript
// Using the ComfyPage fixture
test('Can toggle boolean widget', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/boolean_widget')
const node = (await comfyPage.getFirstNodeRef())!
const widget = await node.getWidget(0)
await widget.click()
});
```
### Node References
The `NodeReference` class provides helpers for interacting with LiteGraph nodes:
```typescript
// Getting node by type and interacting with it
const nodes = await comfyPage.getNodeRefsByType('LoadImage')
const loadImageNode = nodes[0]
const widget = await loadImageNode.getWidget(0)
await widget.click()
```
### Visual Regression Testing
Tests use screenshot comparisons to verify UI state:
```typescript
// Take a screenshot and compare to reference
await expect(comfyPage.canvas).toHaveScreenshot('boolean_widget_toggled.png')
```
### Waiting for Animations
Always call `nextFrame()` after actions that trigger animations:
```typescript
await comfyPage.canvas.click({ position: { x: 100, y: 100 } })
await comfyPage.nextFrame() // Wait for canvas to redraw
```
### Mouse Interactions
Canvas operations use special helpers to ensure proper timing:
```typescript
// Using ComfyMouse for drag and drop
await comfyMouse.dragAndDrop(
{ x: 100, y: 100 }, // From
{ x: 200, y: 200 } // To
)
// Standard ComfyPage helpers
await comfyPage.drag({ x: 100, y: 100 }, { x: 200, y: 200 })
await comfyPage.pan({ x: 200, y: 200 })
await comfyPage.zoom(-100) // Zoom in
```
### Workflow Management
Tests use workflows stored in `assets/` for consistent starting points:
```typescript
// Load a test workflow
await comfyPage.loadWorkflow('single_ksampler')
// Wait for workflow to load and stabilize
await comfyPage.nextFrame()
```
### Custom Assertions
The project includes custom Playwright assertions through `comfyExpect`:
```typescript
// Check if a node is in a specific state
await expect(node).toBePinned()
await expect(node).toBeBypassed()
await expect(node).toBeCollapsed()
```
## Troubleshooting Common Issues
### Flaky Tests
- **Timing Issues**: Always wait for animations to complete with `nextFrame()`
- **Coordinate Sensitivity**: Canvas coordinates are viewport-relative; use node references when possible
- **Test Isolation**: Tests run in parallel; avoid dependencies between tests
- **Screenshots vary**: Ensure your OS and browser match the reference environment (Linux)
- **Async / await**: Race conditions are a very common cause of test flakiness
## Screenshot Expectations
Due to variations in system font rendering, screenshot expectations are platform-specific. Please note:
- **DO NOT commit local screenshot expectations** to the repository
- We maintain Linux screenshot expectations as our GitHub Action runner operates in a Linux environment
- While developing, you can generate local screenshots for your tests, but these will differ from CI-generated ones
- We maintain Linux screenshot expectations as our GitHub Action runner operates in a Linux environment.
- To set new test expectations:
1. Create a pull request from a `Comfy-Org/ComfyUI_frontend` branch.
2. Add the `New Browser Test Expectation` tag to your pull request.
3. This will trigger a GitHub action to update the screenshot expectations automatically.
To set new test expectations for PR:
1. Write your test with screenshot assertions using `toHaveScreenshot(filename)`
2. Create a pull request from a `Comfy-Org/ComfyUI_frontend` branch
3. Add the `New Browser Test Expectation` tag to your pull request
4. The GitHub CI will automatically generate and commit the reference screenshots
This approach ensures consistent screenshot expectations across all PRs and avoids issues with platform-specific rendering.
> **Note:** If you're making a pull request from a forked repository, the GitHub action won't be able to commit updated screenshot expectations directly to your PR branch.
> **Note:** If you're making a pull request from a forked repository, the GitHub action won't be able to commit updated screenshot expectations directly to your PR branch.

View File

@@ -33,11 +33,5 @@
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [0, 0],
"scale": 1
}
},
"version": 0.4
}

View File

@@ -130,11 +130,6 @@
],
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [0, 0],
"scale": 1
}
},
"extra": {},
"version": 0.4
}

View File

@@ -42,12 +42,12 @@
"config": {},
"extra": {
"ds": {
"scale": 1,
"scale": 2.1600300525920346,
"offset": [
0,
0
63.071794466403446,
75.18055335968394
]
}
},
"version": 0.4
}
}

View File

@@ -499,11 +499,6 @@
],
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [0, 0],
"scale": 1
}
},
"extra": {},
"version": 0.4
}
}

View File

@@ -1,85 +0,0 @@
{
"id": "1a95532f-c8aa-4c9d-a7f6-f928ba2d4862",
"revision": 0,
"last_node_id": 4,
"last_link_id": 3,
"nodes": [
{
"id": 4,
"type": "PreviewAny",
"pos": [946.2566528320312, 598.4373168945312],
"size": [140, 76],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "source",
"type": "*",
"link": 3
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewAny"
},
"widgets_values": []
},
{
"id": 1,
"type": "PreviewAny",
"pos": [951.0236206054688, 421.3861083984375],
"size": [140, 76],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "source",
"type": "*",
"link": 2
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewAny"
},
"widgets_values": []
},
{
"id": 3,
"type": "PrimitiveString",
"pos": [575.1760864257812, 504.5214538574219],
"size": [270, 58],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": [2, 3]
}
],
"properties": {
"Node name for S&R": "PrimitiveString"
},
"widgets_values": ["foo"]
}
],
"links": [
[2, 3, 0, 1, 0, "*"],
[3, 3, 0, 4, 0, "*"]
],
"groups": [],
"config": {},
"extra": {
"frontendVersion": "1.19.1",
"ds": {
"offset": [0, 0],
"scale": 1
}
},
"version": 0.4
}

View File

@@ -160,4 +160,4 @@
}
},
"version": 0.4
}
}

View File

@@ -43,10 +43,6 @@
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [0, 0],
"scale": 1
},
"groupNodes": {
"group_node": {
"nodes": [
@@ -405,4 +401,4 @@
}
},
"version": 0.4
}
}

View File

@@ -110,10 +110,6 @@
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [0, 0],
"scale": 1
},
"groupNodes": {
"hello": {
"nodes": [
@@ -253,4 +249,4 @@
}
},
"version": 0.4
}
}

View File

@@ -44,11 +44,6 @@
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [0, 0],
"scale": 1
}
},
"extra": {},
"version": 0.4
}

View File

@@ -61,11 +61,6 @@
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [0, 0],
"scale": 1
}
},
"extra": {},
"version": 0.4
}
}

View File

@@ -1,59 +0,0 @@
{
"last_node_id": 1,
"last_link_id": 0,
"nodes": [
{
"id": 1,
"type": "CheckpointLoaderSimple",
"pos": [256, 256],
"size": [315, 98],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": null
},
{
"name": "CLIP",
"type": "CLIP",
"links": null
},
{
"name": "VAE",
"type": "VAE",
"links": null
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple",
"models": [
{
"name": "outdated_model.safetensors",
"url": "http://localhost:8188/api/devtools/fake_model.safetensors",
"directory": "text_encoders"
},
{
"name": "another_outdated_model.safetensors",
"url": "http://localhost:8188/api/devtools/fake_model.safetensors",
"directory": "text_encoders"
}
]
},
"widgets_values": ["current_selected_model.safetensors"]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [0, 0],
"scale": 1
}
},
"version": 0.4
}

View File

@@ -1,36 +0,0 @@
{
"id": "5635564e-189f-49e4-9b25-6b7634bcd595",
"revision": 0,
"last_node_id": 78,
"last_link_id": 53,
"nodes": [
{
"id": 78,
"type": "DevToolsNodeWithV2ComboInput",
"pos": [1320, 904],
"size": [270.3199157714844, 58],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "COMBO",
"type": "COMBO",
"links": null
}
],
"properties": {
"Node name for S&R": "DevToolsNodeWithV2ComboInput"
},
"widgets_values": ["A"]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"frontendVersion": "1.19.7"
},
"version": 0.4
}

View File

@@ -50,11 +50,6 @@
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [0, 0],
"scale": 1
}
},
"extra": {},
"version": 0.4
}
}

View File

@@ -38,11 +38,7 @@
"groups": [],
"config": {},
"extra": {
"groupNodes": {},
"ds": {
"offset": [0, 0],
"scale": 1
}
"groupNodes": {}
},
"version": 0.4
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -54,11 +54,6 @@
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [0, 0],
"scale": 1
}
},
"extra": {},
"version": 0.4
}

View File

@@ -92,14 +92,10 @@
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [0, 0],
"scale": 1
},
"VHS_latentpreview": true,
"VHS_latentpreviewrate": 0,
"VHS_MetadataImage": false,
"VHS_KeepIntermediate": false
},
"version": 0.4
}
}

View File

@@ -84,10 +84,6 @@
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [0, 0],
"scale": 1
},
"reroutes": [
{
"id": 1,

View File

@@ -106,7 +106,10 @@
"extra": {
"ds": {
"scale": 1,
"offset": [0, 0]
"offset": {
"0": 0,
"1": 0
}
}
},
"version": 0.4

View File

@@ -368,10 +368,10 @@
"ds": {
"scale": 1,
"offset": [
0,
0
149.9747408641311,
383.8593224280729
]
}
},
"version": 0.4
}
}

View File

@@ -31,11 +31,5 @@
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [0, 0],
"scale": 1
}
},
"version": 0.4
}
}

View File

@@ -8,4 +8,4 @@
"title": "Load Animated Image"
}
}
}
}

View File

@@ -27,11 +27,6 @@
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [0, 0],
"scale": 1
}
},
"extra": {},
"version": 0.4
}

View File

@@ -41,11 +41,6 @@
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [0, 0],
"scale": 1
}
},
"extra": {},
"version": 0.4
}
}

View File

@@ -54,11 +54,7 @@
"groups": [],
"config": {},
"extra": {
"frontendVersion": "1.17.0",
"ds": {
"offset": [0, 0],
"scale": 1
}
"frontendVersion": "1.17.0"
},
"version": 0.4
}

View File

@@ -10,7 +10,6 @@ import type { KeyCombo } from '../../src/schemas/keyBindingSchema'
import type { useWorkspaceStore } from '../../src/stores/workspaceStore'
import { NodeBadgeMode } from '../../src/types/nodeSource'
import { ComfyActionbar } from '../helpers/actionbar'
import { PerformanceMonitor } from '../helpers/performanceMonitor'
import { ComfyTemplates } from '../helpers/templates'
import { ComfyMouse } from './ComfyMouse'
import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox'
@@ -134,9 +133,6 @@ export class ComfyPage {
// Inputs
public readonly workflowUploadInput: Locator
// Toasts
public readonly visibleToasts: Locator
// Components
public readonly searchBox: ComfyNodeSearchBox
public readonly menu: ComfyMenu
@@ -144,7 +140,6 @@ export class ComfyPage {
public readonly templates: ComfyTemplates
public readonly settingDialog: SettingDialog
public readonly confirmDialog: ConfirmDialog
public readonly performanceMonitor: PerformanceMonitor
/** Worker index to test user ID */
public readonly userIds: string[] = []
@@ -164,15 +159,12 @@ export class ComfyPage {
this.resetViewButton = page.getByRole('button', { name: 'Reset View' })
this.queueButton = page.getByRole('button', { name: 'Queue Prompt' })
this.workflowUploadInput = page.locator('#comfy-file-input')
this.visibleToasts = page.locator('.p-toast-message:visible')
this.searchBox = new ComfyNodeSearchBox(page)
this.menu = new ComfyMenu(page)
this.actionbar = new ComfyActionbar(page)
this.templates = new ComfyTemplates(page)
this.settingDialog = new SettingDialog(page)
this.confirmDialog = new ConfirmDialog(page)
this.performanceMonitor = new PerformanceMonitor(page)
}
convertLeafToContent(structure: FolderStructure): FolderStructure {
@@ -278,6 +270,7 @@ export class ComfyPage {
localStorage.clear()
sessionStorage.clear()
localStorage.setItem('Comfy.userId', id)
localStorage.setItem('api-nodes-news-seen', 'true')
}, this.id)
}
await this.goto()
@@ -404,30 +397,6 @@ export class ComfyPage {
await this.nextFrame()
}
async deleteWorkflow(
workflowName: string,
whenMissing: 'ignoreMissing' | 'throwIfMissing' = 'ignoreMissing'
) {
// Open workflows tab
const { workflowsTab } = this.menu
await workflowsTab.open()
// Action to take if workflow missing
if (whenMissing === 'ignoreMissing') {
const workflows = await workflowsTab.getTopLevelSavedWorkflowNames()
if (!workflows.includes(workflowName)) return
}
// Delete workflow
await workflowsTab.getPersistedItem(workflowName).click({ button: 'right' })
await this.clickContextMenuItem('Delete')
await this.confirmDialog.delete.click()
// Clear toast & close tab
await this.closeToasts(1)
await workflowsTab.close()
}
async resetView() {
if (await this.resetViewButton.isVisible()) {
await this.resetViewButton.click()
@@ -444,20 +413,7 @@ export class ComfyPage {
}
async getVisibleToastCount() {
return await this.visibleToasts.count()
}
async closeToasts(requireCount = 0) {
if (requireCount) await expect(this.visibleToasts).toHaveCount(requireCount)
// Clear all toasts
const toastCloseButtons = await this.page
.locator('.p-toast-close-button')
.all()
for (const button of toastCloseButtons) {
await button.click()
}
await expect(this.visibleToasts).toHaveCount(0)
return await this.page.locator('.p-toast-message:visible').count()
}
async clickTextEncodeNode1() {
@@ -765,7 +721,7 @@ export class ComfyPage {
y: 625
}
})
await this.page.mouse.move(10, 10)
this.page.mouse.move(10, 10)
await this.nextFrame()
}
@@ -777,7 +733,7 @@ export class ComfyPage {
},
button: 'right'
})
await this.page.mouse.move(10, 10)
this.page.mouse.move(10, 10)
await this.nextFrame()
}
@@ -1061,14 +1017,6 @@ export const comfyPageFixture = base.extend<{
const userId = await comfyPage.setupUser(username)
comfyPage.userIds[parallelIndex] = userId
// Enable performance monitoring for @perf tagged tests
const isPerformanceTest = testInfo.title.includes('@perf')
// console.log('test info', testInfo)
if (isPerformanceTest) {
console.log('Enabling performance monitoring')
// PerformanceMonitor.enable()
}
try {
await comfyPage.setupSettings({
'Comfy.UseNewMenu': 'Disabled',
@@ -1089,24 +1037,12 @@ export const comfyPageFixture = base.extend<{
console.error(e)
}
if (isPerformanceTest) {
// Start performance monitoring just before test execution
console.log('Starting performance monitoring')
await comfyPage.performanceMonitor.startMonitoring(testInfo.title)
}
await comfyPage.setup()
await use(comfyPage)
// Cleanup performance monitoring and collect final metrics
if (isPerformanceTest) {
console.log('Finishing performance monitoring')
await comfyPage.performanceMonitor.finishMonitoring(testInfo.title)
}
},
comfyMouse: async ({ comfyPage }, use) => {
const comfyMouse = new ComfyMouse(comfyPage)
void use(comfyMouse)
use(comfyMouse)
}
})

View File

@@ -1,30 +1,13 @@
import { FullConfig } from '@playwright/test'
import dotenv from 'dotenv'
import { PerformanceMonitor } from './helpers/performanceMonitor'
import { restorePath } from './utils/backupUtils'
dotenv.config()
export default async function globalTeardown(config: FullConfig) {
console.log('🧹 Global teardown starting...')
// Always try to save performance metrics (handles temp files from workers)
try {
const filePath = await PerformanceMonitor.saveMetricsToFile()
console.log(`✅ Performance metrics saved successfully to: ${filePath}`)
} catch (error) {
console.error(
'❌ Failed to save performance metrics in global teardown:',
error
)
}
// Existing teardown logic
export default function globalTeardown(config: FullConfig) {
if (!process.env.CI && process.env.TEST_COMFYUI_DIR) {
restorePath([process.env.TEST_COMFYUI_DIR, 'user'])
restorePath([process.env.TEST_COMFYUI_DIR, 'models'])
}
console.log('🧹 Global teardown completed')
}

View File

@@ -1,346 +0,0 @@
import type { Page, TestInfo } from '@playwright/test'
import * as fs from 'fs'
import * as path from 'path'
export interface PerformanceMetrics {
testName: string
timestamp: number
branch?: string
memoryUsage: {
usedJSHeapSize: number
totalJSHeapSize: number
jsHeapSizeLimit: number
} | null
timing: {
loadStart?: number
domContentLoaded?: number
loadComplete?: number
firstPaint?: number
firstContentfulPaint?: number
largestContentfulPaint?: number
}
customMetrics: Record<string, number>
}
export interface PerformanceRunSummary {
runId: string
timestamp: number
branch: string
gitCommit?: string
environment: {
nodeVersion: string
playwrightVersion: string
os: string
}
testMetrics: PerformanceMetrics[]
}
export class PerformanceMonitor {
private metrics: PerformanceMetrics[] = []
private static allMetrics: PerformanceMetrics[] = []
constructor(
private page: Page,
private testInfo?: TestInfo
) {}
async startMonitoring(testName: string) {
await this.page.evaluate((testName) => {
// Initialize performance monitoring
window.perfMonitor = {
testName,
startTime: performance.now(),
marks: new Map(),
measures: new Map()
}
// Mark test start
performance.mark(`${testName}-start`)
// Set up performance observer to capture paint metrics
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (
entry.entryType === 'paint' ||
entry.entryType === 'largest-contentful-paint'
) {
window.perfMonitor?.measures.set(entry.name, entry.startTime)
}
}
})
observer.observe({ entryTypes: ['paint', 'largest-contentful-paint'] })
}
}, testName)
}
async markEvent(eventName: string) {
await this.page.evaluate((eventName) => {
if (window.perfMonitor) {
const markName = `${window.perfMonitor.testName}-${eventName}`
performance.mark(markName)
window.perfMonitor.marks.set(
eventName,
performance.now() - window.perfMonitor.startTime
)
}
}, eventName)
}
async measureOperation<T>(
operationName: string,
operation: () => Promise<T>
): Promise<T> {
await this.markEvent(`${operationName}-start`)
const result = await operation()
await this.markEvent(`${operationName}-end`)
// Create performance measure
await this.page.evaluate((operationName) => {
if (window.perfMonitor) {
const testName = window.perfMonitor.testName
const startMark = `${testName}-${operationName}-start`
const endMark = `${testName}-${operationName}-end`
try {
performance.measure(`${operationName}`, startMark, endMark)
const measure = performance.getEntriesByName(`${operationName}`)[0]
window.perfMonitor.measures.set(operationName, measure.duration)
} catch (e) {
console.warn('Failed to create performance measure:', e)
}
}
}, operationName)
return result
}
async collectMetrics(
testName: string,
branch: string = 'unknown'
): Promise<PerformanceMetrics | null> {
const metrics = await this.page.evaluate(
({ testName, branch }) => {
if (!window.perfMonitor) return null
// Collect all performance data
const navigationEntry = performance.getEntriesByType(
'navigation'
)[0] as PerformanceNavigationTiming
const paintEntries = performance.getEntriesByType('paint')
const lcpEntries = performance.getEntriesByType(
'largest-contentful-paint'
)
const timing: any = {}
if (navigationEntry) {
timing.loadStart = navigationEntry.loadEventStart
timing.domContentLoaded = navigationEntry.domContentLoadedEventEnd
timing.loadComplete = navigationEntry.loadEventEnd
}
paintEntries.forEach((entry) => {
if (entry.name === 'first-paint') {
timing.firstPaint = entry.startTime
} else if (entry.name === 'first-contentful-paint') {
timing.firstContentfulPaint = entry.startTime
}
})
if (lcpEntries.length > 0) {
timing.largestContentfulPaint =
lcpEntries[lcpEntries.length - 1].startTime
}
const customMetrics: Record<string, number> = {}
window.perfMonitor.measures.forEach((value, key) => {
customMetrics[key] = value
})
return {
testName,
timestamp: Date.now(),
branch,
memoryUsage: performance.memory
? {
usedJSHeapSize: performance.memory.usedJSHeapSize,
totalJSHeapSize: performance.memory.totalJSHeapSize,
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit
}
: null,
timing,
customMetrics
}
},
{ testName, branch }
)
if (metrics) {
this.metrics.push(metrics)
PerformanceMonitor.allMetrics.push(metrics)
// Write individual metric file immediately for worker persistence
try {
const tempDir = path.join(process.cwd(), 'test-results', '.perf-temp')
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true })
}
const tempFile = path.join(
tempDir,
`metric-${Date.now()}-${Math.random().toString(36).substr(2, 9)}.json`
)
fs.writeFileSync(tempFile, JSON.stringify(metrics, null, 2))
} catch (error) {
console.warn('Failed to write temp metric file:', error)
}
console.log('PERFORMANCE_METRICS:', JSON.stringify(metrics))
console.log(
`📈 Total metrics collected so far: ${PerformanceMonitor.allMetrics.length}`
)
}
return metrics
}
async finishMonitoring(testName: string) {
await this.markEvent('test-end')
await this.collectMetrics(testName, 'vue-widget/perf-test')
console.log('Finishing performance monitoring')
// Print all metrics
console.log('PERFORMANCE_METRICS:', JSON.stringify(this.metrics))
// Cleanup
await this.page.evaluate(() => {
if (window.perfMonitor) {
delete window.perfMonitor
}
})
}
getAllMetrics(): PerformanceMetrics[] {
return this.metrics
}
static getAllCollectedMetrics(): PerformanceMetrics[] {
return PerformanceMonitor.allMetrics
}
static clearAllMetrics() {
PerformanceMonitor.allMetrics = []
}
static async saveMetricsToFile(outputPath?: string): Promise<string> {
// This runs in Node.js context (global teardown), not browser
if (typeof window !== 'undefined') {
throw new Error(
'saveMetricsToFile should only be called from Node.js context'
)
}
// Collect metrics from temp files (handles worker persistence)
const allMetrics: PerformanceMetrics[] = []
const tempDir = path.join(process.cwd(), 'test-results', '.perf-temp')
if (fs.existsSync(tempDir)) {
const tempFiles = fs
.readdirSync(tempDir)
.filter((f) => f.startsWith('metric-') && f.endsWith('.json'))
for (const file of tempFiles) {
try {
const content = fs.readFileSync(path.join(tempDir, file), 'utf8')
const metric = JSON.parse(content)
allMetrics.push(metric)
} catch (error) {
console.warn(`Failed to read temp metric file ${file}:`, error)
}
}
// Clean up temp files
try {
fs.rmSync(tempDir, { recursive: true, force: true })
} catch (error) {
console.warn('Failed to clean up temp directory:', error)
}
}
// Also include any metrics from static array (fallback)
allMetrics.push(...PerformanceMonitor.allMetrics)
const defaultPath = path.join(process.cwd(), 'test-results', 'performance')
const resultsDir = outputPath || defaultPath
// Ensure directory exists
if (!fs.existsSync(resultsDir)) {
fs.mkdirSync(resultsDir, { recursive: true })
}
const runId = `run-${new Date().toISOString().replace(/[:.]/g, '-')}`
const branch =
process.env.GIT_BRANCH ||
process.env.GITHUB_HEAD_REF ||
process.env.GITHUB_REF_NAME ||
'unknown'
// Get Playwright version more safely
let playwrightVersion = 'unknown'
try {
playwrightVersion = require('@playwright/test/package.json').version
} catch {
// Fallback if package.json not accessible
playwrightVersion = 'unknown'
}
const summary: PerformanceRunSummary = {
runId,
timestamp: Date.now(),
branch,
gitCommit: process.env.GITHUB_SHA || process.env.GIT_COMMIT,
environment: {
nodeVersion: process.version,
playwrightVersion,
os: process.platform
},
testMetrics: allMetrics
}
const fileName = `${runId}.json`
const filePath = path.join(resultsDir, fileName)
try {
fs.writeFileSync(filePath, JSON.stringify(summary, null, 2))
console.log(`\n📊 Performance metrics saved to: ${filePath}`)
console.log(`📈 Total tests measured: ${allMetrics.length}`)
// Also create/update a latest.json for easy access
const latestPath = path.join(resultsDir, 'latest.json')
fs.writeFileSync(latestPath, JSON.stringify(summary, null, 2))
return filePath
} catch (error) {
console.error('Failed to save performance metrics:', error)
throw error
}
}
}
// Extend window type for TypeScript
declare global {
interface Window {
perfMonitor?: {
testName: string
startTime: number
marks: Map<string, number>
measures: Map<string, number>
}
}
// Chrome-specific performance.memory extension
interface Performance {
memory?: {
usedJSHeapSize: number
totalJSHeapSize: number
jsHeapSizeLimit: number
}
}
}

View File

@@ -1,295 +0,0 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { PerformanceMonitor } from '../helpers/performanceMonitor'
test.describe('Background Image Upload', () => {
test.beforeEach(async ({ comfyPage }) => {
// Reset the background image setting before each test
await comfyPage.setSetting('Comfy.Canvas.BackgroundImage', '')
})
test.afterEach(async ({ comfyPage }) => {
// Clean up background image setting after each test
await comfyPage.setSetting('Comfy.Canvas.BackgroundImage', '')
})
test('should show background image upload component in settings', async ({
comfyPage
}) => {
// Open settings dialog
await comfyPage.page.keyboard.press('Control+,')
// Navigate to Appearance category
const appearanceOption = comfyPage.page.locator('text=Appearance')
await appearanceOption.click()
// Find the background image setting
const backgroundImageSetting = comfyPage.page.locator(
'#Comfy\\.Canvas\\.BackgroundImage'
)
await expect(backgroundImageSetting).toBeVisible()
// Verify the component has the expected elements using semantic selectors
const urlInput = backgroundImageSetting.locator('input[type="text"]')
await expect(urlInput).toBeVisible()
await expect(urlInput).toHaveAttribute('placeholder')
const uploadButton = backgroundImageSetting.locator(
'button:has(.pi-upload)'
)
await expect(uploadButton).toBeVisible()
const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)')
await expect(clearButton).toBeVisible()
await expect(clearButton).toBeDisabled() // Should be disabled when no image
})
test('@perf should upload image file and set as background', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'upload-background-image-file'
await perfMonitor.startMonitoring(testName)
// Open settings dialog
await comfyPage.page.keyboard.press('Control+,')
// Navigate to Appearance category
const appearanceOption = comfyPage.page.locator('text=Appearance')
await appearanceOption.click()
// Find the background image setting
const backgroundImageSetting = comfyPage.page.locator(
'#Comfy\\.Canvas\\.BackgroundImage'
)
// Click the upload button to trigger file input
const uploadButton = backgroundImageSetting.locator(
'button:has(.pi-upload)'
)
// Set up file upload handler and upload
await perfMonitor.measureOperation('trigger-file-upload', async () => {
const fileChooserPromise = comfyPage.page.waitForEvent('filechooser')
await uploadButton.click()
const fileChooser = await fileChooserPromise
await fileChooser.setFiles(comfyPage.assetPath('image32x32.webp'))
})
// Wait for upload to complete and verify the setting was updated
await perfMonitor.measureOperation('process-uploaded-file', async () => {
await comfyPage.page.waitForTimeout(500) // Give time for file reading
})
// Verify the URL input now has an API URL
const urlInput = backgroundImageSetting.locator('input[type="text"]')
const inputValue = await urlInput.inputValue()
expect(inputValue).toMatch(/^\/api\/view\?.*subfolder=backgrounds/)
// Verify clear button is now enabled
const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)')
await expect(clearButton).toBeEnabled()
// Verify the setting value was actually set
const settingValue = await comfyPage.getSetting(
'Comfy.Canvas.BackgroundImage'
)
expect(settingValue).toMatch(/^\/api\/view\?.*subfolder=backgrounds/)
await perfMonitor.finishMonitoring(testName)
})
test('@perf should accept URL input for background image', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'input-background-image-url'
await perfMonitor.startMonitoring(testName)
const testImageUrl = 'https://example.com/test-image.png'
// Open settings dialog
await comfyPage.page.keyboard.press('Control+,')
// Navigate to Appearance category
const appearanceOption = comfyPage.page.locator('text=Appearance')
await appearanceOption.click()
// Find the background image setting
const backgroundImageSetting = comfyPage.page.locator(
'#Comfy\\.Canvas\\.BackgroundImage'
)
// Enter URL in the input field
const urlInput = backgroundImageSetting.locator('input[type="text"]')
await perfMonitor.measureOperation('input-url-text', async () => {
await urlInput.fill(testImageUrl)
await urlInput.blur()
})
// Verify clear button is now enabled
const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)')
await expect(clearButton).toBeEnabled()
// Verify the setting value was updated
const settingValue = await comfyPage.getSetting(
'Comfy.Canvas.BackgroundImage'
)
expect(settingValue).toBe(testImageUrl)
await perfMonitor.finishMonitoring(testName)
})
test('@perf should clear background image when clear button is clicked', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'clear-background-image'
await perfMonitor.startMonitoring(testName)
const testImageUrl = 'https://example.com/test-image.png'
// First set a background image
await perfMonitor.measureOperation('set-initial-setting', async () => {
await comfyPage.setSetting('Comfy.Canvas.BackgroundImage', testImageUrl)
})
// Open settings dialog
await comfyPage.page.keyboard.press('Control+,')
// Navigate to Appearance category
const appearanceOption = comfyPage.page.locator('text=Appearance')
await appearanceOption.click()
// Find the background image setting
const backgroundImageSetting = comfyPage.page.locator(
'#Comfy\\.Canvas\\.BackgroundImage'
)
// Verify the input has the test URL
const urlInput = backgroundImageSetting.locator('input[type="text"]')
await expect(urlInput).toHaveValue(testImageUrl)
// Verify clear button is enabled
const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)')
await expect(clearButton).toBeEnabled()
// Click the clear button
await perfMonitor.measureOperation('click-clear-button', async () => {
await clearButton.click()
})
// Verify the input is now empty
await expect(urlInput).toHaveValue('')
// Verify clear button is now disabled
await expect(clearButton).toBeDisabled()
// Verify the setting value was cleared
const settingValue = await comfyPage.getSetting(
'Comfy.Canvas.BackgroundImage'
)
expect(settingValue).toBe('')
await perfMonitor.finishMonitoring(testName)
})
test('should show tooltip on upload and clear buttons', async ({
comfyPage
}) => {
// Open settings dialog
await comfyPage.page.keyboard.press('Control+,')
// Navigate to Appearance category
const appearanceOption = comfyPage.page.locator('text=Appearance')
await appearanceOption.click()
// Find the background image setting
const backgroundImageSetting = comfyPage.page.locator(
'#Comfy\\.Canvas\\.BackgroundImage'
)
// Hover over upload button and verify tooltip appears
const uploadButton = backgroundImageSetting.locator(
'button:has(.pi-upload)'
)
await uploadButton.hover()
// Wait for tooltip to appear and verify it exists
await comfyPage.page.waitForTimeout(700) // Tooltip delay
const uploadTooltip = comfyPage.page.locator('.p-tooltip:visible')
await expect(uploadTooltip).toBeVisible()
// Move away to hide tooltip
await comfyPage.page.locator('body').hover()
await comfyPage.page.waitForTimeout(100)
// Set a background to enable clear button
const urlInput = backgroundImageSetting.locator('input[type="text"]')
await urlInput.fill('https://example.com/test.png')
await urlInput.blur()
// Hover over clear button and verify tooltip appears
const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)')
await clearButton.hover()
// Wait for tooltip to appear and verify it exists
await comfyPage.page.waitForTimeout(700) // Tooltip delay
const clearTooltip = comfyPage.page.locator('.p-tooltip:visible')
await expect(clearTooltip).toBeVisible()
})
test('@perf should maintain reactive updates between URL input and clear button state', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'reactive-widget-updates'
await perfMonitor.startMonitoring(testName)
// Open settings dialog
await comfyPage.page.keyboard.press('Control+,')
// Navigate to Appearance category
const appearanceOption = comfyPage.page.locator('text=Appearance')
await appearanceOption.click()
// Find the background image setting
const backgroundImageSetting = comfyPage.page.locator(
'#Comfy\\.Canvas\\.BackgroundImage'
)
const urlInput = backgroundImageSetting.locator('input[type="text"]')
const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)')
// Initially clear button should be disabled
await expect(clearButton).toBeDisabled()
// Type some text - clear button should become enabled
await perfMonitor.measureOperation('input-partial-text', async () => {
await urlInput.fill('test')
})
await expect(clearButton).toBeEnabled()
// Clear the text manually - clear button should become disabled again
await perfMonitor.measureOperation('clear-input-manually', async () => {
await urlInput.fill('')
})
await expect(clearButton).toBeDisabled()
// Add text again - clear button should become enabled
await perfMonitor.measureOperation('input-full-url', async () => {
await urlInput.fill('https://example.com/image.png')
})
await expect(clearButton).toBeEnabled()
// Use clear button - should clear input and disable itself
await perfMonitor.measureOperation('clear-via-button', async () => {
await clearButton.click()
})
await expect(urlInput).toHaveValue('')
await expect(clearButton).toBeDisabled()
await perfMonitor.finishMonitoring(testName)
})
})

View File

@@ -3,7 +3,6 @@ import {
comfyExpect as expect,
comfyPageFixture as test
} from '../fixtures/ComfyPage'
import { PerformanceMonitor } from '../helpers/performanceMonitor'
async function beforeChange(comfyPage: ComfyPage) {
await comfyPage.page.evaluate(() => {
@@ -23,161 +22,93 @@ test.describe('Change Tracker', () => {
await comfyPage.setupWorkflowsDirectory({})
})
test.skip('@perf Can undo multiple operations', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'undo-multiple-operations'
await perfMonitor.startMonitoring(testName)
test('Can undo multiple operations', async ({ comfyPage }) => {
expect(await comfyPage.getUndoQueueSize()).toBe(0)
expect(await comfyPage.getRedoQueueSize()).toBe(0)
// Save, confirm no errors & workflow modified flag removed
await perfMonitor.measureOperation('save-workflow', async () => {
await comfyPage.menu.topbar.saveWorkflow('undo-redo-test')
})
await comfyPage.menu.topbar.saveWorkflow('undo-redo-test')
expect(await comfyPage.getToastErrorCount()).toBe(0)
expect(await comfyPage.isCurrentWorkflowModified()).toBe(false)
expect(await comfyPage.getUndoQueueSize()).toBe(0)
expect(await comfyPage.getRedoQueueSize()).toBe(0)
const node = (await comfyPage.getFirstNodeRef())!
await perfMonitor.measureOperation('click-node-title', async () => {
await node.click('title')
})
await perfMonitor.measureOperation('collapse-node', async () => {
await node.click('collapse')
})
await node.click('title')
await node.click('collapse')
await expect(node).toBeCollapsed()
expect(await comfyPage.isCurrentWorkflowModified()).toBe(true)
expect(await comfyPage.getUndoQueueSize()).toBe(1)
expect(await comfyPage.getRedoQueueSize()).toBe(0)
await perfMonitor.measureOperation('bypass-node', async () => {
await comfyPage.ctrlB()
})
await comfyPage.ctrlB()
await expect(node).toBeBypassed()
expect(await comfyPage.isCurrentWorkflowModified()).toBe(true)
expect(await comfyPage.getUndoQueueSize()).toBe(2)
expect(await comfyPage.getRedoQueueSize()).toBe(0)
await perfMonitor.markEvent('before-undo-operations')
await perfMonitor.measureOperation('undo-bypass', async () => {
await comfyPage.ctrlZ()
})
await comfyPage.ctrlZ()
await expect(node).not.toBeBypassed()
expect(await comfyPage.isCurrentWorkflowModified()).toBe(true)
expect(await comfyPage.getUndoQueueSize()).toBe(1)
expect(await comfyPage.getRedoQueueSize()).toBe(1)
await perfMonitor.measureOperation('undo-collapse', async () => {
await comfyPage.ctrlZ()
})
await comfyPage.ctrlZ()
await expect(node).not.toBeCollapsed()
expect(await comfyPage.isCurrentWorkflowModified()).toBe(false)
expect(await comfyPage.getUndoQueueSize()).toBe(0)
expect(await comfyPage.getRedoQueueSize()).toBe(2)
await perfMonitor.finishMonitoring(testName)
})
})
test('@perf Can group multiple change actions into a single transaction', async ({
test('Can group multiple change actions into a single transaction', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'group-change-transactions'
await perfMonitor.startMonitoring(testName)
const node = (await comfyPage.getFirstNodeRef())!
expect(node).toBeTruthy()
await expect(node).not.toBeCollapsed()
await expect(node).not.toBeBypassed()
await perfMonitor.markEvent('individual-changes-start')
// Make changes outside set
// Bypass + collapse node
await perfMonitor.measureOperation('click-node-title', async () => {
await node.click('title')
})
await perfMonitor.measureOperation('collapse-node', async () => {
await node.click('collapse')
})
await perfMonitor.measureOperation('bypass-node', async () => {
await comfyPage.ctrlB()
})
await node.click('title')
await node.click('collapse')
await comfyPage.ctrlB()
await expect(node).toBeCollapsed()
await expect(node).toBeBypassed()
// Undo, undo, ensure both changes undone
await perfMonitor.measureOperation('undo-bypass', async () => {
await comfyPage.ctrlZ()
})
await comfyPage.ctrlZ()
await expect(node).not.toBeBypassed()
await expect(node).toBeCollapsed()
await perfMonitor.measureOperation('undo-collapse', async () => {
await comfyPage.ctrlZ()
})
await comfyPage.ctrlZ()
await expect(node).not.toBeBypassed()
await expect(node).not.toBeCollapsed()
// Prevent clicks registering a double-click
await perfMonitor.measureOperation('click-empty-space', async () => {
await comfyPage.clickEmptySpace()
})
await perfMonitor.measureOperation('click-node-title-again', async () => {
await node.click('title')
})
await perfMonitor.markEvent('transaction-changes-start')
await comfyPage.clickEmptySpace()
await node.click('title')
// Run again, but within a change transaction
await perfMonitor.measureOperation('begin-change-transaction', async () => {
await beforeChange(comfyPage)
})
await beforeChange(comfyPage)
await perfMonitor.measureOperation('collapse-in-transaction', async () => {
await node.click('collapse')
})
await perfMonitor.measureOperation('bypass-in-transaction', async () => {
await comfyPage.ctrlB()
})
await node.click('collapse')
await comfyPage.ctrlB()
await expect(node).toBeCollapsed()
await expect(node).toBeBypassed()
// End transaction
await perfMonitor.measureOperation('end-change-transaction', async () => {
await afterChange(comfyPage)
})
await afterChange(comfyPage)
// Ensure undo reverts both changes
await perfMonitor.measureOperation('undo-transaction', async () => {
await comfyPage.ctrlZ()
})
await comfyPage.ctrlZ()
await expect(node).not.toBeBypassed()
await expect(node).not.toBeCollapsed()
await perfMonitor.finishMonitoring(testName)
})
test('@perf Can nest multiple change transactions without adding undo steps', async ({
test('Can nest multiple change transactions without adding undo steps', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'nested-change-transactions'
await perfMonitor.startMonitoring(testName)
const node = (await comfyPage.getFirstNodeRef())!
const bypassAndPin = async () => {
await beforeChange(comfyPage)
@@ -205,67 +136,32 @@ test.describe('Change Tracker', () => {
await afterChange(comfyPage)
}
await perfMonitor.measureOperation(
'execute-nested-transactions',
async () => {
await multipleChanges()
}
)
await multipleChanges()
await perfMonitor.measureOperation('undo-all-changes', async () => {
await comfyPage.ctrlZ()
})
await comfyPage.ctrlZ()
await expect(node).not.toBeBypassed()
await expect(node).not.toBePinned()
await expect(node).not.toBeCollapsed()
await perfMonitor.measureOperation('redo-all-changes', async () => {
await comfyPage.ctrlY()
})
await comfyPage.ctrlY()
await expect(node).toBeBypassed()
await expect(node).toBePinned()
await expect(node).toBeCollapsed()
await perfMonitor.finishMonitoring(testName)
})
// Skip because fails with vue widget nodes (reason not investigated)
test.skip('@perf Can detect changes in workflow.extra', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'detect-workflow-extra-changes'
await perfMonitor.startMonitoring(testName)
test('Can detect changes in workflow.extra', async ({ comfyPage }) => {
expect(await comfyPage.getUndoQueueSize()).toBe(0)
await perfMonitor.measureOperation('modify-workflow-extra', async () => {
await comfyPage.page.evaluate(() => {
window['app'].graph.extra.foo = 'bar'
})
await comfyPage.page.evaluate(() => {
window['app'].graph.extra.foo = 'bar'
})
// Click empty space to trigger a change detection.
await perfMonitor.measureOperation('trigger-change-detection', async () => {
await comfyPage.clickEmptySpace()
})
await comfyPage.clickEmptySpace()
expect(await comfyPage.getUndoQueueSize()).toBe(1)
await perfMonitor.finishMonitoring(testName)
})
test('@perf Ignores changes in workflow.ds', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'ignore-workflow-ds-changes'
await perfMonitor.startMonitoring(testName)
test('Ignores changes in workflow.ds', async ({ comfyPage }) => {
expect(await comfyPage.getUndoQueueSize()).toBe(0)
await perfMonitor.measureOperation('pan-canvas', async () => {
await comfyPage.pan({ x: 10, y: 10 })
})
await comfyPage.pan({ x: 10, y: 10 })
expect(await comfyPage.getUndoQueueSize()).toBe(0)
await perfMonitor.finishMonitoring(testName)
})
})

View File

@@ -1,182 +0,0 @@
import { Page, expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { PerformanceMonitor } from '../helpers/performanceMonitor'
interface ChatHistoryEntry {
prompt: string
response: string
response_id: string
}
async function renderChatHistory(page: Page, history: ChatHistoryEntry[]) {
const nodeId = await page.evaluate(() => window['app'].graph.nodes[0]?.id)
// Simulate API sending display_component message
await page.evaluate(
({ nodeId, history }) => {
const event = new CustomEvent('display_component', {
detail: {
node_id: nodeId,
component: 'ChatHistoryWidget',
props: {
history: JSON.stringify(history)
}
}
})
window['app'].api.dispatchEvent(event)
return true
},
{ nodeId, history }
)
return nodeId
}
test.describe('Chat History Widget', () => {
let nodeId: string
test.beforeEach(async ({ comfyPage }) => {
nodeId = await renderChatHistory(comfyPage.page, [
{ prompt: 'Hello', response: 'World', response_id: '123' }
])
// Wait for chat history to be rendered
await comfyPage.page.waitForSelector('.pi-pencil')
})
// Skip because fails with vue widget nodes (reason not investigated)
test.skip('@perf displays chat history when receiving display_component message', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'display-chat-history-component'
await perfMonitor.startMonitoring(testName)
// Verify the chat history is displayed correctly
await expect(comfyPage.page.getByText('Hello')).toBeVisible()
await expect(comfyPage.page.getByText('World')).toBeVisible()
await perfMonitor.finishMonitoring(testName)
})
test.skip('@perf handles message editing interaction', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'message-editing-interaction'
await perfMonitor.startMonitoring(testName)
// Get first node's ID
await perfMonitor.measureOperation('setup-node-widgets', async () => {
nodeId = await comfyPage.page.evaluate(() => {
const node = window['app'].graph.nodes[0]
// Make sure the node has a prompt widget (for editing functionality)
if (!node.widgets) {
node.widgets = []
}
// Add a prompt widget if it doesn't exist
if (!node.widgets.find((w) => w.name === 'prompt')) {
node.widgets.push({
name: 'prompt',
type: 'text',
value: 'Original prompt'
})
}
return node.id
})
})
await perfMonitor.measureOperation('render-chat-history', async () => {
await renderChatHistory(comfyPage.page, [
{
prompt: 'Message 1',
response: 'Response 1',
response_id: '123'
},
{
prompt: 'Message 2',
response: 'Response 2',
response_id: '456'
}
])
await comfyPage.page.waitForSelector('.pi-pencil')
})
const originalTextAreaInput = await comfyPage.page
.getByPlaceholder('text')
.nth(1)
.inputValue()
// Click edit button on first message
await perfMonitor.measureOperation('click-edit-button', async () => {
await comfyPage.page.getByLabel('Edit').first().click()
await comfyPage.nextFrame()
})
// Verify cancel button appears
await expect(comfyPage.page.getByLabel('Cancel')).toBeVisible()
// Click cancel edit
await perfMonitor.measureOperation('click-cancel-button', async () => {
await comfyPage.page.getByLabel('Cancel').click()
})
// Verify prompt input is restored
await expect(comfyPage.page.getByPlaceholder('text').nth(1)).toHaveValue(
originalTextAreaInput
)
await perfMonitor.finishMonitoring(testName)
})
test.skip('@perf handles real-time updates to chat history', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'real-time-chat-history-updates'
await perfMonitor.startMonitoring(testName)
// Send initial history
await perfMonitor.measureOperation('render-initial-history', async () => {
await renderChatHistory(comfyPage.page, [
{
prompt: 'Initial message',
response: 'Initial response',
response_id: '123'
}
])
await comfyPage.page.waitForSelector('.pi-pencil')
})
await perfMonitor.markEvent('before-history-update')
// Update history with additional messages
await perfMonitor.measureOperation('update-chat-history', async () => {
await renderChatHistory(comfyPage.page, [
{
prompt: 'Follow-up',
response: 'New response',
response_id: '456'
}
])
await comfyPage.page.waitForSelector('.pi-pencil')
})
// Move mouse over the canvas to force update
await perfMonitor.measureOperation('trigger-canvas-update', async () => {
await comfyPage.page.mouse.move(100, 100)
await comfyPage.nextFrame()
})
await perfMonitor.markEvent('after-canvas-update')
// Verify new messages appear
await expect(comfyPage.page.getByText('Follow-up')).toBeVisible()
await expect(comfyPage.page.getByText('New response')).toBeVisible()
await perfMonitor.finishMonitoring(testName)
})
})

View File

@@ -2,7 +2,6 @@ import { expect } from '@playwright/test'
import type { Palette } from '../../src/schemas/colorPaletteSchema'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { PerformanceMonitor } from '../helpers/performanceMonitor'
const customColorPalettes: Record<string, Palette> = {
obsidian: {
@@ -149,99 +148,45 @@ const customColorPalettes: Record<string, Palette> = {
}
test.describe('Color Palette', () => {
test('@perf Can show custom color palette', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'show-custom-color-palette'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('set-custom-palettes', async () => {
await comfyPage.setSetting(
'Comfy.CustomColorPalettes',
customColorPalettes
)
})
test('Can show custom color palette', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.CustomColorPalettes', customColorPalettes)
// Reload to apply the new setting. Setting Comfy.CustomColorPalettes directly
// doesn't update the store immediately.
await perfMonitor.measureOperation('reload-page', async () => {
await comfyPage.setup()
})
await perfMonitor.measureOperation('load-workflow', async () => {
await comfyPage.loadWorkflow('every_node_color')
})
await perfMonitor.measureOperation(
'apply-obsidian-dark-palette',
async () => {
await comfyPage.setSetting('Comfy.ColorPalette', 'obsidian_dark')
}
)
await comfyPage.setup()
await comfyPage.loadWorkflow('every_node_color')
await comfyPage.setSetting('Comfy.ColorPalette', 'obsidian_dark')
await expect(comfyPage.canvas).toHaveScreenshot(
'custom-color-palette-obsidian-dark-all-colors.png'
)
await perfMonitor.measureOperation('apply-light-red-palette', async () => {
await comfyPage.setSetting('Comfy.ColorPalette', 'light_red')
await comfyPage.nextFrame()
})
await comfyPage.setSetting('Comfy.ColorPalette', 'light_red')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'custom-color-palette-light-red.png'
)
await perfMonitor.measureOperation(
'apply-default-dark-palette',
async () => {
await comfyPage.setSetting('Comfy.ColorPalette', 'dark')
await comfyPage.nextFrame()
}
)
await comfyPage.setSetting('Comfy.ColorPalette', 'dark')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('default-color-palette.png')
await perfMonitor.finishMonitoring(testName)
})
test.skip('@perf Can add custom color palette', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'add-custom-color-palette'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('add-palette-via-api', async () => {
await comfyPage.page.evaluate((p) => {
window['app'].extensionManager.colorPalette.addCustomColorPalette(p)
}, customColorPalettes.obsidian_dark)
})
test('Can add custom color palette', async ({ comfyPage }) => {
await comfyPage.page.evaluate((p) => {
window['app'].extensionManager.colorPalette.addCustomColorPalette(p)
}, customColorPalettes.obsidian_dark)
expect(await comfyPage.getToastErrorCount()).toBe(0)
await perfMonitor.measureOperation('apply-custom-palette', async () => {
await comfyPage.setSetting('Comfy.ColorPalette', 'obsidian_dark')
await comfyPage.nextFrame()
})
await comfyPage.setSetting('Comfy.ColorPalette', 'obsidian_dark')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'custom-color-palette-obsidian-dark.png'
)
// Legacy `custom_` prefix is still supported
await perfMonitor.measureOperation(
'apply-custom-palette-legacy-prefix',
async () => {
await comfyPage.setSetting('Comfy.ColorPalette', 'custom_obsidian_dark')
await comfyPage.nextFrame()
}
)
await comfyPage.setSetting('Comfy.ColorPalette', 'custom_obsidian_dark')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'custom-color-palette-obsidian-dark.png'
)
await perfMonitor.finishMonitoring(testName)
})
})
@@ -250,121 +195,58 @@ test.describe('Node Color Adjustments', () => {
await comfyPage.loadWorkflow('every_node_color')
})
test('@perf should adjust opacity via node opacity setting', async ({
test('should adjust opacity via node opacity setting', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'adjust-node-opacity'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('set-opacity-0-5', async () => {
await comfyPage.setSetting('Comfy.Node.Opacity', 0.5)
await comfyPage.page.waitForTimeout(128)
})
await comfyPage.setSetting('Comfy.Node.Opacity', 0.5)
await comfyPage.page.waitForTimeout(128)
// Drag mouse to force canvas to redraw
await perfMonitor.measureOperation('redraw-canvas', async () => {
await comfyPage.page.mouse.move(0, 0)
})
await comfyPage.page.mouse.move(0, 0)
await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-0.5.png')
await perfMonitor.measureOperation('set-opacity-1-0', async () => {
await comfyPage.setSetting('Comfy.Node.Opacity', 1.0)
await comfyPage.page.waitForTimeout(128)
})
await perfMonitor.measureOperation(
'redraw-canvas-full-opacity',
async () => {
await comfyPage.page.mouse.move(8, 8)
}
)
await comfyPage.setSetting('Comfy.Node.Opacity', 1.0)
await comfyPage.page.waitForTimeout(128)
await comfyPage.page.mouse.move(8, 8)
await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-1.png')
await perfMonitor.finishMonitoring(testName)
})
test('@perf should persist color adjustments when changing themes', async ({
test('should persist color adjustments when changing themes', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'persist-opacity-across-themes'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('set-opacity-and-theme', async () => {
await comfyPage.setSetting('Comfy.Node.Opacity', 0.2)
await comfyPage.setSetting('Comfy.ColorPalette', 'arc')
await comfyPage.nextFrame()
})
await perfMonitor.measureOperation('redraw-canvas-with-theme', async () => {
await comfyPage.page.mouse.move(0, 0)
})
await comfyPage.setSetting('Comfy.Node.Opacity', 0.2)
await comfyPage.setSetting('Comfy.ColorPalette', 'arc')
await comfyPage.nextFrame()
await comfyPage.page.mouse.move(0, 0)
await expect(comfyPage.canvas).toHaveScreenshot(
'node-opacity-0.2-arc-theme.png'
)
await perfMonitor.finishMonitoring(testName)
})
test('@perf should not serialize color adjustments in workflow', async ({
test('should not serialize color adjustments in workflow', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'workflow-serialization-color-adjustments'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('apply-color-settings', async () => {
await comfyPage.setSetting('Comfy.Node.Opacity', 0.5)
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
})
await comfyPage.setSetting('Comfy.Node.Opacity', 0.5)
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
const saveWorkflowInterval = 1000
await perfMonitor.measureOperation('wait-for-workflow-save', async () => {
await comfyPage.page.waitForTimeout(saveWorkflowInterval)
await comfyPage.page.waitForTimeout(saveWorkflowInterval)
const workflow = await comfyPage.page.evaluate(() => {
return localStorage.getItem('workflow')
})
let workflow: string | null
await perfMonitor.measureOperation(
'get-workflow-from-storage',
async () => {
workflow = await comfyPage.page.evaluate(() => {
return localStorage.getItem('workflow')
})
}
)
for (const node of JSON.parse(workflow! ?? '{}').nodes) {
for (const node of JSON.parse(workflow ?? '{}').nodes) {
if (node.bgcolor) expect(node.bgcolor).not.toMatch(/hsla/)
if (node.color) expect(node.color).not.toMatch(/hsla/)
}
await perfMonitor.finishMonitoring(testName)
})
// Skip because fails with vue widget nodes (reason not investigated)
test.skip('@perf should lighten node colors when switching to light theme', async ({
test('should lighten node colors when switching to light theme', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'lighten-colors-light-theme'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('apply-light-theme', async () => {
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.nextFrame()
})
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('node-lightened-colors.png')
await perfMonitor.finishMonitoring(testName)
})
test.describe('Context menu color adjustments', () => {
@@ -375,48 +257,26 @@ test.describe('Node Color Adjustments', () => {
await node?.clickContextMenuOption('Colors')
})
test('@perf should persist color adjustments when changing custom node colors', async ({
test('should persist color adjustments when changing custom node colors', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'persist-opacity-color-change'
await perfMonitor.startMonitoring(testName)
// Context menu interaction - monitor the node color change operation
await perfMonitor.measureOperation('select-red-color', async () => {
await comfyPage.page
.locator('.litemenu-entry.submenu span:has-text("red")')
.click()
})
await comfyPage.page
.locator('.litemenu-entry.submenu span:has-text("red")')
.click()
await expect(comfyPage.canvas).toHaveScreenshot(
'node-opacity-0.3-color-changed.png'
)
await perfMonitor.finishMonitoring(testName)
})
test('@perf should persist color adjustments when removing custom node color', async ({
test('should persist color adjustments when removing custom node color', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'persist-opacity-color-removal'
await perfMonitor.startMonitoring(testName)
// Context menu interaction - monitor the node color removal operation
await perfMonitor.measureOperation('remove-node-color', async () => {
await comfyPage.page
.locator('.litemenu-entry.submenu span:has-text("No color")')
.click()
})
await comfyPage.page
.locator('.litemenu-entry.submenu span:has-text("No color")')
.click()
await expect(comfyPage.canvas).toHaveScreenshot(
'node-opacity-0.3-color-removed.png'
)
await perfMonitor.finishMonitoring(testName)
})
})
})

View File

@@ -1,7 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { PerformanceMonitor } from '../helpers/performanceMonitor'
test.describe('Copy Paste', () => {
test('Can copy and paste node', async ({ comfyPage }) => {
@@ -12,290 +11,107 @@ test.describe('Copy Paste', () => {
await expect(comfyPage.canvas).toHaveScreenshot('copied-node.png')
})
test('@perf Can copy and paste node with link', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'copy-paste-node-with-link'
await perfMonitor.startMonitoring(testName)
// Click node with performance tracking
await perfMonitor.measureOperation('click-text-encode-node', async () => {
await comfyPage.clickTextEncodeNode1()
})
// Mouse move with performance tracking
await perfMonitor.measureOperation('mouse-move', async () => {
await comfyPage.page.mouse.move(10, 10)
})
// Copy operation with performance tracking
await perfMonitor.measureOperation('copy-operation', async () => {
await comfyPage.ctrlC()
})
// Mark before paste
await perfMonitor.markEvent('before-paste')
// Paste operation with performance tracking
await perfMonitor.measureOperation('paste-operation', async () => {
await comfyPage.page.keyboard.press('Control+Shift+V')
})
// Mark after paste
await perfMonitor.markEvent('after-paste')
await perfMonitor.finishMonitoring(testName)
test('Can copy and paste node with link', async ({ comfyPage }) => {
await comfyPage.clickTextEncodeNode1()
await comfyPage.page.mouse.move(10, 10)
await comfyPage.ctrlC()
await comfyPage.page.keyboard.press('Control+Shift+V')
await expect(comfyPage.canvas).toHaveScreenshot('copied-node-with-link.png')
})
test('@perf Can copy and paste text', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'copy-paste-text'
await perfMonitor.startMonitoring(testName)
test('Can copy and paste text', async ({ comfyPage }) => {
const textBox = comfyPage.widgetTextBox
await perfMonitor.measureOperation('click-textbox', async () => {
await textBox.click()
})
let originalString: string
await perfMonitor.measureOperation('get-input-value', async () => {
originalString = await textBox.inputValue()
})
await perfMonitor.measureOperation('select-text', async () => {
await textBox.selectText()
})
await perfMonitor.measureOperation('copy-text', async () => {
await comfyPage.ctrlC(null)
})
await perfMonitor.measureOperation('paste-text-first', async () => {
await comfyPage.ctrlV(null)
})
await perfMonitor.measureOperation('paste-text-second', async () => {
await comfyPage.ctrlV(null)
})
await textBox.click()
const originalString = await textBox.inputValue()
await textBox.selectText()
await comfyPage.ctrlC(null)
await comfyPage.ctrlV(null)
await comfyPage.ctrlV(null)
const resultString = await textBox.inputValue()
expect(resultString).toBe(originalString! + originalString!)
await perfMonitor.finishMonitoring(testName)
expect(resultString).toBe(originalString + originalString)
})
// skip reason: fails, did not investigate why
test.skip('@perf Can copy and paste widget value', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'copy-paste-widget-value'
await perfMonitor.startMonitoring(testName)
test('Can copy and paste widget value', async ({ comfyPage }) => {
// Copy width value (512) from empty latent node to KSampler's seed.
// KSampler's seed
await perfMonitor.measureOperation('click-ksampler-seed', async () => {
await comfyPage.canvas.click({
position: {
x: 1005,
y: 281
}
})
await comfyPage.canvas.click({
position: {
x: 1005,
y: 281
}
})
await perfMonitor.measureOperation('copy-widget-value', async () => {
await comfyPage.ctrlC(null)
})
await comfyPage.ctrlC(null)
// Empty latent node's width
await perfMonitor.measureOperation('click-empty-latent-width', async () => {
await comfyPage.canvas.click({
position: {
x: 718,
y: 643
}
})
await comfyPage.canvas.click({
position: {
x: 718,
y: 643
}
})
await perfMonitor.measureOperation('paste-widget-value', async () => {
await comfyPage.ctrlV(null)
})
await perfMonitor.measureOperation('confirm-with-enter', async () => {
await comfyPage.page.keyboard.press('Enter')
})
await perfMonitor.finishMonitoring(testName)
await comfyPage.ctrlV(null)
await comfyPage.page.keyboard.press('Enter')
await expect(comfyPage.canvas).toHaveScreenshot('copied-widget-value.png')
})
/**
* https://github.com/Comfy-Org/ComfyUI_frontend/issues/98
*/
test('@perf Paste in text area with node previously copied', async ({
test('Paste in text area with node previously copied', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'paste-text-with-node-copied'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('click-empty-latent-node', async () => {
await comfyPage.clickEmptyLatentNode()
})
await perfMonitor.measureOperation('copy-node', async () => {
await comfyPage.ctrlC(null)
})
await comfyPage.clickEmptyLatentNode()
await comfyPage.ctrlC(null)
const textBox = comfyPage.widgetTextBox
await perfMonitor.measureOperation('click-textbox', async () => {
await textBox.click()
})
await perfMonitor.measureOperation('get-input-value', async () => {
await textBox.inputValue()
})
await perfMonitor.measureOperation('select-text', async () => {
await textBox.selectText()
})
await perfMonitor.measureOperation('copy-text', async () => {
await comfyPage.ctrlC(null)
})
await perfMonitor.measureOperation('paste-text-first', async () => {
await comfyPage.ctrlV(null)
})
await perfMonitor.measureOperation('paste-text-second', async () => {
await comfyPage.ctrlV(null)
})
await perfMonitor.finishMonitoring(testName)
await textBox.click()
await textBox.inputValue()
await textBox.selectText()
await comfyPage.ctrlC(null)
await comfyPage.ctrlV(null)
await comfyPage.ctrlV(null)
await expect(comfyPage.canvas).toHaveScreenshot(
'paste-in-text-area-with-node-previously-copied.png'
)
})
test('@perf Copy text area does not copy node', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'copy-text-no-node'
await perfMonitor.startMonitoring(testName)
test('Copy text area does not copy node', async ({ comfyPage }) => {
const textBox = comfyPage.widgetTextBox
await perfMonitor.measureOperation('click-textbox', async () => {
await textBox.click()
})
await perfMonitor.measureOperation('get-input-value', async () => {
await textBox.inputValue()
})
await perfMonitor.measureOperation('select-text', async () => {
await textBox.selectText()
})
await perfMonitor.measureOperation('copy-text', async () => {
await comfyPage.ctrlC(null)
})
await textBox.click()
await textBox.inputValue()
await textBox.selectText()
await comfyPage.ctrlC(null)
// Unfocus textbox.
await perfMonitor.measureOperation('unfocus-textbox', async () => {
await comfyPage.page.mouse.click(10, 10)
})
await perfMonitor.measureOperation('paste-attempt', async () => {
await comfyPage.ctrlV(null)
})
await perfMonitor.finishMonitoring(testName)
await comfyPage.page.mouse.click(10, 10)
await comfyPage.ctrlV(null)
await expect(comfyPage.canvas).toHaveScreenshot('no-node-copied.png')
})
test('@perf Copy node by dragging + alt', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'copy-node-drag-alt'
await perfMonitor.startMonitoring(testName)
test('Copy node by dragging + alt', async ({ comfyPage }) => {
// TextEncodeNode1
await perfMonitor.measureOperation('mouse-move-to-node', async () => {
await comfyPage.page.mouse.move(618, 191)
})
await perfMonitor.markEvent('alt-key-down')
await comfyPage.page.mouse.move(618, 191)
await comfyPage.page.keyboard.down('Alt')
await perfMonitor.measureOperation('mouse-down', async () => {
await comfyPage.page.mouse.down()
})
await perfMonitor.measureOperation('drag-node', async () => {
await comfyPage.page.mouse.move(100, 100)
})
await perfMonitor.measureOperation('mouse-up', async () => {
await comfyPage.page.mouse.up()
})
await perfMonitor.markEvent('alt-key-up')
await comfyPage.page.mouse.down()
await comfyPage.page.mouse.move(100, 100)
await comfyPage.page.mouse.up()
await comfyPage.page.keyboard.up('Alt')
await perfMonitor.finishMonitoring(testName)
await expect(comfyPage.canvas).toHaveScreenshot('drag-copy-copied-node.png')
})
// skip reason: fails, did not investigate why
test.skip('@perf Can undo paste multiple nodes as single action', async ({
test('Can undo paste multiple nodes as single action', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'undo-paste-multiple-nodes'
const initialCount = await comfyPage.getGraphNodesCount()
expect(initialCount).toBeGreaterThan(1)
await comfyPage.canvas.click()
await comfyPage.ctrlA()
await comfyPage.page.mouse.move(10, 10)
await comfyPage.ctrlC()
await comfyPage.ctrlV()
await perfMonitor.startMonitoring(testName)
const pasteCount = await comfyPage.getGraphNodesCount()
expect(pasteCount).toBe(initialCount * 2)
let initialCount: number
await perfMonitor.measureOperation('get-initial-count', async () => {
initialCount = await comfyPage.getGraphNodesCount()
})
expect(initialCount!).toBeGreaterThan(1)
await perfMonitor.measureOperation('click-canvas', async () => {
await comfyPage.canvas.click()
})
await perfMonitor.measureOperation('select-all', async () => {
await comfyPage.ctrlA()
})
await perfMonitor.measureOperation('mouse-move', async () => {
await comfyPage.page.mouse.move(10, 10)
})
await perfMonitor.measureOperation('copy-all-nodes', async () => {
await comfyPage.ctrlC()
})
await perfMonitor.measureOperation('paste-all-nodes', async () => {
await comfyPage.ctrlV()
})
let pasteCount: number
await perfMonitor.measureOperation('get-paste-count', async () => {
pasteCount = await comfyPage.getGraphNodesCount()
})
expect(pasteCount!).toBe(initialCount! * 2)
await perfMonitor.measureOperation('undo-paste', async () => {
await comfyPage.ctrlZ()
})
let undoCount: number
await perfMonitor.measureOperation('get-undo-count', async () => {
undoCount = await comfyPage.getGraphNodesCount()
})
expect(undoCount!).toBe(initialCount!)
await perfMonitor.finishMonitoring(testName)
await comfyPage.ctrlZ()
const undoCount = await comfyPage.getGraphNodesCount()
expect(undoCount).toBe(initialCount)
})
})

View File

@@ -2,7 +2,6 @@ import { Locator, expect } from '@playwright/test'
import type { Keybinding } from '../../src/schemas/keyBindingSchema'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { PerformanceMonitor } from '../helpers/performanceMonitor'
test.describe('Load workflow warning', () => {
test('Should display a warning when loading a workflow with missing nodes', async ({
@@ -16,89 +15,46 @@ test.describe('Load workflow warning', () => {
})
})
test.skip('@perf Does not report warning on undo/redo', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'undo-redo-no-warning'
await perfMonitor.startMonitoring(testName)
test('Does not report warning on undo/redo', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default')
await perfMonitor.measureOperation('load-workflow', async () => {
await comfyPage.loadWorkflow('missing_nodes')
})
await comfyPage.loadWorkflow('missing_nodes')
await comfyPage.closeDialog()
// Make a change to the graph
await perfMonitor.measureOperation('add-node-sequence', async () => {
await comfyPage.doubleClickCanvas()
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
})
await comfyPage.doubleClickCanvas()
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
// Undo and redo the change
await perfMonitor.measureOperation('undo-operation', async () => {
await comfyPage.ctrlZ()
})
await comfyPage.ctrlZ()
await expect(comfyPage.page.locator('.comfy-missing-nodes')).not.toBeVisible()
await perfMonitor.measureOperation('redo-operation', async () => {
await comfyPage.ctrlY()
})
await comfyPage.ctrlY()
await expect(comfyPage.page.locator('.comfy-missing-nodes')).not.toBeVisible()
await perfMonitor.finishMonitoring(testName)
})
test.describe('Execution error', () => {
test.skip('@perf Should display an error message when an execution error occurs', async ({
test('Should display an error message when an execution error occurs', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'execution-error-display'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('load-workflow', async () => {
await comfyPage.loadWorkflow('execution_error')
})
await perfMonitor.measureOperation('queue-execution', async () => {
await comfyPage.queueButton.click()
await comfyPage.nextFrame()
})
await comfyPage.loadWorkflow('execution_error')
await comfyPage.queueButton.click()
await comfyPage.nextFrame()
// Wait for the element with the .comfy-execution-error selector to be visible
const executionError = comfyPage.page.locator('.comfy-error-report')
await expect(executionError).toBeVisible()
await perfMonitor.finishMonitoring(testName)
})
test.skip('@perf Can display Issue Report form', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'issue-report-form-display'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('load-workflow', async () => {
await comfyPage.loadWorkflow('execution_error')
})
await perfMonitor.measureOperation('queue-execution', async () => {
await comfyPage.queueButton.click()
await comfyPage.nextFrame()
})
test('Can display Issue Report form', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('execution_error')
await comfyPage.queueButton.click()
await comfyPage.nextFrame()
await comfyPage.page.getByLabel('Help Fix This').click()
const issueReportForm = comfyPage.page.getByText(
'Submit Error Report (Optional)'
)
await expect(issueReportForm).toBeVisible()
await perfMonitor.finishMonitoring(testName)
})
})
@@ -147,7 +103,7 @@ test.describe('Missing models warning', () => {
}
])
}
await comfyPage.page.route(
comfyPage.page.route(
'**/api/experiment/models',
(route) => route.fulfill(modelFoldersRes),
{ times: 1 }
@@ -165,7 +121,7 @@ test.describe('Missing models warning', () => {
}
])
}
await comfyPage.page.route(
comfyPage.page.route(
'**/api/experiment/models/text_encoders',
(route) => route.fulfill(clipModelsRes),
{ times: 1 }
@@ -177,18 +133,6 @@ test.describe('Missing models warning', () => {
await expect(missingModelsWarning).not.toBeVisible()
})
test('Should not display warning when model metadata exists but widget values have changed', async ({
comfyPage
}) => {
// This tests the scenario where outdated model metadata exists in the workflow
// but the actual selected models (widget values) have changed
await comfyPage.loadWorkflow('model_metadata_widget_mismatch')
// The missing models warning should NOT appear
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
await expect(missingModelsWarning).not.toBeVisible()
})
// Flaky test after parallelization
// https://github.com/Comfy-Org/ComfyUI_frontend/pull/1400
test.skip('Should download missing model when clicking download button', async ({
@@ -399,29 +343,18 @@ test.describe('Error dialog', () => {
})
test.describe('Signin dialog', () => {
// Skip because fails with vue widget nodes (reason not investigated)
test.skip('@perf Paste content to signin dialog should not paste node on canvas', async ({
test('Paste content to signin dialog should not paste node on canvas', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'signin-dialog-paste-isolation'
await perfMonitor.startMonitoring(testName)
const nodeNum = (await comfyPage.getNodes()).length
await comfyPage.clickEmptyLatentNode()
await comfyPage.ctrlC()
await perfMonitor.measureOperation('copy-node-sequence', async () => {
await comfyPage.clickEmptyLatentNode()
await comfyPage.ctrlC()
})
await perfMonitor.measureOperation('widget-text-operations', async () => {
const textBox = comfyPage.widgetTextBox
await textBox.click()
await textBox.fill('test_password')
await textBox.press('Control+a')
await textBox.press('Control+c')
})
const textBox = comfyPage.widgetTextBox
await textBox.click()
await textBox.fill('test_password')
await textBox.press('Control+a')
await textBox.press('Control+c')
await comfyPage.page.evaluate(() => {
window['app'].extensionManager.dialog.showSignInDialog()
@@ -433,7 +366,5 @@ test.describe('Signin dialog', () => {
await expect(input).toHaveValue('test_password')
expect(await comfyPage.getNodes()).toHaveLength(nodeNum)
await perfMonitor.finishMonitoring(testName)
})
})

View File

@@ -1,35 +1,15 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { PerformanceMonitor } from '../helpers/performanceMonitor'
test.describe('DOM Widget', () => {
test('@perf Collapsed multiline textarea is not visible', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'collapsed-multiline-textarea-visibility'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('load-workflow', async () => {
await comfyPage.loadWorkflow('collapsed_multiline')
})
test('Collapsed multiline textarea is not visible', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('collapsed_multiline')
const textareaWidget = comfyPage.page.locator('.comfy-multiline-input')
await expect(textareaWidget).not.toBeVisible()
await perfMonitor.finishMonitoring(testName)
})
test.skip('@perf Multiline textarea correctly collapses', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'multiline-textarea-collapse'
await perfMonitor.startMonitoring(testName)
test('Multiline textarea correctly collapses', async ({ comfyPage }) => {
const multilineTextAreas = comfyPage.page.locator('.comfy-multiline-input')
const firstMultiline = multilineTextAreas.first()
const lastMultiline = multilineTextAreas.last()
@@ -37,91 +17,34 @@ test.describe('DOM Widget', () => {
await expect(firstMultiline).toBeVisible()
await expect(lastMultiline).toBeVisible()
let nodes: any[]
await perfMonitor.measureOperation('get-nodes-by-type', async () => {
nodes = await comfyPage.getNodeRefsByType('CLIPTextEncode')
})
await perfMonitor.markEvent('before-collapse')
await perfMonitor.measureOperation('collapse-all-nodes', async () => {
for (const node of nodes!) {
await node.click('collapse')
}
})
await perfMonitor.markEvent('after-collapse')
const nodes = await comfyPage.getNodeRefsByType('CLIPTextEncode')
for (const node of nodes) {
await node.click('collapse')
}
await expect(firstMultiline).not.toBeVisible()
await expect(lastMultiline).not.toBeVisible()
await perfMonitor.finishMonitoring(testName)
})
// Skip because fails with vue widget nodes (reason not investigated)
test.skip('@perf Position update when entering focus mode', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'focus-mode-position-update'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('set-menu-setting', async () => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
})
await perfMonitor.measureOperation('toggle-focus-mode', async () => {
await comfyPage.executeCommand('Workspace.ToggleFocusMode')
})
await perfMonitor.measureOperation('wait-frame-update', async () => {
await comfyPage.nextFrame()
})
test('Position update when entering focus mode', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.executeCommand('Workspace.ToggleFocusMode')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('focus-mode-on.png')
await perfMonitor.finishMonitoring(testName)
})
// No DOM widget should be created by creation of interim LGraphNode objects.
test.skip('@perf Copy node with DOM widget by dragging + alt', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'copy-node-alt-drag'
await perfMonitor.startMonitoring(testName)
let initialCount: number
await perfMonitor.measureOperation('get-initial-widget-count', async () => {
initialCount = await comfyPage.getDOMWidgetCount()
})
await perfMonitor.markEvent('before-copy-operation')
test('Copy node with DOM widget by dragging + alt', async ({ comfyPage }) => {
const initialCount = await comfyPage.getDOMWidgetCount()
// TextEncodeNode1
await perfMonitor.measureOperation('position-mouse-on-node', async () => {
await comfyPage.page.mouse.move(618, 191)
})
await comfyPage.page.mouse.move(618, 191)
await comfyPage.page.keyboard.down('Alt')
await comfyPage.page.mouse.down()
await comfyPage.page.mouse.move(100, 100)
await comfyPage.page.mouse.up()
await comfyPage.page.keyboard.up('Alt')
await perfMonitor.measureOperation('alt-drag-copy', async () => {
await comfyPage.page.keyboard.down('Alt')
await comfyPage.page.mouse.down()
await comfyPage.page.mouse.move(100, 100)
await comfyPage.page.mouse.up()
await comfyPage.page.keyboard.up('Alt')
})
await perfMonitor.markEvent('after-copy-operation')
let finalCount: number
await perfMonitor.measureOperation('get-final-widget-count', async () => {
finalCount = await comfyPage.getDOMWidgetCount()
})
expect(finalCount!).toBe(initialCount! + 1)
await perfMonitor.finishMonitoring(testName)
const finalCount = await comfyPage.getDOMWidgetCount()
expect(finalCount).toBe(initialCount + 1)
})
})

View File

@@ -18,27 +18,3 @@ test.describe('Execution', () => {
)
})
})
test.describe('Execute to selected output nodes', () => {
test('Execute to selected output nodes', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('execution/partial_execution')
const input = await comfyPage.getNodeRefById(3)
const output1 = await comfyPage.getNodeRefById(1)
const output2 = await comfyPage.getNodeRefById(4)
expect(await (await input.getWidget(0)).getValue()).toBe('foo')
expect(await (await output1.getWidget(0)).getValue()).toBe('')
expect(await (await output2.getWidget(0)).getValue()).toBe('')
await output1.click('title')
await comfyPage.executeCommand('Comfy.QueueSelectedOutputNodes')
// @note: Wait for the execution to finish. We might want to move to a more
// reliable way to wait for the execution to finish. Workflow in this test
// is simple enough that this is fine for now.
await comfyPage.page.waitForTimeout(200)
expect(await (await input.getWidget(0)).getValue()).toBe('foo')
expect(await (await output1.getWidget(0)).getValue()).toBe('foo')
expect(await (await output2.getWidget(0)).getValue()).toBe('')
})
})

View File

@@ -1,7 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { PerformanceMonitor } from '../helpers/performanceMonitor'
test.describe('Graph Canvas Menu', () => {
test.beforeEach(async ({ comfyPage }) => {
@@ -10,28 +9,14 @@ test.describe('Graph Canvas Menu', () => {
await comfyPage.setSetting('Comfy.LinkRenderMode', 2)
})
test('@perf Can toggle link visibility', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'toggle-link-visibility'
await perfMonitor.startMonitoring(testName)
test('Can toggle link visibility', async ({ comfyPage }) => {
// Note: `Comfy.Graph.CanvasMenu` is disabled in comfyPage setup.
// so no cleanup is needed.
await perfMonitor.measureOperation('enable-canvas-menu', async () => {
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true)
})
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true)
const button = comfyPage.page.getByTestId('toggle-link-visibility-button')
await perfMonitor.markEvent('before-hide-links')
await perfMonitor.measureOperation('hide-links', async () => {
await button.click()
await comfyPage.nextFrame()
})
await perfMonitor.markEvent('after-hide-links')
// Screenshot assertions and validations stay outside performance monitoring
await button.click()
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'canvas-with-hidden-links.png'
)
@@ -42,21 +27,13 @@ test.describe('Graph Canvas Menu', () => {
hiddenLinkRenderMode
)
await perfMonitor.markEvent('before-show-links')
await perfMonitor.measureOperation('show-links', async () => {
await button.click()
await comfyPage.nextFrame()
})
await perfMonitor.markEvent('after-show-links')
// Screenshot assertions and validations stay outside performance monitoring
await button.click()
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'canvas-with-visible-links.png'
)
expect(await comfyPage.getSetting('Comfy.LinkRenderMode')).not.toBe(
hiddenLinkRenderMode
)
await perfMonitor.finishMonitoring(testName)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -2,7 +2,6 @@ import { expect } from '@playwright/test'
import { ComfyPage, comfyPageFixture as test } from '../fixtures/ComfyPage'
import type { NodeReference } from '../fixtures/utils/litegraphUtils'
import { PerformanceMonitor } from '../helpers/performanceMonitor'
test.describe('Group Node', () => {
test.describe('Node library sidebar', () => {
@@ -22,47 +21,25 @@ test.describe('Group Node', () => {
expect(await libraryTab.getFolder('group nodes').count()).toBe(1)
})
test('@perf Can be added to canvas using node library sidebar', async ({
test('Can be added to canvas using node library sidebar', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'add-group-node-from-library'
await perfMonitor.startMonitoring(testName)
const initialNodeCount = await comfyPage.getGraphNodesCount()
// Add group node from node library sidebar
await perfMonitor.measureOperation('expand-category-folder', async () => {
await libraryTab.getFolder(groupNodeCategory).click()
})
await perfMonitor.measureOperation('add-node-from-library', async () => {
await libraryTab.getNode(groupNodeName).click()
})
await libraryTab.getFolder(groupNodeCategory).click()
await libraryTab.getNode(groupNodeName).click()
// Verify the node is added to the canvas
expect(await comfyPage.getGraphNodesCount()).toBe(initialNodeCount + 1)
await perfMonitor.finishMonitoring(testName)
})
test('@perf Can be bookmarked and unbookmarked', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'bookmark-unbookmark-group-node'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('expand-category-folder', async () => {
await libraryTab.getFolder(groupNodeCategory).click()
})
await perfMonitor.measureOperation('bookmark-node', async () => {
await libraryTab
.getNode(groupNodeName)
.locator('.bookmark-button')
.click()
})
test('Can be bookmarked and unbookmarked', async ({ comfyPage }) => {
await libraryTab.getFolder(groupNodeCategory).click()
await libraryTab
.getNode(groupNodeName)
.locator('.bookmark-button')
.click()
// Verify the node is added to the bookmarks tab
expect(
@@ -72,20 +49,16 @@ test.describe('Group Node', () => {
expect(await libraryTab.getNode(groupNodeName).count()).not.toBe(0)
// Unbookmark the node
await perfMonitor.measureOperation('unbookmark-node', async () => {
await libraryTab
.getNode(groupNodeName)
.locator('.bookmark-button')
.first()
.click()
})
await libraryTab
.getNode(groupNodeName)
.locator('.bookmark-button')
.first()
.click()
// Verify the node is removed from the bookmarks tab
expect(
await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toHaveLength(0)
await perfMonitor.finishMonitoring(testName)
})
test('Displays preview on bookmark hover', async ({ comfyPage }) => {
@@ -122,38 +95,18 @@ test.describe('Group Node', () => {
)
})
test.skip('@perf Displays tooltip on title hover', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'group-node-tooltip-display'
await perfMonitor.startMonitoring(testName)
test('Displays tooltip on title hover', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.EnableTooltips', true)
await perfMonitor.measureOperation('convert-to-group-node', async () => {
await comfyPage.convertAllNodesToGroupNode('Group Node')
})
await perfMonitor.measureOperation('hover-for-tooltip', async () => {
await comfyPage.page.mouse.move(47, 173)
const tooltipTimeout = 500
await comfyPage.page.waitForTimeout(tooltipTimeout + 16)
})
await comfyPage.convertAllNodesToGroupNode('Group Node')
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 perfMonitor.finishMonitoring(testName)
})
// Skip because fails with vue widget nodes (reason not investigated)
test.skip('@perf Manage group opens with the correct group selected', async ({
test('Manage group opens with the correct group selected', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'manage-group-node-selection'
await perfMonitor.startMonitoring(testName)
const makeGroup = async (name, type1, type2) => {
const node1 = (await comfyPage.getNodeRefsByType(type1))[0]
const node2 = (await comfyPage.getNodeRefsByType(type2))[0]
@@ -164,44 +117,21 @@ test.describe('Group Node', () => {
return await node2.convertToGroupNode(name)
}
let group1
await perfMonitor.measureOperation('create-first-group', async () => {
group1 = await makeGroup('g1', 'CLIPTextEncode', 'CheckpointLoaderSimple')
})
let group2
await perfMonitor.measureOperation('create-second-group', async () => {
group2 = await makeGroup('g2', 'EmptyLatentImage', 'KSampler')
})
let manage1
await perfMonitor.measureOperation('open-first-manage-dialog', async () => {
manage1 = await group1.manageGroupNode()
await comfyPage.nextFrame()
})
expect(await manage1.getSelectedNodeType()).toBe('g1')
await perfMonitor.measureOperation(
'close-first-manage-dialog',
async () => {
await manage1.close()
}
const group1 = await makeGroup(
'g1',
'CLIPTextEncode',
'CheckpointLoaderSimple'
)
const group2 = await makeGroup('g2', 'EmptyLatentImage', 'KSampler')
const manage1 = await group1.manageGroupNode()
await comfyPage.nextFrame()
expect(await manage1.getSelectedNodeType()).toBe('g1')
await manage1.close()
await expect(manage1.root).not.toBeVisible()
let manage2
await perfMonitor.measureOperation(
'open-second-manage-dialog',
async () => {
manage2 = await group2.manageGroupNode()
}
)
const manage2 = await group2.manageGroupNode()
expect(await manage2.getSelectedNodeType()).toBe('g2')
await perfMonitor.finishMonitoring(testName)
})
test('Preserves hidden input configuration when containing duplicate node types', async ({
@@ -235,14 +165,9 @@ test.describe('Group Node', () => {
expect(visibleInputCount).toBe(2)
})
test.skip('@perf Reconnects inputs after configuration changed via manage dialog save', async ({
test('Reconnects inputs after configuration changed via manage dialog save', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'reconnect-inputs-after-config-change'
await perfMonitor.startMonitoring(testName)
const expectSingleNode = async (type: string) => {
const nodes = await comfyPage.getNodeRefsByType(type)
expect(nodes).toHaveLength(1)
@@ -250,65 +175,30 @@ test.describe('Group Node', () => {
}
const latent = await expectSingleNode('EmptyLatentImage')
const sampler = await expectSingleNode('KSampler')
// Remove existing link
const samplerInput = await sampler.getInput(0)
await perfMonitor.measureOperation('remove-existing-links', async () => {
await samplerInput.removeLinks()
})
await samplerInput.removeLinks()
// Group latent + sampler
await perfMonitor.measureOperation('select-nodes-for-group', async () => {
await latent.click('title', {
modifiers: ['Shift']
})
await sampler.click('title', {
modifiers: ['Shift']
})
await latent.click('title', {
modifiers: ['Shift']
})
let groupNode
await perfMonitor.measureOperation('convert-to-group-node', async () => {
groupNode = await sampler.convertToGroupNode()
await sampler.click('title', {
modifiers: ['Shift']
})
const groupNode = await sampler.convertToGroupNode()
// Connect node to group
const ckpt = await expectSingleNode('CheckpointLoaderSimple')
let input
await perfMonitor.measureOperation('connect-nodes', async () => {
input = await ckpt.connectOutput(0, groupNode, 0)
})
const input = await ckpt.connectOutput(0, groupNode, 0)
expect(await input.getLinkCount()).toBe(1)
// Modify the group node via manage dialog
await perfMonitor.markEvent('before-manage-dialog')
let manage
await perfMonitor.measureOperation('open-manage-dialog', async () => {
manage = await groupNode.manageGroupNode()
})
await perfMonitor.measureOperation(
'configure-in-manage-dialog',
async () => {
await manage.selectNode('KSampler')
await manage.changeTab('Inputs')
await manage.setLabel('model', 'test')
await manage.save()
}
)
await perfMonitor.measureOperation('close-manage-dialog', async () => {
await manage.close()
})
await perfMonitor.markEvent('after-manage-dialog')
const manage = await groupNode.manageGroupNode()
await manage.selectNode('KSampler')
await manage.changeTab('Inputs')
await manage.setLabel('model', 'test')
await manage.save()
await manage.close()
// Ensure the link is still present
expect(await input.getLinkCount()).toBe(1)
await perfMonitor.finishMonitoring(testName)
})
test('Loads from a workflow using the legacy path separator ("/")', async ({
@@ -364,131 +254,57 @@ test.describe('Group Node', () => {
await groupNode.copy()
})
test('@perf Copies and pastes group node within the same workflow', async ({
test('Copies and pastes group node within the same workflow', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'copy-paste-group-node-same-workflow'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('paste-group-node', async () => {
await comfyPage.ctrlV()
})
await comfyPage.ctrlV()
await verifyNodeLoaded(comfyPage, 2)
await perfMonitor.finishMonitoring(testName)
})
test('@perf Copies and pastes group node after clearing workflow', async ({
test('Copies and pastes group node after clearing workflow', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'copy-paste-group-node-after-clear'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('clear-workflow', async () => {
await comfyPage.menu.topbar.triggerTopbarCommand([
'Edit',
'Clear Workflow'
])
})
await perfMonitor.measureOperation('paste-group-node', async () => {
await comfyPage.ctrlV()
})
await comfyPage.menu.topbar.triggerTopbarCommand([
'Edit',
'Clear Workflow'
])
await comfyPage.ctrlV()
await verifyNodeLoaded(comfyPage, 1)
await perfMonitor.finishMonitoring(testName)
})
test('@perf Copies and pastes group node into a newly created blank workflow', async ({
test('Copies and pastes group node into a newly created blank workflow', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'copy-paste-group-node-new-workflow'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('create-new-workflow', async () => {
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
})
await perfMonitor.measureOperation('paste-group-node', async () => {
await comfyPage.ctrlV()
})
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
await comfyPage.ctrlV()
await verifyNodeLoaded(comfyPage, 1)
await perfMonitor.finishMonitoring(testName)
})
test('@perf Copies and pastes group node across different workflows', async ({
test('Copies and pastes group node across different workflows', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'copy-paste-group-node-different-workflow'
await comfyPage.loadWorkflow('default')
await comfyPage.ctrlV()
await verifyNodeLoaded(comfyPage, 1)
})
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation(
'load-different-workflow',
async () => {
await comfyPage.loadWorkflow('default')
}
test('Serializes group node after copy and paste across workflows', async ({
comfyPage
}) => {
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
await comfyPage.ctrlV()
const currentGraphState = await comfyPage.page.evaluate(() =>
window['app'].graph.serialize()
)
await perfMonitor.measureOperation('paste-group-node', async () => {
await comfyPage.ctrlV()
})
await verifyNodeLoaded(comfyPage, 1)
await perfMonitor.finishMonitoring(testName)
})
test('@perf Serializes group node after copy and paste across workflows', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'serialize-group-node-cross-workflow'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('create-new-workflow', async () => {
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
})
await perfMonitor.measureOperation('paste-group-node', async () => {
await comfyPage.ctrlV()
})
let currentGraphState
await perfMonitor.measureOperation('serialize-graph', async () => {
currentGraphState = await comfyPage.page.evaluate(() =>
window['app'].graph.serialize()
)
})
await test.step('Load workflow containing a group node pasted from a different workflow', async () => {
await perfMonitor.measureOperation(
'load-serialized-workflow',
async () => {
await comfyPage.page.evaluate(
(workflow) => window['app'].loadGraphData(workflow),
currentGraphState
)
await comfyPage.nextFrame()
}
await comfyPage.page.evaluate(
(workflow) => window['app'].loadGraphData(workflow),
currentGraphState
)
await comfyPage.nextFrame()
await verifyNodeLoaded(comfyPage, 1)
})
await perfMonitor.finishMonitoring(testName)
})
})
@@ -499,31 +315,12 @@ test.describe('Group Node', () => {
await comfyPage.page.waitForTimeout(300)
expect(await comfyPage.getVisibleToastCount()).toBe(1)
})
test('@perf Convert to group node, selected 1 node', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'convert-single-node-to-group-keybinding'
await perfMonitor.startMonitoring(testName)
test('Convert to group node, selected 1 node', async ({ comfyPage }) => {
expect(await comfyPage.getVisibleToastCount()).toBe(0)
await perfMonitor.measureOperation('select-node', async () => {
await comfyPage.clickTextEncodeNode1()
})
await perfMonitor.measureOperation(
'trigger-group-keybinding',
async () => {
await comfyPage.page.keyboard.press('Alt+g')
await comfyPage.page.waitForTimeout(300)
}
)
await comfyPage.clickTextEncodeNode1()
await comfyPage.page.keyboard.press('Alt+g')
await comfyPage.page.waitForTimeout(300)
expect(await comfyPage.getVisibleToastCount()).toBe(1)
await perfMonitor.finishMonitoring(testName)
})
})
})

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

View File

@@ -1,7 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { PerformanceMonitor } from '../helpers/performanceMonitor'
function listenForEvent(): Promise<Event> {
return new Promise<Event>((resolve) => {
@@ -12,50 +11,27 @@ function listenForEvent(): Promise<Event> {
}
test.describe('Canvas Event', () => {
test.skip('@perf Emit litegraph:canvas empty-release', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'canvas-empty-release'
await perfMonitor.startMonitoring(testName)
test('Emit litegraph:canvas empty-release', async ({ comfyPage }) => {
const eventPromise = comfyPage.page.evaluate(listenForEvent)
await perfMonitor.measureOperation('disconnect-edge', async () => {
await comfyPage.disconnectEdge()
})
const disconnectPromise = comfyPage.disconnectEdge()
const event = await eventPromise
await disconnectPromise
expect(event).not.toBeNull()
// No further check on event content as the content is dropped by
// playwright for some reason.
// See https://github.com/microsoft/playwright/issues/31580
await perfMonitor.finishMonitoring(testName)
})
// Skip because fails with vue widget nodes (reason not investigated)
test.skip('@perf Emit litegraph:canvas empty-double-click', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'canvas-double-click'
await perfMonitor.startMonitoring(testName)
test('Emit litegraph:canvas empty-double-click', async ({ comfyPage }) => {
const eventPromise = comfyPage.page.evaluate(listenForEvent)
await perfMonitor.measureOperation('double-click-canvas', async () => {
await comfyPage.doubleClickCanvas()
})
const doubleClickPromise = comfyPage.doubleClickCanvas()
const event = await eventPromise
await doubleClickPromise
expect(event).not.toBeNull()
// No further check on event content as the content is dropped by
// playwright for some reason.
// See https://github.com/microsoft/playwright/issues/31580
await perfMonitor.finishMonitoring(testName)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -3,200 +3,109 @@ import { expect } from '@playwright/test'
import type { ComfyApp } from '../../src/scripts/app'
import { NodeBadgeMode } from '../../src/types/nodeSource'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { PerformanceMonitor } from '../helpers/performanceMonitor'
test.describe('Node Badge', () => {
test('@perf Can add badge', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'add-single-badge'
test('Can add badge', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
const LGraphBadge = window['LGraphBadge']
const app = window['app'] as ComfyApp
const graph = app.graph
const nodes = graph.nodes
await perfMonitor.startMonitoring(testName)
for (const node of nodes) {
node.badges = [new LGraphBadge({ text: 'Test Badge' })]
}
await perfMonitor.measureOperation('add-badge-to-nodes', async () => {
await comfyPage.page.evaluate(() => {
const LGraphBadge = window['LGraphBadge']
const app = window['app'] as ComfyApp
const graph = app.graph
const nodes = graph.nodes
for (const node of nodes) {
node.badges = [new LGraphBadge({ text: 'Test Badge' })]
}
graph.setDirtyCanvas(true, true)
})
graph.setDirtyCanvas(true, true)
})
await expect(comfyPage.canvas).toHaveScreenshot('node-badge.png')
await perfMonitor.finishMonitoring(testName)
})
test('@perf Can add multiple badges', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'add-multiple-badges'
test('Can add multiple badges', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
const LGraphBadge = window['LGraphBadge']
const app = window['app'] as ComfyApp
const graph = app.graph
const nodes = graph.nodes
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation(
'add-multiple-badges-to-nodes',
async () => {
await comfyPage.page.evaluate(() => {
const LGraphBadge = window['LGraphBadge']
const app = window['app'] as ComfyApp
const graph = app.graph
const nodes = graph.nodes
for (const node of nodes) {
node.badges = [
new LGraphBadge({ text: 'Test Badge 1' }),
new LGraphBadge({ text: 'Test Badge 2' })
]
}
graph.setDirtyCanvas(true, true)
})
for (const node of nodes) {
node.badges = [
new LGraphBadge({ text: 'Test Badge 1' }),
new LGraphBadge({ text: 'Test Badge 2' })
]
}
)
graph.setDirtyCanvas(true, true)
})
await expect(comfyPage.canvas).toHaveScreenshot('node-badge-multiple.png')
await perfMonitor.finishMonitoring(testName)
})
test('@perf Can add badge left-side', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'add-badge-left-position'
test('Can add badge left-side', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
const LGraphBadge = window['LGraphBadge']
const app = window['app'] as ComfyApp
const graph = app.graph
const nodes = graph.nodes
await perfMonitor.startMonitoring(testName)
for (const node of nodes) {
node.badges = [new LGraphBadge({ text: 'Test Badge' })]
// @ts-expect-error - Enum value
node.badgePosition = 'top-left'
}
await perfMonitor.measureOperation('add-badge-with-position', async () => {
await comfyPage.page.evaluate(() => {
const LGraphBadge = window['LGraphBadge']
const app = window['app'] as ComfyApp
const graph = app.graph
const nodes = graph.nodes
for (const node of nodes) {
node.badges = [new LGraphBadge({ text: 'Test Badge' })]
// @ts-expect-error - Enum value
node.badgePosition = 'top-left'
}
graph.setDirtyCanvas(true, true)
})
graph.setDirtyCanvas(true, true)
})
await expect(comfyPage.canvas).toHaveScreenshot('node-badge-left.png')
await perfMonitor.finishMonitoring(testName)
})
})
test.describe('Node source badge', () => {
Object.values(NodeBadgeMode).forEach(async (mode) => {
test(`@perf Shows node badges (${mode})`, async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = `node-source-badge-${mode}`
await perfMonitor.startMonitoring(testName)
test(`Shows node badges (${mode})`, async ({ comfyPage }) => {
// Execution error workflow has both custom node and core node.
await perfMonitor.measureOperation('load-workflow', async () => {
await comfyPage.loadWorkflow('execution_error')
})
await perfMonitor.measureOperation(
'configure-badge-settings',
async () => {
await comfyPage.setSetting(
'Comfy.NodeBadge.NodeSourceBadgeMode',
mode
)
await comfyPage.setSetting('Comfy.NodeBadge.NodeIdBadgeMode', mode)
}
)
await perfMonitor.measureOperation('render-badges', async () => {
await comfyPage.nextFrame()
await comfyPage.resetView()
})
await comfyPage.loadWorkflow('execution_error')
await comfyPage.setSetting('Comfy.NodeBadge.NodeSourceBadgeMode', mode)
await comfyPage.setSetting('Comfy.NodeBadge.NodeIdBadgeMode', mode)
await comfyPage.nextFrame()
await comfyPage.resetView()
await expect(comfyPage.canvas).toHaveScreenshot(`node-badge-${mode}.png`)
await perfMonitor.finishMonitoring(testName)
})
})
})
test.describe('Node badge color', () => {
test('@perf Can show node badge with unknown color palette', async ({
test('Can show node badge with unknown color palette', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'node-badge-unknown-color-palette'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation(
'configure-badge-and-palette',
async () => {
await comfyPage.setSetting(
'Comfy.NodeBadge.NodeIdBadgeMode',
NodeBadgeMode.ShowAll
)
await comfyPage.setSetting('Comfy.ColorPalette', 'unknown')
}
await comfyPage.setSetting(
'Comfy.NodeBadge.NodeIdBadgeMode',
NodeBadgeMode.ShowAll
)
await perfMonitor.measureOperation(
'render-with-unknown-palette',
async () => {
await comfyPage.nextFrame()
// Click empty space to trigger canvas re-render.
await comfyPage.clickEmptySpace()
}
)
await comfyPage.setSetting('Comfy.ColorPalette', 'unknown')
await comfyPage.nextFrame()
// Click empty space to trigger canvas re-render.
await comfyPage.clickEmptySpace()
await expect(comfyPage.canvas).toHaveScreenshot(
'node-badge-unknown-color-palette.png'
)
await perfMonitor.finishMonitoring(testName)
})
test('@perf Can show node badge with light color palette', async ({
test('Can show node badge with light color palette', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'node-badge-light-color-palette'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation(
'configure-badge-and-light-palette',
async () => {
await comfyPage.setSetting(
'Comfy.NodeBadge.NodeIdBadgeMode',
NodeBadgeMode.ShowAll
)
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
}
await comfyPage.setSetting(
'Comfy.NodeBadge.NodeIdBadgeMode',
NodeBadgeMode.ShowAll
)
await perfMonitor.measureOperation(
'render-with-light-palette',
async () => {
await comfyPage.nextFrame()
// Click empty space to trigger canvas re-render.
await comfyPage.clickEmptySpace()
}
)
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.nextFrame()
// Click empty space to trigger canvas re-render.
await comfyPage.clickEmptySpace()
await expect(comfyPage.canvas).toHaveScreenshot(
'node-badge-light-color-palette.png'
)
await perfMonitor.finishMonitoring(testName)
})
})

View File

@@ -1,102 +1,38 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { PerformanceMonitor } from '../helpers/performanceMonitor'
// If an input is optional by node definition, it should be shown as
// a hollow circle no matter what shape it was defined in the workflow JSON.
test.describe('Optional input', () => {
test('@perf No shape specified', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'optional-input-no-shape'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('load-workflow', async () => {
await comfyPage.loadWorkflow('optional_input_no_shape')
})
test('No shape specified', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('optional_input_no_shape')
await expect(comfyPage.canvas).toHaveScreenshot('optional_input.png')
await perfMonitor.finishMonitoring(testName)
})
test('@perf Wrong shape specified', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'optional-input-wrong-shape'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('load-workflow', async () => {
await comfyPage.loadWorkflow('optional_input_wrong_shape')
})
test('Wrong shape specified', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('optional_input_wrong_shape')
await expect(comfyPage.canvas).toHaveScreenshot('optional_input.png')
await perfMonitor.finishMonitoring(testName)
})
test('@perf Correct shape specified', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'optional-input-correct-shape'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('load-workflow', async () => {
await comfyPage.loadWorkflow('optional_input_correct_shape')
})
test('Correct shape specified', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('optional_input_correct_shape')
await expect(comfyPage.canvas).toHaveScreenshot('optional_input.png')
await perfMonitor.finishMonitoring(testName)
})
test('@perf Force input', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'force-input'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('load-workflow', async () => {
await comfyPage.loadWorkflow('force_input')
})
test('Force input', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('force_input')
await expect(comfyPage.canvas).toHaveScreenshot('force_input.png')
await perfMonitor.finishMonitoring(testName)
})
test('@perf Default input', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'default-input'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('load-workflow', async () => {
await comfyPage.loadWorkflow('default_input')
})
test('Default input', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('default_input')
await expect(comfyPage.canvas).toHaveScreenshot('default_input.png')
await perfMonitor.finishMonitoring(testName)
})
test.skip('@perf Only optional inputs', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'only-optional-inputs'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('load-workflow', async () => {
await comfyPage.loadWorkflow('only_optional_inputs')
})
let nodeCount: number
await perfMonitor.measureOperation('get-nodes-count', async () => {
nodeCount = await comfyPage.getGraphNodesCount()
})
expect(nodeCount!).toBe(1)
test('Only optional inputs', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('only_optional_inputs')
expect(await comfyPage.getGraphNodesCount()).toBe(1)
await expect(
comfyPage.page.locator('.comfy-missing-nodes')
).not.toBeVisible()
@@ -105,29 +41,11 @@ test.describe('Optional input', () => {
await expect(comfyPage.page.locator('.comfy-multiline-input')).toHaveCount(
1
)
await perfMonitor.finishMonitoring(testName)
})
test('@perf Old workflow with converted input', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'old-workflow-converted-input'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('load-workflow', async () => {
await comfyPage.loadWorkflow('old_workflow_converted_input')
})
let node: any
await perfMonitor.measureOperation('get-node', async () => {
node = await comfyPage.getNodeRefById('1')
})
let inputs: any
await perfMonitor.measureOperation('get-node-inputs', async () => {
inputs = await node.getProperty('inputs')
})
test('Old workflow with converted input', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('old_workflow_converted_input')
const node = await comfyPage.getNodeRefById('1')
const inputs = await node.getProperty('inputs')
const vaeInput = inputs.find((w) => w.name === 'vae')
const convertedInput = inputs.find((w) => w.name === 'strength')
@@ -135,87 +53,29 @@ test.describe('Optional input', () => {
expect(convertedInput).toBeDefined()
expect(vaeInput.link).toBeNull()
expect(convertedInput.link).not.toBeNull()
await perfMonitor.finishMonitoring(testName)
})
test('@perf Renamed converted input', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'renamed-converted-input'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('load-workflow', async () => {
await comfyPage.loadWorkflow('renamed_converted_widget')
})
let node: any
await perfMonitor.measureOperation('get-node', async () => {
node = await comfyPage.getNodeRefById('3')
})
let inputs: any
await perfMonitor.measureOperation('get-node-inputs', async () => {
inputs = await node.getProperty('inputs')
})
test('Renamed converted input', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('renamed_converted_widget')
const node = await comfyPage.getNodeRefById('3')
const inputs = await node.getProperty('inputs')
const renamedInput = inputs.find((w) => w.name === 'breadth')
expect(renamedInput).toBeUndefined()
await perfMonitor.finishMonitoring(testName)
})
// Skip because fails with vue widget nodes (reason not investigated)
test.skip('@perf slider', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'simple-slider'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('load-workflow', async () => {
await comfyPage.loadWorkflow('simple_slider')
})
test('slider', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('simple_slider')
await expect(comfyPage.canvas).toHaveScreenshot('simple_slider.png')
await perfMonitor.finishMonitoring(testName)
})
test('@perf unknown converted widget', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'unknown-converted-widget'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('set-setting', async () => {
await comfyPage.setSetting(
'Comfy.Workflow.ShowMissingNodesWarning',
false
)
})
await perfMonitor.measureOperation('load-workflow', async () => {
await comfyPage.loadWorkflow('missing_nodes_converted_widget')
})
test('unknown converted widget', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Workflow.ShowMissingNodesWarning', false)
await comfyPage.loadWorkflow('missing_nodes_converted_widget')
await expect(comfyPage.canvas).toHaveScreenshot(
'missing_nodes_converted_widget.png'
)
await perfMonitor.finishMonitoring(testName)
})
// Skip because fails with vue widget nodes (reason not investigated)
test.skip('@perf dynamically added input', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'dynamically-added-input'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('load-workflow', async () => {
await comfyPage.loadWorkflow('dynamically_added_input')
})
test('dynamically added input', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('dynamically_added_input')
await expect(comfyPage.canvas).toHaveScreenshot(
'dynamically_added_input.png'
)
await perfMonitor.finishMonitoring(testName)
})
})

View File

@@ -1,556 +0,0 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../fixtures/ComfyPage'
// TODO: there might be a better solution for this
// Helper function to pan canvas and select node
async function selectNodeWithPan(comfyPage: any, nodeRef: any) {
const nodePos = await nodeRef.getPosition()
await comfyPage.page.evaluate((pos) => {
const app = window['app']
const canvas = app.canvas
canvas.ds.offset[0] = -pos.x + canvas.canvas.width / 2
canvas.ds.offset[1] = -pos.y + canvas.canvas.height / 2 + 100
canvas.setDirty(true, true)
}, nodePos)
await comfyPage.nextFrame()
await nodeRef.click('title')
}
test.describe('Node Help', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setup()
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
})
test.describe('Selection Toolbox', () => {
test('Should open help menu for selected node', async ({ comfyPage }) => {
// Load a workflow with a node
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
await comfyPage.loadWorkflow('default')
// Select a single node (KSampler) using node references
const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler')
if (ksamplerNodes.length === 0) {
throw new Error('No KSampler nodes found in the workflow')
}
// Select the node with panning to ensure toolbox is visible
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
// Wait for selection overlay container and toolbox to appear
await expect(
comfyPage.page.locator('.selection-overlay-container')
).toBeVisible()
await expect(comfyPage.page.locator('.selection-toolbox')).toBeVisible()
// Click the help button in the selection toolbox
const helpButton = comfyPage.page.locator(
'.selection-toolbox button:has(.pi-question-circle)'
)
await expect(helpButton).toBeVisible()
await helpButton.click()
// Verify that the node library sidebar is opened
await expect(
comfyPage.menu.nodeLibraryTab.selectedTabButton
).toBeVisible()
// Verify that the help page is shown for the correct node
const helpPage = comfyPage.page.locator('.sidebar-content-container')
await expect(helpPage).toContainText('KSampler')
await expect(helpPage.locator('.node-help-content')).toBeVisible()
})
})
test.describe('Node Library Sidebar', () => {
test('Should open help menu from node library', async ({ comfyPage }) => {
// Open the node library sidebar
await comfyPage.menu.nodeLibraryTab.open()
// Wait for node library to load
await expect(comfyPage.menu.nodeLibraryTab.nodeLibraryTree).toBeVisible()
// Search for KSampler to make it easier to find
await comfyPage.menu.nodeLibraryTab.nodeLibrarySearchBoxInput.fill(
'KSampler'
)
// Find the KSampler node in search results
const ksamplerNode = comfyPage.page
.locator('.tree-explorer-node-label')
.filter({ hasText: 'KSampler' })
.first()
await expect(ksamplerNode).toBeVisible()
// Hover over the node to show action buttons
await ksamplerNode.hover()
// Click the help button
const helpButton = ksamplerNode.locator('button:has(.pi-question)')
await expect(helpButton).toBeVisible()
await helpButton.click()
// Verify that the help page is shown
const helpPage = comfyPage.page.locator('.sidebar-content-container')
await expect(helpPage).toContainText('KSampler')
await expect(helpPage.locator('.node-help-content')).toBeVisible()
})
test('Should show node library tab when clicking back from help page', async ({
comfyPage
}) => {
// Open the node library sidebar
await comfyPage.menu.nodeLibraryTab.open()
// Wait for node library to load
await expect(comfyPage.menu.nodeLibraryTab.nodeLibraryTree).toBeVisible()
// Search for KSampler
await comfyPage.menu.nodeLibraryTab.nodeLibrarySearchBoxInput.fill(
'KSampler'
)
// Find and interact with the node
const ksamplerNode = comfyPage.page
.locator('.tree-explorer-node-label')
.filter({ hasText: 'KSampler' })
.first()
await ksamplerNode.hover()
const helpButton = ksamplerNode.locator('button:has(.pi-question)')
await helpButton.click()
// Verify help page is shown
const helpPage = comfyPage.page.locator('.sidebar-content-container')
await expect(helpPage).toContainText('KSampler')
// Click the back button - use a more specific selector
const backButton = comfyPage.page.locator('button:has(.pi-arrow-left)')
await expect(backButton).toBeVisible()
await backButton.click()
// Verify that we're back to the node library view
await expect(comfyPage.menu.nodeLibraryTab.nodeLibraryTree).toBeVisible()
await expect(
comfyPage.menu.nodeLibraryTab.nodeLibrarySearchBoxInput
).toBeVisible()
// Verify help page is no longer visible
await expect(helpPage.locator('.node-help-content')).not.toBeVisible()
})
})
test.describe('Help Content', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
})
test('Should display loading state while fetching help', async ({
comfyPage
}) => {
// Mock slow network response
await comfyPage.page.route('**/docs/**/*.md', async (route) => {
await new Promise((resolve) => setTimeout(resolve, 1000))
await route.fulfill({
status: 200,
body: '# Test Help Content\nThis is test help content.'
})
})
// Load workflow and select a node
await comfyPage.loadWorkflow('default')
const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler')
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
// Click help button
const helpButton = comfyPage.page.locator(
'.selection-toolbox button:has(.pi-question-circle)'
)
await helpButton.click()
// Verify loading spinner is shown
const helpPage = comfyPage.page.locator('.sidebar-content-container')
await expect(helpPage.locator('.p-progressspinner')).toBeVisible()
// Wait for content to load
await expect(helpPage).toContainText('Test Help Content')
})
test('Should display fallback content when help file not found', async ({
comfyPage
}) => {
// Mock 404 response for help files
await comfyPage.page.route('**/docs/**/*.md', async (route) => {
await route.fulfill({
status: 404,
body: 'Not Found'
})
})
// Load workflow and select a node
await comfyPage.loadWorkflow('default')
const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler')
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
// Click help button
const helpButton = comfyPage.page.locator(
'.selection-toolbox button:has(.pi-question-circle)'
)
await helpButton.click()
// Verify fallback content is shown (description, inputs, outputs)
const helpPage = comfyPage.page.locator('.sidebar-content-container')
await expect(helpPage).toContainText('Description')
await expect(helpPage).toContainText('Inputs')
await expect(helpPage).toContainText('Outputs')
})
test('Should render markdown with images correctly', async ({
comfyPage
}) => {
// Mock response with markdown containing images
await comfyPage.page.route('**/docs/KSampler/en.md', async (route) => {
await route.fulfill({
status: 200,
body: `# KSampler Documentation
![Example Image](example.jpg)
![External Image](https://example.com/image.png)
## Parameters
- **steps**: Number of steps
`
})
})
await comfyPage.loadWorkflow('default')
const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler')
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
const helpButton = comfyPage.page.locator(
'.selection-toolbox button:has(.pi-question-circle)'
)
await helpButton.click()
const helpPage = comfyPage.page.locator('.sidebar-content-container')
await expect(helpPage).toContainText('KSampler Documentation')
// Check that relative image paths are prefixed correctly
const relativeImage = helpPage.locator('img[alt="Example Image"]')
await expect(relativeImage).toBeVisible()
await expect(relativeImage).toHaveAttribute(
'src',
/.*\/docs\/KSampler\/example\.jpg/
)
// Check that absolute URLs are not modified
const externalImage = helpPage.locator('img[alt="External Image"]')
await expect(externalImage).toHaveAttribute(
'src',
'https://example.com/image.png'
)
})
test('Should render video elements with source tags in markdown', async ({
comfyPage
}) => {
// Mock response with video elements
await comfyPage.page.route('**/docs/KSampler/en.md', async (route) => {
await route.fulfill({
status: 200,
body: `# KSampler Demo
<video src="demo.mp4" controls autoplay></video>
<video src="/absolute/video.mp4" controls></video>
<video controls>
<source src="video.mp4" type="video/mp4">
<source src="https://example.com/video.webm" type="video/webm">
</video>
`
})
})
await comfyPage.loadWorkflow('default')
const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler')
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
const helpButton = comfyPage.page.locator(
'.selection-toolbox button:has(.pi-question-circle)'
)
await helpButton.click()
const helpPage = comfyPage.page.locator('.sidebar-content-container')
// Check relative video paths are prefixed
const relativeVideo = helpPage.locator('video[src*="demo.mp4"]')
await expect(relativeVideo).toBeVisible()
await expect(relativeVideo).toHaveAttribute(
'src',
/.*\/docs\/KSampler\/demo\.mp4/
)
await expect(relativeVideo).toHaveAttribute('controls', '')
await expect(relativeVideo).toHaveAttribute('autoplay', '')
// Check absolute paths are not modified
const absoluteVideo = helpPage.locator('video[src="/absolute/video.mp4"]')
await expect(absoluteVideo).toHaveAttribute('src', '/absolute/video.mp4')
// Check video source elements
const relativeVideoSource = helpPage.locator('source[src*="video.mp4"]')
await expect(relativeVideoSource).toHaveAttribute(
'src',
/.*\/docs\/KSampler\/video\.mp4/
)
const externalVideoSource = helpPage.locator(
'source[src="https://example.com/video.webm"]'
)
await expect(externalVideoSource).toHaveAttribute(
'src',
'https://example.com/video.webm'
)
})
test('Should handle custom node documentation paths', async ({
comfyPage
}) => {
// First load workflow with custom node
await comfyPage.loadWorkflow('group_node_v1.3.3')
// Mock custom node documentation with fallback
await comfyPage.page.route(
'**/extensions/*/docs/*/en.md',
async (route) => {
await route.fulfill({ status: 404 })
}
)
await comfyPage.page.route('**/extensions/*/docs/*.md', async (route) => {
await route.fulfill({
status: 200,
body: `# Custom Node Documentation
This is documentation for a custom node.
![Custom Image](assets/custom.png)
`
})
})
// Find and select a custom/group node
const nodeRefs = await comfyPage.page.evaluate(() => {
return window['app'].graph.nodes.map((n: any) => n.id)
})
if (nodeRefs.length > 0) {
const firstNode = await comfyPage.getNodeRefById(nodeRefs[0])
await selectNodeWithPan(comfyPage, firstNode)
}
const helpButton = comfyPage.page.locator(
'.selection-toolbox button:has(.pi-question-circle)'
)
if (await helpButton.isVisible()) {
await helpButton.click()
const helpPage = comfyPage.page.locator('.sidebar-content-container')
await expect(helpPage).toContainText('Custom Node Documentation')
// Check image path for custom nodes
const image = helpPage.locator('img[alt="Custom Image"]')
await expect(image).toHaveAttribute(
'src',
/.*\/extensions\/.*\/docs\/assets\/custom\.png/
)
}
})
test('Should sanitize dangerous HTML content', async ({ comfyPage }) => {
// Mock response with potentially dangerous content
await comfyPage.page.route('**/docs/KSampler/en.md', async (route) => {
await route.fulfill({
status: 200,
body: `# Safe Content
<script>alert('XSS')</script>
<img src="x" onerror="alert('XSS')">
<a href="javascript:alert('XSS')">Dangerous Link</a>
<iframe src="evil.com"></iframe>
<!-- Safe content -->
<video src="safe.mp4" controls></video>
<img src="safe.jpg" alt="Safe Image">
`
})
})
await comfyPage.loadWorkflow('default')
const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler')
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
const helpButton = comfyPage.page.locator(
'.selection-toolbox button:has(.pi-question-circle)'
)
await helpButton.click()
const helpPage = comfyPage.page.locator('.sidebar-content-container')
// Dangerous elements should be removed
await expect(helpPage.locator('script')).toHaveCount(0)
await expect(helpPage.locator('iframe')).toHaveCount(0)
// Check that onerror attribute is removed
const images = helpPage.locator('img')
const imageCount = await images.count()
for (let i = 0; i < imageCount; i++) {
const img = images.nth(i)
const onError = await img.getAttribute('onerror')
expect(onError).toBeNull()
}
// Check that javascript: links are sanitized
const links = helpPage.locator('a')
const linkCount = await links.count()
for (let i = 0; i < linkCount; i++) {
const link = links.nth(i)
const href = await link.getAttribute('href')
if (href !== null) {
expect(href).not.toContain('javascript:')
}
}
// Safe content should remain
await expect(helpPage.locator('video[src*="safe.mp4"]')).toBeVisible()
await expect(helpPage.locator('img[alt="Safe Image"]')).toBeVisible()
})
test('Should handle locale-specific documentation', async ({
comfyPage
}) => {
// Mock different responses for different locales
await comfyPage.page.route('**/docs/KSampler/ja.md', async (route) => {
await route.fulfill({
status: 200,
body: `# KSamplerード
これは日本語のドキュメントです。
`
})
})
await comfyPage.page.route('**/docs/KSampler/en.md', async (route) => {
await route.fulfill({
status: 200,
body: `# KSampler Node
This is English documentation.
`
})
})
// Set locale to Japanese
await comfyPage.setSetting('Comfy.Locale', 'ja')
await comfyPage.loadWorkflow('default')
const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler')
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
const helpButton = comfyPage.page.locator(
'.selection-toolbox button:has(.pi-question-circle)'
)
await helpButton.click()
const helpPage = comfyPage.page.locator('.sidebar-content-container')
await expect(helpPage).toContainText('KSamplerード')
await expect(helpPage).toContainText('これは日本語のドキュメントです')
// Reset locale
await comfyPage.setSetting('Comfy.Locale', 'en')
})
test('Should handle network errors gracefully', async ({ comfyPage }) => {
// Mock network error
await comfyPage.page.route('**/docs/**/*.md', async (route) => {
await route.abort('failed')
})
await comfyPage.loadWorkflow('default')
const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler')
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
const helpButton = comfyPage.page.locator(
'.selection-toolbox button:has(.pi-question-circle)'
)
await helpButton.click()
const helpPage = comfyPage.page.locator('.sidebar-content-container')
// Should show fallback content (node description)
await expect(helpPage).toBeVisible()
await expect(helpPage.locator('.p-progressspinner')).not.toBeVisible()
// Should show some content even on error
const content = await helpPage.textContent()
expect(content).toBeTruthy()
})
test('Should update help content when switching between nodes', async ({
comfyPage
}) => {
// Mock different help content for different nodes
await comfyPage.page.route('**/docs/KSampler/en.md', async (route) => {
await route.fulfill({
status: 200,
body: '# KSampler Help\n\nThis is KSampler documentation.'
})
})
await comfyPage.page.route(
'**/docs/CheckpointLoaderSimple/en.md',
async (route) => {
await route.fulfill({
status: 200,
body: '# Checkpoint Loader Help\n\nThis is Checkpoint Loader documentation.'
})
}
)
await comfyPage.loadWorkflow('default')
// Select KSampler first
const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler')
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
const helpButton = comfyPage.page.locator(
'.selection-toolbox button:has(.pi-question-circle)'
)
await helpButton.click()
const helpPage = comfyPage.page.locator('.sidebar-content-container')
await expect(helpPage).toContainText('KSampler Help')
await expect(helpPage).toContainText('This is KSampler documentation')
// Now select Checkpoint Loader
const checkpointNodes = await comfyPage.getNodeRefsByType(
'CheckpointLoaderSimple'
)
await selectNodeWithPan(comfyPage, checkpointNodes[0])
// Click help button again
const helpButton2 = comfyPage.page.locator(
'.selection-toolbox button:has(.pi-question-circle)'
)
await helpButton2.click()
// Content should update
await expect(helpPage).toContainText('Checkpoint Loader Help')
await expect(helpPage).toContainText(
'This is Checkpoint Loader documentation'
)
await expect(helpPage).not.toContainText('KSampler documentation')
})
})
})

View File

@@ -2,7 +2,6 @@ import {
comfyExpect as expect,
comfyPageFixture as test
} from '../fixtures/ComfyPage'
import { PerformanceMonitor } from '../helpers/performanceMonitor'
test.describe('Node search box', () => {
test.beforeEach(async ({ comfyPage }) => {
@@ -11,8 +10,7 @@ test.describe('Node search box', () => {
await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default')
})
// Skip because fails with vue widget nodes (reason not investigated)
test.skip(`Can trigger on empty canvas double click`, async ({ comfyPage }) => {
test(`Can trigger on empty canvas double click`, async ({ comfyPage }) => {
await comfyPage.doubleClickCanvas()
await expect(comfyPage.searchBox.input).toHaveCount(1)
})
@@ -29,61 +27,24 @@ test.describe('Node search box', () => {
await expect(comfyPage.searchBox.input).toHaveCount(1)
})
test.skip('@perf Can add node', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'add-node-via-search'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('double-click-canvas', async () => {
await comfyPage.doubleClickCanvas()
})
test('Can add node', async ({ comfyPage }) => {
await comfyPage.doubleClickCanvas()
await expect(comfyPage.searchBox.input).toHaveCount(1)
await perfMonitor.measureOperation('search-and-add-node', async () => {
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
})
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
await expect(comfyPage.canvas).toHaveScreenshot('added-node.png')
await perfMonitor.finishMonitoring(testName)
})
test.skip('@perf Can auto link node', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'auto-link-node'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('disconnect-edge', async () => {
await comfyPage.disconnectEdge()
})
test('Can auto link node', async ({ comfyPage }) => {
await comfyPage.disconnectEdge()
// Select the second item as the first item is always reroute
await perfMonitor.measureOperation(
'search-and-auto-link-node',
async () => {
await comfyPage.searchBox.fillAndSelectFirstNode('CLIPTextEncode', {
suggestionIndex: 0
})
}
)
await comfyPage.searchBox.fillAndSelectFirstNode('CLIPTextEncode', {
suggestionIndex: 0
})
await expect(comfyPage.canvas).toHaveScreenshot('auto-linked-node.png')
await perfMonitor.finishMonitoring(testName)
})
test.skip('@perf Can auto link batch moved node', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'auto-link-batch-moved-node'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('load-workflow', async () => {
await comfyPage.loadWorkflow('batch_move_links')
})
test('Can auto link batch moved node', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('batch_move_links')
const outputSlot1Pos = {
x: 304,
@@ -93,57 +54,29 @@ test.describe('Node search box', () => {
x: 5,
y: 5
}
await perfMonitor.measureOperation('batch-move-links', async () => {
await comfyPage.page.keyboard.down('Shift')
await comfyPage.dragAndDrop(outputSlot1Pos, emptySpacePos)
await comfyPage.page.keyboard.up('Shift')
})
await comfyPage.page.keyboard.down('Shift')
await comfyPage.dragAndDrop(outputSlot1Pos, emptySpacePos)
await comfyPage.page.keyboard.up('Shift')
// Select the second item as the first item is always reroute
await perfMonitor.measureOperation(
'search-and-auto-link-batch-node',
async () => {
await comfyPage.searchBox.fillAndSelectFirstNode('Load Checkpoint', {
suggestionIndex: 0
})
}
)
await comfyPage.searchBox.fillAndSelectFirstNode('Load Checkpoint', {
suggestionIndex: 0
})
await expect(comfyPage.canvas).toHaveScreenshot(
'auto-linked-node-batch.png'
)
await perfMonitor.finishMonitoring(testName)
})
test.skip('@perf Link release connecting to node with no slots', async ({
test('Link release connecting to node with no slots', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'link-release-no-slots'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('disconnect-edge', async () => {
await comfyPage.disconnectEdge()
})
await comfyPage.disconnectEdge()
await expect(comfyPage.searchBox.input).toHaveCount(1)
await perfMonitor.measureOperation('remove-filter-chip', async () => {
await comfyPage.page.locator('.p-chip-remove-icon').click()
})
await perfMonitor.measureOperation('add-node-no-connection', async () => {
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
})
await comfyPage.page.locator('.p-chip-remove-icon').click()
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
await expect(comfyPage.canvas).toHaveScreenshot(
'added-node-no-connection.png'
)
await perfMonitor.finishMonitoring(testName)
})
test('Has correct aria-labels on search results', async ({ comfyPage }) => {
@@ -239,10 +172,10 @@ test.describe('Node search box', () => {
await comfyPage.page.mouse.click(panelBounds!.x - 10, panelBounds!.y - 10)
// Verify the filter selection panel is hidden
await expect(panel.header).not.toBeVisible()
expect(panel.header).not.toBeVisible()
// Verify the node search dialog is still visible
await expect(comfyPage.searchBox.input).toBeVisible()
expect(comfyPage.searchBox.input).toBeVisible()
})
test('Can add multiple filters', async ({ comfyPage }) => {
@@ -319,38 +252,16 @@ test.describe('Release context menu', () => {
)
})
test.skip('@perf Can search and add node from context menu', async ({
test('Can search and add node from context menu', async ({
comfyPage,
comfyMouse
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'context-menu-search-add-node'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('disconnect-edge', async () => {
await comfyPage.disconnectEdge()
})
await perfMonitor.measureOperation('position-mouse', async () => {
await comfyMouse.move({ x: 10, y: 10 })
})
await perfMonitor.measureOperation(
'click-context-menu-search',
async () => {
await comfyPage.clickContextMenuItem('Search')
}
)
await perfMonitor.measureOperation('search-and-add-node', async () => {
await comfyPage.searchBox.fillAndSelectFirstNode('CLIP Prompt')
})
await comfyPage.disconnectEdge()
await comfyMouse.move({ x: 10, y: 10 })
await comfyPage.clickContextMenuItem('Search')
await comfyPage.searchBox.fillAndSelectFirstNode('CLIP Prompt')
await expect(comfyPage.canvas).toHaveScreenshot(
'link-context-menu-search.png'
)
await perfMonitor.finishMonitoring(testName)
})
})

View File

@@ -1,315 +0,0 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { PerformanceMonitor } from '../helpers/performanceMonitor'
test.describe('Performance Tests', () => {
test('@perf Navigation performance with default workflow', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'navigation-default-workflow'
await perfMonitor.startMonitoring(testName)
// Load default workflow for consistent starting state
await perfMonitor.measureOperation('load-default-workflow', async () => {
await comfyPage.loadWorkflow('default')
})
// Test basic panning operations
await perfMonitor.measureOperation('pan-operations', async () => {
// Pan in different directions
await comfyPage.canvas.dispatchEvent('wheel', {
deltaX: 100,
deltaY: 0,
ctrlKey: false,
shiftKey: true
})
await comfyPage.nextFrame()
await comfyPage.canvas.dispatchEvent('wheel', {
deltaX: -100,
deltaY: 100,
ctrlKey: false,
shiftKey: true
})
await comfyPage.nextFrame()
await comfyPage.canvas.dispatchEvent('wheel', {
deltaX: 0,
deltaY: -100,
ctrlKey: false,
shiftKey: true
})
await comfyPage.nextFrame()
})
// Test zoom operations
await perfMonitor.measureOperation('zoom-operations', async () => {
// Zoom in
await comfyPage.canvas.dispatchEvent('wheel', {
deltaY: -100,
ctrlKey: true
})
await comfyPage.nextFrame()
// Zoom out
await comfyPage.canvas.dispatchEvent('wheel', {
deltaY: 100,
ctrlKey: true
})
await comfyPage.nextFrame()
// Zoom way out
await comfyPage.canvas.dispatchEvent('wheel', {
deltaY: 500,
ctrlKey: true
})
await comfyPage.nextFrame()
// Reset to fit
await comfyPage.executeCommand('Comfy.Canvas.FitView')
await comfyPage.nextFrame()
})
// Test viewport reset
await perfMonitor.measureOperation('viewport-reset', async () => {
await comfyPage.resetView()
await comfyPage.nextFrame()
})
await perfMonitor.finishMonitoring(testName)
})
test('@perf Workflow loading performance - small workflow', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'load-small-workflow'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('load-img2img-default', async () => {
await comfyPage.loadWorkflow('performance-img2img-default')
})
// Basic navigation after loading
await perfMonitor.measureOperation('post-load-navigation', async () => {
await comfyPage.executeCommand('Comfy.Canvas.FitView')
await comfyPage.nextFrame()
// Quick zoom test
await comfyPage.canvas.dispatchEvent('wheel', {
deltaY: -200,
ctrlKey: true
})
await comfyPage.nextFrame()
})
await perfMonitor.finishMonitoring(testName)
})
test('@perf Workflow loading performance - large workflow', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'load-large-workflow'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('load-img2img-huge', async () => {
await comfyPage.loadWorkflow('performance-img2img-huge')
})
// Navigation with large workflow
await perfMonitor.measureOperation(
'large-workflow-navigation',
async () => {
await comfyPage.executeCommand('Comfy.Canvas.FitView')
await comfyPage.nextFrame()
// Pan around the large workflow
await comfyPage.canvas.dispatchEvent('wheel', {
deltaX: 200,
deltaY: 0,
ctrlKey: false,
shiftKey: true
})
await comfyPage.nextFrame()
await comfyPage.canvas.dispatchEvent('wheel', {
deltaX: -200,
deltaY: 200,
ctrlKey: false,
shiftKey: true
})
await comfyPage.nextFrame()
}
)
await perfMonitor.finishMonitoring(testName)
})
test('@perf Workflow loading performance - many nodes workflow', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'load-many-nodes-workflow'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation(
'load-dozens-load-image-nodes',
async () => {
await comfyPage.loadWorkflow('performance-dozens-load-image-nodes')
}
)
// Test performance with many similar nodes
await perfMonitor.measureOperation('many-nodes-navigation', async () => {
// Fit to view all nodes
await comfyPage.executeCommand('Comfy.Canvas.FitView')
await comfyPage.nextFrame()
// Zoom in to see details
await comfyPage.canvas.dispatchEvent('wheel', {
deltaY: -300,
ctrlKey: true
})
await comfyPage.nextFrame()
// Pan to explore different areas
for (let i = 0; i < 3; i++) {
await comfyPage.canvas.dispatchEvent('wheel', {
deltaX: 150,
deltaY: 100,
ctrlKey: false,
shiftKey: true
})
await comfyPage.nextFrame()
}
// Zoom back out
await comfyPage.canvas.dispatchEvent('wheel', {
deltaY: 300,
ctrlKey: true
})
await comfyPage.nextFrame()
})
await perfMonitor.finishMonitoring(testName)
})
test('@perf Viewport manipulation stress test', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'viewport-stress-test'
await perfMonitor.startMonitoring(testName)
// Load a workflow for context
await perfMonitor.measureOperation('load-test-workflow', async () => {
await comfyPage.loadWorkflow('performance-img2img-default')
})
// Rapid zoom in/out cycles
await perfMonitor.measureOperation('rapid-zoom-cycles', async () => {
for (let i = 0; i < 5; i++) {
// Zoom in
await comfyPage.canvas.dispatchEvent('wheel', {
deltaY: -150,
ctrlKey: true
})
await comfyPage.nextFrame()
// Zoom out
await comfyPage.canvas.dispatchEvent('wheel', {
deltaY: 150,
ctrlKey: true
})
await comfyPage.nextFrame()
}
})
// Rapid panning in different directions
await perfMonitor.measureOperation('rapid-pan-cycles', async () => {
const panDirections = [
{ deltaX: 100, deltaY: 0 },
{ deltaX: 0, deltaY: 100 },
{ deltaX: -100, deltaY: 0 },
{ deltaX: 0, deltaY: -100 }
]
for (let cycle = 0; cycle < 3; cycle++) {
for (const direction of panDirections) {
await comfyPage.canvas.dispatchEvent('wheel', {
deltaX: direction.deltaX,
deltaY: direction.deltaY,
ctrlKey: false,
shiftKey: true
})
await comfyPage.nextFrame()
}
}
})
// Combined zoom and pan operations
await perfMonitor.measureOperation('combined-operations', async () => {
for (let i = 0; i < 4; i++) {
// Zoom in while panning
await comfyPage.canvas.dispatchEvent('wheel', {
deltaY: -100,
ctrlKey: true
})
await comfyPage.canvas.dispatchEvent('wheel', {
deltaX: 50,
deltaY: 25,
ctrlKey: false,
shiftKey: true
})
await comfyPage.nextFrame()
}
// Reset to clean state
await comfyPage.executeCommand('Comfy.Canvas.FitView')
await comfyPage.nextFrame()
})
await perfMonitor.finishMonitoring(testName)
})
test.skip('@perf Sequential workflow loading performance', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'sequential-workflow-loading'
await perfMonitor.startMonitoring(testName)
const workflows = [
'performance-img2img-default',
'performance-dozens-load-image-nodes',
'performance-img2img-huge'
]
for (const workflow of workflows) {
await perfMonitor.measureOperation(`load-${workflow}`, async () => {
await comfyPage.loadWorkflow(workflow)
})
// Brief navigation after each load
await perfMonitor.measureOperation(`navigate-${workflow}`, async () => {
await comfyPage.executeCommand('Comfy.Canvas.FitView')
await comfyPage.nextFrame()
await comfyPage.canvas.dispatchEvent('wheel', {
deltaY: -100,
ctrlKey: true
})
await comfyPage.nextFrame()
})
}
await perfMonitor.finishMonitoring(testName)
})
})

View File

@@ -2,114 +2,46 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import type { NodeReference } from '../fixtures/utils/litegraphUtils'
import { PerformanceMonitor } from '../helpers/performanceMonitor'
test.describe('Primitive Node', () => {
test('@perf Can load with correct size', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'primitive-node-load'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('load-workflow', async () => {
await comfyPage.loadWorkflow('primitive/primitive_node')
})
test('Can load with correct size', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('primitive/primitive_node')
await expect(comfyPage.canvas).toHaveScreenshot('primitive_node.png')
await perfMonitor.finishMonitoring(testName)
})
// When link is dropped on widget, it should automatically convert the widget
// to input.
test('@perf Can connect to widget', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'primitive-node-connect-widget'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('load-workflow', async () => {
await comfyPage.loadWorkflow('primitive/primitive_node_unconnected')
})
let primitiveNode: NodeReference
let ksamplerNode: NodeReference
await perfMonitor.measureOperation('get-node-references', async () => {
primitiveNode = await comfyPage.getNodeRefById(1)
ksamplerNode = await comfyPage.getNodeRefById(2)
})
test('Can connect to widget', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('primitive/primitive_node_unconnected')
const primitiveNode: NodeReference = await comfyPage.getNodeRefById(1)
const ksamplerNode: NodeReference = await comfyPage.getNodeRefById(2)
// Connect the output of the primitive node to the input of first widget of the ksampler node
await perfMonitor.measureOperation('connect-widget', async () => {
await primitiveNode!.connectWidget(0, ksamplerNode!, 0)
})
await primitiveNode.connectWidget(0, ksamplerNode, 0)
await expect(comfyPage.canvas).toHaveScreenshot(
'primitive_node_connected.png'
)
await perfMonitor.finishMonitoring(testName)
})
test('@perf Can connect to dom widget', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'primitive-node-connect-dom-widget'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('load-workflow', async () => {
await comfyPage.loadWorkflow(
'primitive/primitive_node_unconnected_dom_widget'
)
})
let primitiveNode: NodeReference
let clipEncoderNode: NodeReference
await perfMonitor.measureOperation('get-node-references', async () => {
primitiveNode = await comfyPage.getNodeRefById(1)
clipEncoderNode = await comfyPage.getNodeRefById(2)
})
await perfMonitor.measureOperation('connect-dom-widget', async () => {
await primitiveNode!.connectWidget(0, clipEncoderNode!, 0)
})
test('Can connect to dom widget', async ({ comfyPage }) => {
await comfyPage.loadWorkflow(
'primitive/primitive_node_unconnected_dom_widget'
)
const primitiveNode: NodeReference = await comfyPage.getNodeRefById(1)
const clipEncoderNode: NodeReference = await comfyPage.getNodeRefById(2)
await primitiveNode.connectWidget(0, clipEncoderNode, 0)
await expect(comfyPage.canvas).toHaveScreenshot(
'primitive_node_connected_dom_widget.png'
)
await perfMonitor.finishMonitoring(testName)
})
test('@perf Can connect to static primitive node', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'primitive-node-connect-static'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('load-workflow', async () => {
await comfyPage.loadWorkflow('primitive/static_primitive_unconnected')
})
let primitiveNode: NodeReference
let ksamplerNode: NodeReference
await perfMonitor.measureOperation('get-node-references', async () => {
primitiveNode = await comfyPage.getNodeRefById(1)
ksamplerNode = await comfyPage.getNodeRefById(2)
})
await perfMonitor.measureOperation('connect-static-primitive', async () => {
await primitiveNode!.connectWidget(0, ksamplerNode!, 0)
})
test('Can connect to static primitive node', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('primitive/static_primitive_unconnected')
const primitiveNode: NodeReference = await comfyPage.getNodeRefById(1)
const ksamplerNode: NodeReference = await comfyPage.getNodeRefById(2)
await primitiveNode.connectWidget(0, ksamplerNode, 0)
await expect(comfyPage.canvas).toHaveScreenshot(
'static_primitive_connected.png'
)
await perfMonitor.finishMonitoring(testName)
})
test('Report missing nodes when connect to missing node', async ({

View File

@@ -2,7 +2,6 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { getMiddlePoint } from '../fixtures/utils/litegraphUtils'
import { PerformanceMonitor } from '../helpers/performanceMonitor'
test.describe('Reroute Node', () => {
test.beforeEach(async ({ comfyPage }) => {
@@ -13,57 +12,29 @@ test.describe('Reroute Node', () => {
await comfyPage.setupWorkflowsDirectory({})
})
// Skip because fails with vue widget nodes (reason not investigated)
test.skip('@perf loads from inserted workflow', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'load-workflow-with-reroute'
await perfMonitor.startMonitoring(testName)
test('loads from inserted workflow', async ({ comfyPage }) => {
const workflowName = 'single_connected_reroute_node.json'
await perfMonitor.measureOperation('setup-workflow-directory', async () => {
await comfyPage.setupWorkflowsDirectory({
[workflowName]: workflowName
})
})
await perfMonitor.measureOperation('setup-page', async () => {
await comfyPage.setup()
})
await perfMonitor.measureOperation('create-new-workflow', async () => {
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
await comfyPage.setupWorkflowsDirectory({
[workflowName]: workflowName
})
await comfyPage.setup()
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
// Insert the workflow
const workflowsTab = comfyPage.menu.workflowsTab
await perfMonitor.measureOperation('open-workflows-tab', async () => {
await workflowsTab.open()
await workflowsTab.open()
await workflowsTab.getPersistedItem(workflowName).click({ button: 'right' })
const insertButton = comfyPage.page.locator('.p-contextmenu-item-link', {
hasText: 'Insert'
})
await insertButton.click()
await perfMonitor.measureOperation('insert-workflow', async () => {
await workflowsTab
.getPersistedItem(workflowName)
.click({ button: 'right' })
const insertButton = comfyPage.page.locator('.p-contextmenu-item-link', {
hasText: 'Insert'
})
await insertButton.click()
})
await perfMonitor.measureOperation('close-sidebar', async () => {
// Close the sidebar tab
await workflowsTab.tabButton.click()
await workflowsTab.root.waitFor({ state: 'hidden' })
})
await perfMonitor.measureOperation('set-focus-mode', async () => {
await comfyPage.setFocusMode(true)
})
// Close the sidebar tab
await workflowsTab.tabButton.click()
await workflowsTab.root.waitFor({ state: 'hidden' })
await comfyPage.setFocusMode(true)
await expect(comfyPage.canvas).toHaveScreenshot('reroute_inserted.png')
await perfMonitor.finishMonitoring(testName)
})
})
@@ -72,108 +43,53 @@ test.describe('LiteGraph Native Reroute Node', () => {
await comfyPage.setSetting('LiteGraph.Reroute.SplineOffset', 80)
})
test('@perf loads from workflow', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'load-native-reroute-workflow'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('load-workflow', async () => {
await comfyPage.loadWorkflow('reroute/native_reroute')
})
test('loads from workflow', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('reroute/native_reroute')
await expect(comfyPage.canvas).toHaveScreenshot('native_reroute.png')
await perfMonitor.finishMonitoring(testName)
})
test('@perf Can add reroute by alt clicking on link', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'add-reroute-alt-click'
test('Can add reroute by alt clicking on link', async ({ comfyPage }) => {
const loadCheckpointNode = (
await comfyPage.getNodeRefsByTitle('Load Checkpoint')
)[0]
const clipEncodeNode = (
await comfyPage.getNodeRefsByTitle('CLIP Text Encode (Prompt)')
)[0]
await perfMonitor.startMonitoring(testName)
const slot1 = await loadCheckpointNode.getOutput(1)
const slot2 = await clipEncodeNode.getInput(0)
const middlePoint = getMiddlePoint(
await slot1.getPosition(),
await slot2.getPosition()
)
let loadCheckpointNode: any
let clipEncodeNode: any
await perfMonitor.measureOperation('get-nodes', async () => {
loadCheckpointNode = (
await comfyPage.getNodeRefsByTitle('Load Checkpoint')
)[0]
clipEncodeNode = (
await comfyPage.getNodeRefsByTitle('CLIP Text Encode (Prompt)')
)[0]
})
let slot1: any
let slot2: any
let middlePoint: any
await perfMonitor.measureOperation('calculate-link-position', async () => {
slot1 = await loadCheckpointNode.getOutput(1)
slot2 = await clipEncodeNode.getInput(0)
middlePoint = getMiddlePoint(
await slot1.getPosition(),
await slot2.getPosition()
)
})
await perfMonitor.measureOperation('alt-click-link', async () => {
await comfyPage.page.keyboard.down('Alt')
await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y)
await comfyPage.page.keyboard.up('Alt')
})
await comfyPage.page.keyboard.down('Alt')
await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y)
await comfyPage.page.keyboard.up('Alt')
await expect(comfyPage.canvas).toHaveScreenshot(
'native_reroute_alt_click.png'
)
await perfMonitor.finishMonitoring(testName)
})
test.skip('@perf Can add reroute by clicking middle of link context menu', async ({
test('Can add reroute by clicking middle of link context menu', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'add-reroute-context-menu'
const loadCheckpointNode = (
await comfyPage.getNodeRefsByTitle('Load Checkpoint')
)[0]
const clipEncodeNode = (
await comfyPage.getNodeRefsByTitle('CLIP Text Encode (Prompt)')
)[0]
await perfMonitor.startMonitoring(testName)
let loadCheckpointNode: any
let clipEncodeNode: any
await perfMonitor.measureOperation('get-nodes', async () => {
loadCheckpointNode = (
await comfyPage.getNodeRefsByTitle('Load Checkpoint')
)[0]
clipEncodeNode = (
await comfyPage.getNodeRefsByTitle('CLIP Text Encode (Prompt)')
)[0]
})
let slot1: any
let slot2: any
let middlePoint: any
await perfMonitor.measureOperation('calculate-link-position', async () => {
slot1 = await loadCheckpointNode.getOutput(1)
slot2 = await clipEncodeNode.getInput(0)
middlePoint = getMiddlePoint(
await slot1.getPosition(),
await slot2.getPosition()
)
})
await perfMonitor.measureOperation(
'click-link-for-context-menu',
async () => {
await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y)
}
const slot1 = await loadCheckpointNode.getOutput(1)
const slot2 = await clipEncodeNode.getInput(0)
const middlePoint = getMiddlePoint(
await slot1.getPosition(),
await slot2.getPosition()
)
// Context menu interaction not monitored (floating menu - skip per guide)
await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y)
await comfyPage.page
.locator('.litecontextmenu .litemenu-entry', { hasText: 'Add Reroute' })
.click()
@@ -181,7 +97,5 @@ test.describe('LiteGraph Native Reroute Node', () => {
await expect(comfyPage.canvas).toHaveScreenshot(
'native_reroute_context_menu.png'
)
await perfMonitor.finishMonitoring(testName)
})
})

View File

@@ -2,145 +2,65 @@ import { expect } from '@playwright/test'
import { NodeBadgeMode } from '../../src/types/nodeSource'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { PerformanceMonitor } from '../helpers/performanceMonitor'
test.describe('Canvas Right Click Menu', () => {
test.skip('@perf Can add node', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'add-node-from-menu'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('right-click-canvas', async () => {
await comfyPage.rightClickCanvas()
})
test('Can add node', async ({ comfyPage }) => {
await comfyPage.rightClickCanvas()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png')
await perfMonitor.measureOperation('navigate-to-node', async () => {
await comfyPage.page.getByText('Add Node').click()
await comfyPage.nextFrame()
await comfyPage.page.getByText('loaders').click()
await comfyPage.nextFrame()
await comfyPage.page.getByText('Load VAE').click()
await comfyPage.nextFrame()
})
await comfyPage.page.getByText('Add Node').click()
await comfyPage.nextFrame()
await comfyPage.page.getByText('loaders').click()
await comfyPage.nextFrame()
await comfyPage.page.getByText('Load VAE').click()
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('add-node-node-added.png')
await perfMonitor.finishMonitoring(testName)
})
test.skip('@perf Can add group', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'add-group-from-menu'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('right-click-canvas', async () => {
await comfyPage.rightClickCanvas()
})
test('Can add group', async ({ comfyPage }) => {
await comfyPage.rightClickCanvas()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png')
await perfMonitor.measureOperation('add-group', async () => {
await comfyPage.page.getByText('Add Group', { exact: true }).click()
await comfyPage.nextFrame()
})
await comfyPage.page.getByText('Add Group', { exact: true }).click()
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('add-group-group-added.png')
await perfMonitor.finishMonitoring(testName)
})
// Skip because fails with vue widget nodes (reason not investigated)
test.skip('@perf Can convert to group node', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'convert-to-group-node'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('select-nodes', async () => {
await comfyPage.select2Nodes()
})
test('Can convert to group node', async ({ comfyPage }) => {
await comfyPage.select2Nodes()
await expect(comfyPage.canvas).toHaveScreenshot('selected-2-nodes.png')
await perfMonitor.measureOperation('right-click-canvas', async () => {
await comfyPage.rightClickCanvas()
})
await perfMonitor.measureOperation('convert-to-group-node', async () => {
await comfyPage.clickContextMenuItem('Convert to Group Node')
await comfyPage.promptDialogInput.fill('GroupNode2CLIP')
await comfyPage.page.keyboard.press('Enter')
await comfyPage.promptDialogInput.waitFor({ state: 'hidden' })
await comfyPage.nextFrame()
})
await comfyPage.rightClickCanvas()
await comfyPage.clickContextMenuItem('Convert to Group Node')
await comfyPage.promptDialogInput.fill('GroupNode2CLIP')
await comfyPage.page.keyboard.press('Enter')
await comfyPage.promptDialogInput.waitFor({ state: 'hidden' })
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'right-click-node-group-node.png'
)
await perfMonitor.finishMonitoring(testName)
})
})
test.describe('Node Right Click Menu', () => {
test.skip('@perf Can open properties panel', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'open-properties-panel'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('right-click-node', async () => {
await comfyPage.rightClickEmptyLatentNode()
})
test('Can open properties panel', async ({ comfyPage }) => {
await comfyPage.rightClickEmptyLatentNode()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
await perfMonitor.measureOperation('open-properties-panel', async () => {
await comfyPage.page.getByText('Properties Panel').click()
await comfyPage.nextFrame()
})
await comfyPage.page.getByText('Properties Panel').click()
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'right-click-node-properties-panel.png'
)
await perfMonitor.finishMonitoring(testName)
})
test.skip('@perf Can collapse', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'collapse-node'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('right-click-node', async () => {
await comfyPage.rightClickEmptyLatentNode()
})
test('Can collapse', async ({ comfyPage }) => {
await comfyPage.rightClickEmptyLatentNode()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
await perfMonitor.measureOperation('collapse-node', async () => {
await comfyPage.page.getByText('Collapse').click()
await comfyPage.nextFrame()
})
await comfyPage.page.getByText('Collapse').click()
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'right-click-node-collapsed.png'
)
await perfMonitor.finishMonitoring(testName)
})
test.skip('@perf Can collapse (Node Badge)', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'collapse-node-with-badge'
await perfMonitor.startMonitoring(testName)
test('Can collapse (Node Badge)', async ({ comfyPage }) => {
await comfyPage.setSetting(
'Comfy.NodeBadge.NodeIdBadgeMode',
NodeBadgeMode.ShowAll
@@ -150,200 +70,88 @@ test.describe('Node Right Click Menu', () => {
NodeBadgeMode.ShowAll
)
await perfMonitor.measureOperation('right-click-node', async () => {
await comfyPage.rightClickEmptyLatentNode()
})
await perfMonitor.measureOperation('collapse-node-with-badge', async () => {
await comfyPage.page.getByText('Collapse').click()
await comfyPage.nextFrame()
})
await comfyPage.rightClickEmptyLatentNode()
await comfyPage.page.getByText('Collapse').click()
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'right-click-node-collapsed-badge.png'
)
await perfMonitor.finishMonitoring(testName)
})
test.skip('@perf Can bypass', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'bypass-node'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('right-click-node', async () => {
await comfyPage.rightClickEmptyLatentNode()
})
test('Can bypass', async ({ comfyPage }) => {
await comfyPage.rightClickEmptyLatentNode()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
await perfMonitor.measureOperation('bypass-node', async () => {
await comfyPage.page.getByText('Bypass').click()
await comfyPage.nextFrame()
})
await comfyPage.page.getByText('Bypass').click()
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'right-click-node-bypassed.png'
)
await perfMonitor.finishMonitoring(testName)
})
test.skip('@perf Can pin and unpin', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'pin-unpin-node'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('right-click-node', async () => {
await comfyPage.rightClickEmptyLatentNode()
})
test('Can pin and unpin', async ({ comfyPage }) => {
await comfyPage.rightClickEmptyLatentNode()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
await perfMonitor.measureOperation('pin-node', async () => {
await comfyPage.page.click('.litemenu-entry:has-text("Pin")')
await comfyPage.nextFrame()
})
await perfMonitor.measureOperation('drag-pinned-node', async () => {
await comfyPage.dragAndDrop({ x: 621, y: 617 }, { x: 16, y: 16 })
})
await comfyPage.page.click('.litemenu-entry:has-text("Pin")')
await comfyPage.nextFrame()
await comfyPage.dragAndDrop({ x: 621, y: 617 }, { x: 16, y: 16 })
await expect(comfyPage.canvas).toHaveScreenshot('node-pinned.png')
await perfMonitor.measureOperation('right-click-pinned-node', async () => {
await comfyPage.rightClickEmptyLatentNode()
})
await comfyPage.rightClickEmptyLatentNode()
await expect(comfyPage.canvas).toHaveScreenshot(
'right-click-pinned-node.png'
)
await perfMonitor.measureOperation('unpin-node', async () => {
await comfyPage.page.click('.litemenu-entry:has-text("Unpin")')
await comfyPage.nextFrame()
})
await perfMonitor.measureOperation(
'right-click-unpinned-node',
async () => {
await comfyPage.rightClickEmptyLatentNode()
}
)
await comfyPage.page.click('.litemenu-entry:has-text("Unpin")')
await comfyPage.nextFrame()
await comfyPage.rightClickEmptyLatentNode()
await expect(comfyPage.canvas).toHaveScreenshot(
'right-click-unpinned-node.png'
)
await perfMonitor.finishMonitoring(testName)
})
test.skip('@perf Can move after unpin', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'move-after-unpin'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('pin-node', async () => {
await comfyPage.rightClickEmptyLatentNode()
await comfyPage.page.click('.litemenu-entry:has-text("Pin")')
await comfyPage.nextFrame()
})
await perfMonitor.measureOperation('unpin-node', async () => {
await comfyPage.rightClickEmptyLatentNode()
await comfyPage.page.click('.litemenu-entry:has-text("Unpin")')
await comfyPage.nextFrame()
await comfyPage.page.waitForTimeout(256)
})
await perfMonitor.measureOperation('move-unpinned-node', async () => {
await comfyPage.dragAndDrop({ x: 496, y: 618 }, { x: 200, y: 590 })
})
test('Can move after unpin', async ({ comfyPage }) => {
await comfyPage.rightClickEmptyLatentNode()
await comfyPage.page.click('.litemenu-entry:has-text("Pin")')
await comfyPage.nextFrame()
await comfyPage.rightClickEmptyLatentNode()
await comfyPage.page.click('.litemenu-entry:has-text("Unpin")')
await comfyPage.nextFrame()
await comfyPage.page.waitForTimeout(256)
await comfyPage.dragAndDrop({ x: 496, y: 618 }, { x: 200, y: 590 })
await expect(comfyPage.canvas).toHaveScreenshot(
'right-click-unpinned-node-moved.png'
)
await perfMonitor.finishMonitoring(testName)
})
test.skip('@perf Can pin/unpin selected nodes', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'pin-unpin-selected-nodes'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('select-multiple-nodes', async () => {
await comfyPage.select2Nodes()
})
await perfMonitor.measureOperation('pin-selected-nodes', async () => {
await comfyPage.page.keyboard.down('Control')
await comfyPage.rightClickEmptyLatentNode()
await comfyPage.page.click('.litemenu-entry:has-text("Pin")')
await comfyPage.page.keyboard.up('Control')
await comfyPage.nextFrame()
})
test('Can pin/unpin selected nodes', async ({ comfyPage }) => {
await comfyPage.select2Nodes()
await comfyPage.page.keyboard.down('Control')
await comfyPage.rightClickEmptyLatentNode()
await comfyPage.page.click('.litemenu-entry:has-text("Pin")')
await comfyPage.page.keyboard.up('Control')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('selected-nodes-pinned.png')
await perfMonitor.measureOperation('unpin-selected-nodes', async () => {
await comfyPage.rightClickEmptyLatentNode()
await comfyPage.page.click('.litemenu-entry:has-text("Unpin")')
await comfyPage.nextFrame()
})
await comfyPage.rightClickEmptyLatentNode()
await comfyPage.page.click('.litemenu-entry:has-text("Unpin")')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'selected-nodes-unpinned.png'
)
await perfMonitor.finishMonitoring(testName)
})
test('@perf Can clone pinned nodes', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'clone-pinned-node'
await perfMonitor.startMonitoring(testName)
let nodeCount: number
await perfMonitor.measureOperation('get-initial-node-count', async () => {
nodeCount = await comfyPage.getGraphNodesCount()
})
let node: any
await perfMonitor.measureOperation('get-node-reference', async () => {
node = (await comfyPage.getFirstNodeRef())!
})
await perfMonitor.measureOperation('pin-node', async () => {
await node.clickContextMenuOption('Pin')
await comfyPage.nextFrame()
})
await perfMonitor.measureOperation('right-click-pinned-node', async () => {
await node.click('title', { button: 'right' })
})
test('Can clone pinned nodes', async ({ comfyPage }) => {
const nodeCount = await comfyPage.getGraphNodesCount()
const node = (await comfyPage.getFirstNodeRef())!
await node.clickContextMenuOption('Pin')
await comfyPage.nextFrame()
await node.click('title', { button: 'right' })
await expect(
comfyPage.page.locator('.litemenu-entry:has-text("Unpin")')
).toBeAttached()
const cloneItem = comfyPage.page.locator(
'.litemenu-entry:has-text("Clone")'
)
await perfMonitor.measureOperation('clone-node', async () => {
await cloneItem.click()
await expect(cloneItem).toHaveCount(0)
await comfyPage.nextFrame()
})
expect(await comfyPage.getGraphNodesCount()).toBe(nodeCount! + 1)
await perfMonitor.finishMonitoring(testName)
await cloneItem.click()
await expect(cloneItem).toHaveCount(0)
await comfyPage.nextFrame()
expect(await comfyPage.getGraphNodesCount()).toBe(nodeCount + 1)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 101 KiB

View File

@@ -1,7 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture } from '../fixtures/ComfyPage'
import { PerformanceMonitor } from '../helpers/performanceMonitor'
const test = comfyPageFixture
@@ -13,21 +12,14 @@ test.describe('Selection Toolbox', () => {
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
})
test('@perf shows selection toolbox', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'show-selection-toolbox'
await perfMonitor.startMonitoring(testName)
test('shows selection toolbox', async ({ comfyPage }) => {
// By default, selection toolbox should be enabled
expect(
await comfyPage.page.locator('.selection-overlay-container').isVisible()
).toBe(false)
// Select multiple nodes
await perfMonitor.measureOperation('select-multiple-nodes', async () => {
await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)'])
})
await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)'])
// Selection toolbox should be visible with multiple nodes selected
await expect(
@@ -36,37 +28,16 @@ test.describe('Selection Toolbox', () => {
await expect(
comfyPage.page.locator('.selection-overlay-container.show-border')
).toBeVisible()
await perfMonitor.finishMonitoring(testName)
})
test('@perf shows at correct position when node is pasted', async ({
test('shows at correct position when node is pasted', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'node-paste-position'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('load-workflow', async () => {
await comfyPage.loadWorkflow('single_ksampler')
})
await perfMonitor.measureOperation('select-node', async () => {
await comfyPage.selectNodes(['KSampler'])
})
await perfMonitor.measureOperation('copy-node', async () => {
await comfyPage.ctrlC()
})
await perfMonitor.measureOperation('position-mouse', async () => {
await comfyPage.page.mouse.move(100, 100)
})
await perfMonitor.measureOperation('paste-node', async () => {
await comfyPage.ctrlV()
})
await comfyPage.loadWorkflow('single_ksampler')
await comfyPage.selectNodes(['KSampler'])
await comfyPage.ctrlC()
await comfyPage.page.mouse.move(100, 100)
await comfyPage.ctrlV()
const overlayContainer = comfyPage.page.locator(
'.selection-overlay-container'
@@ -80,60 +51,28 @@ test.describe('Selection Toolbox', () => {
expect(Math.round(boundingBox!.x)).toBeCloseTo(90, -1) // Allow ~10px tolerance
// 30px offset of node title height
expect(Math.round(boundingBox!.y)).toBeCloseTo(60, -1)
await perfMonitor.finishMonitoring(testName)
})
// Skip because fails with vue widget nodes (reason not investigated)
test.skip('@perf hide when select and drag happen at the same time', async ({
test('hide when select and drag happen at the same time', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'hide-toolbox-during-drag'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('load-workflow', async () => {
await comfyPage.loadWorkflow('single_ksampler')
})
let node: any
let nodePos: any
await perfMonitor.measureOperation('get-node-position', async () => {
node = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
nodePos = await node.getPosition()
})
await comfyPage.loadWorkflow('single_ksampler')
const node = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
const nodePos = await node.getPosition()
// Drag on the title of the node
await perfMonitor.measureOperation('start-drag', async () => {
await comfyPage.page.mouse.move(nodePos.x + 100, nodePos.y - 15)
await comfyPage.page.mouse.down()
})
await perfMonitor.measureOperation('drag-to-position', async () => {
await comfyPage.page.mouse.move(nodePos.x + 200, nodePos.y + 200)
})
await comfyPage.page.mouse.move(nodePos.x + 100, nodePos.y - 15)
await comfyPage.page.mouse.down()
await comfyPage.page.mouse.move(nodePos.x + 200, nodePos.y + 200)
await comfyPage.nextFrame()
await expect(
comfyPage.page.locator('.selection-overlay-container')
).not.toBeVisible()
await perfMonitor.finishMonitoring(testName)
})
test('@perf shows border only with multiple selections', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'border-multiple-selections'
await perfMonitor.startMonitoring(testName)
test('shows border only with multiple selections', async ({ comfyPage }) => {
// Select single node
await perfMonitor.measureOperation('select-single-node', async () => {
await comfyPage.selectNodes(['KSampler'])
})
await comfyPage.selectNodes(['KSampler'])
// Selection overlay should be visible but without border
await expect(
@@ -144,9 +83,7 @@ test.describe('Selection Toolbox', () => {
).not.toBeVisible()
// Select multiple nodes
await perfMonitor.measureOperation('select-multiple-nodes', async () => {
await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)'])
})
await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)'])
// Selection overlay should show border with multiple selections
await expect(
@@ -154,37 +91,23 @@ test.describe('Selection Toolbox', () => {
).toBeVisible()
// Deselect to single node
await perfMonitor.measureOperation('deselect-to-single', async () => {
await comfyPage.selectNodes(['CLIP Text Encode (Prompt)'])
})
await comfyPage.selectNodes(['CLIP Text Encode (Prompt)'])
// Border should be hidden again
await expect(
comfyPage.page.locator('.selection-overlay-container.show-border')
).not.toBeVisible()
await perfMonitor.finishMonitoring(testName)
})
test('@perf displays bypass button in toolbox when nodes are selected', async ({
test('displays bypass button in toolbox when nodes are selected', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'bypass-button-display'
await perfMonitor.startMonitoring(testName)
// A group + a KSampler node
await perfMonitor.measureOperation('load-workflow', async () => {
await comfyPage.loadWorkflow('single_group')
})
await comfyPage.loadWorkflow('single_group')
// Select group + node should show bypass button
await perfMonitor.measureOperation('select-all-nodes', async () => {
await comfyPage.page.focus('canvas')
await comfyPage.page.keyboard.press('Control+A')
})
await comfyPage.page.focus('canvas')
await comfyPage.page.keyboard.press('Control+A')
await expect(
comfyPage.page.locator(
'.selection-toolbox *[data-testid="bypass-button"]'
@@ -192,32 +115,20 @@ test.describe('Selection Toolbox', () => {
).toBeVisible()
// Deselect node (Only group is selected) should hide bypass button
await perfMonitor.measureOperation('select-single-node', async () => {
await comfyPage.selectNodes(['KSampler'])
})
await comfyPage.selectNodes(['KSampler'])
await expect(
comfyPage.page.locator(
'.selection-toolbox *[data-testid="bypass-button"]'
)
).not.toBeVisible()
await perfMonitor.finishMonitoring(testName)
})
test.describe('Color Picker', () => {
test('@perf displays color picker button and allows color selection', async ({
test('displays color picker button and allows color selection', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'color-picker-selection'
await perfMonitor.startMonitoring(testName)
// Select a node
await perfMonitor.measureOperation('select-node', async () => {
await comfyPage.selectNodes(['KSampler'])
})
await comfyPage.selectNodes(['KSampler'])
// Color picker button should be visible
const colorPickerButton = comfyPage.page.locator(
@@ -226,9 +137,7 @@ test.describe('Selection Toolbox', () => {
await expect(colorPickerButton).toBeVisible()
// Click color picker button
await perfMonitor.measureOperation('open-color-picker', async () => {
await colorPickerButton.click()
})
await colorPickerButton.click()
// Color picker dropdown should be visible
const colorPickerDropdown = comfyPage.page.locator(
@@ -237,12 +146,10 @@ test.describe('Selection Toolbox', () => {
await expect(colorPickerDropdown).toBeVisible()
// Select a color (e.g., blue)
await perfMonitor.measureOperation('select-color', async () => {
const blueColorOption = colorPickerDropdown.locator(
'i[data-testid="blue"]'
)
await blueColorOption.click()
})
const blueColorOption = colorPickerDropdown.locator(
'i[data-testid="blue"]'
)
await blueColorOption.click()
// Dropdown should close after selection
await expect(colorPickerDropdown).not.toBeVisible()
@@ -251,22 +158,13 @@ test.describe('Selection Toolbox', () => {
// Note: Exact verification method depends on how color is applied to nodes
const selectedNode = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
expect(selectedNode.getProperty('color')).not.toBeNull()
await perfMonitor.finishMonitoring(testName)
})
test.skip('@perf color picker shows current color of selected nodes', async ({
test('color picker shows current color of selected nodes', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'color-picker-current-color'
await perfMonitor.startMonitoring(testName)
// Select multiple nodes
await perfMonitor.measureOperation('select-multiple-nodes', async () => {
await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)'])
})
await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)'])
const colorPickerButton = comfyPage.page.locator(
'.selection-toolbox .pi-circle-fill'
@@ -276,136 +174,84 @@ test.describe('Selection Toolbox', () => {
await expect(colorPickerButton).not.toHaveAttribute('color')
// Click color picker and select a color
await perfMonitor.measureOperation('open-color-picker', async () => {
await colorPickerButton.click()
})
await perfMonitor.measureOperation('select-red-color', async () => {
const redColorOption = comfyPage.page.locator(
'.color-picker-container i[data-testid="red"]'
)
await redColorOption.click()
})
await colorPickerButton.click()
const redColorOption = comfyPage.page.locator(
'.color-picker-container i[data-testid="red"]'
)
await redColorOption.click()
// Button should now show the selected color
await expect(colorPickerButton).toHaveCSS('color', RED_COLOR)
await perfMonitor.finishMonitoring(testName)
})
test('@perf color picker shows mixed state for differently colored selections', async ({
test('color picker shows mixed state for differently colored selections', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'color-picker-mixed-state'
await perfMonitor.startMonitoring(testName)
// Select first node and color it
await perfMonitor.measureOperation('color-first-node', async () => {
await comfyPage.selectNodes(['KSampler'])
await comfyPage.page
.locator('.selection-toolbox .pi-circle-fill')
.click()
await comfyPage.page
.locator('.color-picker-container i[data-testid="blue"]')
.click()
await comfyPage.selectNodes(['KSampler'])
})
await comfyPage.selectNodes(['KSampler'])
await comfyPage.page.locator('.selection-toolbox .pi-circle-fill').click()
await comfyPage.page
.locator('.color-picker-container i[data-testid="blue"]')
.click()
await comfyPage.selectNodes(['KSampler'])
// Select second node and color it differently
await perfMonitor.measureOperation('color-second-node', async () => {
await comfyPage.selectNodes(['CLIP Text Encode (Prompt)'])
await comfyPage.page
.locator('.selection-toolbox .pi-circle-fill')
.click()
await comfyPage.page
.locator('.color-picker-container i[data-testid="red"]')
.click()
})
await comfyPage.selectNodes(['CLIP Text Encode (Prompt)'])
await comfyPage.page.locator('.selection-toolbox .pi-circle-fill').click()
await comfyPage.page
.locator('.color-picker-container i[data-testid="red"]')
.click()
// Select both nodes
await perfMonitor.measureOperation('select-both-nodes', async () => {
await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)'])
})
await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)'])
// Color picker should show null/mixed state
const colorPickerButton = comfyPage.page.locator(
'.selection-toolbox .pi-circle-fill'
)
await expect(colorPickerButton).not.toHaveAttribute('color')
await perfMonitor.finishMonitoring(testName)
})
test.skip('@perf color picker shows correct color when selecting pre-colored node', async ({
test('color picker shows correct color when selecting pre-colored node', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'color-picker-pre-colored'
await perfMonitor.startMonitoring(testName)
// First color a node
await perfMonitor.measureOperation('color-node-blue', async () => {
await comfyPage.selectNodes(['KSampler'])
await comfyPage.page
.locator('.selection-toolbox .pi-circle-fill')
.click()
await comfyPage.page
.locator('.color-picker-container i[data-testid="blue"]')
.click()
})
await comfyPage.selectNodes(['KSampler'])
await comfyPage.page.locator('.selection-toolbox .pi-circle-fill').click()
await comfyPage.page
.locator('.color-picker-container i[data-testid="blue"]')
.click()
// Clear selection
await perfMonitor.measureOperation('clear-selection', async () => {
await comfyPage.selectNodes(['KSampler'])
})
await comfyPage.selectNodes(['KSampler'])
// Re-select the node
await perfMonitor.measureOperation('reselect-node', async () => {
await comfyPage.selectNodes(['KSampler'])
})
await comfyPage.selectNodes(['KSampler'])
// Color picker button should show the correct color
const colorPickerButton = comfyPage.page.locator(
'.selection-toolbox .pi-circle-fill'
)
await expect(colorPickerButton).toHaveCSS('color', BLUE_COLOR)
await perfMonitor.finishMonitoring(testName)
})
test('@perf colorization via color picker can be undone', async ({
test('colorization via color picker can be undone', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'color-picker-undo'
await perfMonitor.startMonitoring(testName)
// Select a node and color it
await perfMonitor.measureOperation('color-node', async () => {
await comfyPage.selectNodes(['KSampler'])
await comfyPage.page
.locator('.selection-toolbox .pi-circle-fill')
.click()
await comfyPage.page
.locator('.color-picker-container i[data-testid="blue"]')
.click()
})
await comfyPage.selectNodes(['KSampler'])
await comfyPage.page.locator('.selection-toolbox .pi-circle-fill').click()
await comfyPage.page
.locator('.color-picker-container i[data-testid="blue"]')
.click()
// Undo the colorization
await perfMonitor.measureOperation('undo-operation', async () => {
await comfyPage.page.keyboard.press('Control+Z')
await comfyPage.nextFrame()
})
await comfyPage.page.keyboard.press('Control+Z')
await comfyPage.nextFrame()
// Node should be uncolored again
const selectedNode = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
expect(await selectedNode.getProperty('color')).toBeUndefined()
await perfMonitor.finishMonitoring(testName)
})
})
})

View File

@@ -32,9 +32,7 @@ test.describe('Templates', () => {
}
})
// TODO: Re-enable this test once issue resolved
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/3992
test.skip('should have all required thumbnail media for each template', async ({
test('should have all required thumbnail media for each template', async ({
comfyPage
}) => {
test.slow()
@@ -144,136 +142,4 @@ test.describe('Templates', () => {
// Expect the title to be used as fallback for the template categories
await expect(comfyPage.page.getByLabel('FALLBACK CATEGORY')).toBeVisible()
})
test('template cards are dynamically sized and responsive', async ({
comfyPage
}) => {
// Open templates dialog
await comfyPage.executeCommand('Comfy.BrowseTemplates')
await expect(comfyPage.templates.content).toBeVisible()
// Wait for at least one template card to appear
await expect(comfyPage.page.locator('.template-card').first()).toBeVisible({
timeout: 5000
})
// Take snapshot of the template grid
const templateGrid = comfyPage.templates.content.locator('.grid').first()
await expect(templateGrid).toBeVisible()
await expect(templateGrid).toHaveScreenshot('template-grid-desktop.png')
// Check cards at mobile viewport size
await comfyPage.page.setViewportSize({ width: 640, height: 800 })
await expect(templateGrid).toBeVisible()
await expect(templateGrid).toHaveScreenshot('template-grid-mobile.png')
// Check cards at tablet size
await comfyPage.page.setViewportSize({ width: 1024, height: 800 })
await expect(templateGrid).toBeVisible()
await expect(templateGrid).toHaveScreenshot('template-grid-tablet.png')
})
test('hover effects work on template cards', async ({ comfyPage }) => {
// Open templates dialog
await comfyPage.executeCommand('Comfy.BrowseTemplates')
await expect(comfyPage.templates.content).toBeVisible()
// Get a template card
const firstCard = comfyPage.page.locator('.template-card').first()
await expect(firstCard).toBeVisible({ timeout: 5000 })
// Take snapshot before hover
await expect(firstCard).toHaveScreenshot('template-card-before-hover.png')
// Hover over the card
await firstCard.hover()
// Take snapshot after hover to verify hover effect
await expect(firstCard).toHaveScreenshot('template-card-after-hover.png')
})
test('template cards descriptions adjust height dynamically', async ({
comfyPage
}) => {
// Setup test by intercepting templates response to inject cards with varying description lengths
await comfyPage.page.route('**/templates/index.json', async (route, _) => {
const response = [
{
moduleName: 'default',
title: 'Test Templates',
type: 'image',
templates: [
{
name: 'short-description',
title: 'Short Description',
mediaType: 'image',
mediaSubtype: 'webp',
description: 'This is a short description.'
},
{
name: 'medium-description',
title: 'Medium Description',
mediaType: 'image',
mediaSubtype: 'webp',
description:
'This is a medium length description that should take up two lines on most displays.'
},
{
name: 'long-description',
title: 'Long Description',
mediaType: 'image',
mediaSubtype: 'webp',
description:
'This is a much longer description that should definitely wrap to multiple lines. It contains enough text to demonstrate how the cards handle varying amounts of content while maintaining a consistent layout grid.'
}
]
}
]
await route.fulfill({
status: 200,
body: JSON.stringify(response),
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-store'
}
})
})
// Mock the thumbnail images to avoid 404s
await comfyPage.page.route('**/templates/**.webp', async (route) => {
const headers = {
'Content-Type': 'image/webp',
'Cache-Control': 'no-store'
}
await route.fulfill({
status: 200,
path: 'browser_tests/assets/example.webp',
headers
})
})
// Open templates dialog
await comfyPage.executeCommand('Comfy.BrowseTemplates')
await expect(comfyPage.templates.content).toBeVisible()
// Verify cards are visible with varying content lengths
await expect(
comfyPage.page.getByText('This is a short description.')
).toBeVisible({ timeout: 5000 })
await expect(
comfyPage.page.getByText('This is a medium length description')
).toBeVisible({ timeout: 5000 })
await expect(
comfyPage.page.getByText('This is a much longer description')
).toBeVisible({ timeout: 5000 })
// Take snapshot of a grid with specific cards
const templateGrid = comfyPage.templates.content
.locator('.grid:has-text("Short Description")')
.first()
await expect(templateGrid).toBeVisible()
await expect(templateGrid).toHaveScreenshot(
'template-grid-varying-content.png'
)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

View File

@@ -1,84 +1,35 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { PerformanceMonitor } from '../helpers/performanceMonitor'
test.describe('Combo text widget', () => {
test.skip('@perf Truncates text when resized', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'combo-widget-resize-truncation'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation(
'resize-load-checkpoint-node',
async () => {
await comfyPage.resizeLoadCheckpointNode(0.2, 1)
}
)
test('Truncates text when resized', async ({ comfyPage }) => {
await comfyPage.resizeLoadCheckpointNode(0.2, 1)
await expect(comfyPage.canvas).toHaveScreenshot(
'load-checkpoint-resized-min-width.png'
)
await perfMonitor.measureOperation('close-menu', async () => {
await comfyPage.closeMenu()
})
await perfMonitor.measureOperation('resize-ksampler-node', async () => {
await comfyPage.resizeKsamplerNode(0.2, 1)
})
await comfyPage.closeMenu()
await comfyPage.resizeKsamplerNode(0.2, 1)
await expect(comfyPage.canvas).toHaveScreenshot(
`ksampler-resized-min-width.png`
)
await perfMonitor.finishMonitoring(testName)
})
test("@perf Doesn't truncate when space still available", async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'combo-widget-no-truncation'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('resize-empty-latent-node', async () => {
await comfyPage.resizeEmptyLatentNode(0.8, 0.8)
})
test("Doesn't truncate when space still available", async ({ comfyPage }) => {
await comfyPage.resizeEmptyLatentNode(0.8, 0.8)
await expect(comfyPage.canvas).toHaveScreenshot(
'empty-latent-resized-80-percent.png'
)
await perfMonitor.finishMonitoring(testName)
})
test('@perf Can revert to full text', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'combo-widget-revert-text'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('resize-to-original', async () => {
await comfyPage.resizeLoadCheckpointNode(0.8, 1, true)
})
test('Can revert to full text', async ({ comfyPage }) => {
await comfyPage.resizeLoadCheckpointNode(0.8, 1, true)
await expect(comfyPage.canvas).toHaveScreenshot('resized-to-original.png')
await perfMonitor.finishMonitoring(testName)
})
// Skip because fails with vue widget nodes (reason not investigated)
test.skip('@perf should refresh combo values of optional inputs', async ({
test('should refresh combo values of optional inputs', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'combo-widget-refresh-optional-inputs'
await perfMonitor.startMonitoring(testName)
const getComboValues = async () =>
comfyPage.page.evaluate(() => {
return window['app'].graph.nodes
@@ -87,336 +38,151 @@ test.describe('Combo text widget', () => {
.options.values
})
await perfMonitor.measureOperation('load-workflow', async () => {
await comfyPage.loadWorkflow('optional_combo_input')
})
await comfyPage.loadWorkflow('optional_combo_input')
const initialComboValues = await getComboValues()
let initialComboValues: any
await perfMonitor.measureOperation('get-initial-combo-values', async () => {
initialComboValues = await getComboValues()
})
// Focus canvas
await comfyPage.page.mouse.click(400, 300)
await perfMonitor.measureOperation('focus-canvas', async () => {
await comfyPage.page.mouse.click(400, 300)
})
// Press R to trigger refresh
await comfyPage.page.keyboard.press('r')
await perfMonitor.measureOperation('trigger-refresh', async () => {
await comfyPage.page.keyboard.press('r')
})
// Wait for nodes' widgets to be updated
await comfyPage.nextFrame()
await perfMonitor.measureOperation('wait-for-update', async () => {
await comfyPage.nextFrame()
})
let refreshedComboValues: any
await perfMonitor.measureOperation(
'get-refreshed-combo-values',
async () => {
refreshedComboValues = await getComboValues()
}
)
expect(refreshedComboValues).not.toEqual(initialComboValues!)
await perfMonitor.finishMonitoring(testName)
})
test.skip('@perf Should refresh combo values of nodes with v2 combo input spec', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'combo-widget-v2-refresh'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('load-v2-workflow', async () => {
await comfyPage.loadWorkflow('node_with_v2_combo_input')
})
await perfMonitor.measureOperation('focus-canvas', async () => {
await comfyPage.page.mouse.click(400, 300)
})
await perfMonitor.measureOperation('trigger-refresh', async () => {
await comfyPage.page.keyboard.press('r')
})
await perfMonitor.measureOperation('wait-for-update', async () => {
await comfyPage.page.mouse.click(400, 300)
await comfyPage.nextFrame()
})
let comboValues: any
await perfMonitor.measureOperation('get-combo-values', async () => {
comboValues = await comfyPage.page.evaluate(() => {
return window['app'].graph.nodes
.find((node) => node.title === 'Node With V2 Combo Input')
.widgets.find((widget) => widget.name === 'combo_input').options
.values
})
})
expect(comboValues!).toEqual(['A', 'B'])
await perfMonitor.finishMonitoring(testName)
const refreshedComboValues = await getComboValues()
expect(refreshedComboValues).not.toEqual(initialComboValues)
})
})
test.describe('Boolean widget', () => {
test('@perf Can toggle', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'boolean-widget-toggle'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('load-boolean-workflow', async () => {
await comfyPage.loadWorkflow('widgets/boolean_widget')
})
test('Can toggle', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/boolean_widget')
await expect(comfyPage.canvas).toHaveScreenshot('boolean_widget.png')
let node: any
await perfMonitor.measureOperation('get-node-reference', async () => {
node = (await comfyPage.getFirstNodeRef())!
})
let widget: any
await perfMonitor.measureOperation('get-widget-reference', async () => {
widget = await node.getWidget(0)
})
await perfMonitor.measureOperation('toggle-boolean-widget', async () => {
await widget.click()
})
const node = (await comfyPage.getFirstNodeRef())!
const widget = await node.getWidget(0)
await widget.click()
await expect(comfyPage.canvas).toHaveScreenshot(
'boolean_widget_toggled.png'
)
await perfMonitor.finishMonitoring(testName)
})
})
test.describe('Slider widget', () => {
test.skip('@perf Can drag adjust value', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'slider-widget-drag-value'
test('Can drag adjust value', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('simple_slider')
await comfyPage.page.waitForTimeout(300)
const node = (await comfyPage.getFirstNodeRef())!
const widget = await node.getWidget(0)
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('load-slider-workflow', async () => {
await comfyPage.loadWorkflow('simple_slider')
await comfyPage.page.waitForTimeout(300)
await comfyPage.page.evaluate(() => {
const widget = window['app'].graph.nodes[0].widgets[0]
widget.callback = (value: number) => {
window['widgetValue'] = value
}
})
let node: any
await perfMonitor.measureOperation('get-node-reference', async () => {
node = (await comfyPage.getFirstNodeRef())!
})
let widget: any
await perfMonitor.measureOperation('get-widget-reference', async () => {
widget = await node.getWidget(0)
})
await perfMonitor.measureOperation('setup-widget-callback', async () => {
await comfyPage.page.evaluate(() => {
const widget = window['app'].graph.nodes[0].widgets[0]
widget.callback = (value: number) => {
window['widgetValue'] = value
}
})
})
await perfMonitor.measureOperation('drag-slider-widget', async () => {
await widget.dragHorizontal(50)
})
await widget.dragHorizontal(50)
await expect(comfyPage.canvas).toHaveScreenshot('slider_widget_dragged.png')
expect(
await comfyPage.page.evaluate(() => window['widgetValue'])
).toBeDefined()
await perfMonitor.finishMonitoring(testName)
})
})
test.describe('Number widget', () => {
test.skip('@perf Can drag adjust value', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'number-widget-drag-value'
test('Can drag adjust value', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/seed_widget')
await comfyPage.page.waitForTimeout(300)
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('load-seed-workflow', async () => {
await comfyPage.loadWorkflow('widgets/seed_widget')
await comfyPage.page.waitForTimeout(300)
const node = (await comfyPage.getFirstNodeRef())!
const widget = await node.getWidget(0)
await comfyPage.page.evaluate(() => {
const widget = window['app'].graph.nodes[0].widgets[0]
widget.callback = (value: number) => {
window['widgetValue'] = value
}
})
let node: any
await perfMonitor.measureOperation('get-node-reference', async () => {
node = (await comfyPage.getFirstNodeRef())!
})
let widget: any
await perfMonitor.measureOperation('get-widget-reference', async () => {
widget = await node.getWidget(0)
})
await perfMonitor.measureOperation('setup-widget-callback', async () => {
await comfyPage.page.evaluate(() => {
const widget = window['app'].graph.nodes[0].widgets[0]
widget.callback = (value: number) => {
window['widgetValue'] = value
}
})
})
await perfMonitor.measureOperation('drag-number-widget', async () => {
await widget.dragHorizontal(50)
})
await widget.dragHorizontal(50)
await expect(comfyPage.canvas).toHaveScreenshot('seed_widget_dragged.png')
expect(
await comfyPage.page.evaluate(() => window['widgetValue'])
).toBeDefined()
await perfMonitor.finishMonitoring(testName)
})
})
test.describe('Dynamic widget manipulation', () => {
test('@perf Auto expand node when widget is added dynamically', async ({
test('Auto expand node when widget is added dynamically', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'dynamic-widget-addition'
await comfyPage.loadWorkflow('single_ksampler')
await comfyPage.page.waitForTimeout(300)
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('load-ksampler-workflow', async () => {
await comfyPage.loadWorkflow('single_ksampler')
await comfyPage.page.waitForTimeout(300)
})
await perfMonitor.measureOperation('add-dynamic-widget', async () => {
await comfyPage.page.evaluate(() => {
window['graph'].nodes[0].addWidget('number', 'new_widget', 10)
window['graph'].setDirtyCanvas(true, true)
})
await comfyPage.page.evaluate(() => {
window['graph'].nodes[0].addWidget('number', 'new_widget', 10)
window['graph'].setDirtyCanvas(true, true)
})
await expect(comfyPage.canvas).toHaveScreenshot('ksampler_widget_added.png')
await perfMonitor.finishMonitoring(testName)
})
})
test.describe('Image widget', () => {
// Skip because fails with vue widget nodes (reason not investigated)
test.skip('@perf Can load image', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'image-widget-load'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('load-image-workflow', async () => {
await comfyPage.loadWorkflow('widgets/load_image_widget')
})
test('Can load image', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/load_image_widget')
await expect(comfyPage.canvas).toHaveScreenshot('load_image_widget.png')
await perfMonitor.finishMonitoring(testName)
})
test.skip('@perf Can drag and drop image', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'image-widget-drag-drop'
test('Can drag and drop image', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/load_image_widget')
await perfMonitor.startMonitoring(testName)
// Get position of the load image node
const nodes = await comfyPage.getNodeRefsByType('LoadImage')
const loadImageNode = nodes[0]
const { x, y } = await loadImageNode.getPosition()
await perfMonitor.measureOperation('load-image-workflow', async () => {
await comfyPage.loadWorkflow('widgets/load_image_widget')
})
let nodes: any
let loadImageNode: any
let position: any
await perfMonitor.measureOperation('get-load-image-node', async () => {
nodes = await comfyPage.getNodeRefsByType('LoadImage')
loadImageNode = nodes[0]
position = await loadImageNode.getPosition()
})
await perfMonitor.measureOperation('drag-drop-image-file', async () => {
await comfyPage.dragAndDropFile('image32x32.webp', {
dropPosition: { x: position.x, y: position.y }
})
// Drag and drop image file onto the load image node
await comfyPage.dragAndDropFile('image32x32.webp', {
dropPosition: { x, y }
})
// Expect the image preview to change automatically
await expect(comfyPage.canvas).toHaveScreenshot(
'image_preview_drag_and_dropped.png'
)
let fileComboWidget: any
let filename: any
await perfMonitor.measureOperation('get-updated-filename', async () => {
fileComboWidget = await loadImageNode.getWidget(0)
filename = await fileComboWidget.getValue()
})
expect(filename!).toBe('image32x32.webp')
await perfMonitor.finishMonitoring(testName)
// Expect the filename combo value to be updated
const fileComboWidget = await loadImageNode.getWidget(0)
const filename = await fileComboWidget.getValue()
expect(filename).toBe('image32x32.webp')
})
// Skip because fails with vue widget nodes (reason not investigated)
test.skip('@perf Can change image by changing the filename combo value', async ({
test('Can change image by changing the filename combo value', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'image-widget-combo-change'
await comfyPage.loadWorkflow('widgets/load_image_widget')
const nodes = await comfyPage.getNodeRefsByType('LoadImage')
const loadImageNode = nodes[0]
await perfMonitor.startMonitoring(testName)
// Click the combo widget used to select the image filename
const fileComboWidget = await loadImageNode.getWidget(0)
await fileComboWidget.click()
await perfMonitor.measureOperation('load-image-workflow', async () => {
await comfyPage.loadWorkflow('widgets/load_image_widget')
})
let nodes: any
let loadImageNode: any
await perfMonitor.measureOperation('get-load-image-node', async () => {
nodes = await comfyPage.getNodeRefsByType('LoadImage')
loadImageNode = nodes[0]
})
let fileComboWidget: any
await perfMonitor.measureOperation('click-combo-widget', async () => {
fileComboWidget = await loadImageNode.getWidget(0)
await fileComboWidget.click()
})
await perfMonitor.measureOperation('select-combo-entry', async () => {
const comboEntry = comfyPage.page.getByRole('menuitem', {
name: 'image32x32.webp'
})
await comboEntry.click({ noWaitAfter: true })
// Select a new image filename value from the combo context menu
const comboEntry = comfyPage.page.getByRole('menuitem', {
name: 'image32x32.webp'
})
await comboEntry.click({ noWaitAfter: true })
// Expect the image preview to change automatically
await expect(comfyPage.canvas).toHaveScreenshot(
'image_preview_changed_by_combo_value.png'
)
let filename: any
await perfMonitor.measureOperation('get-updated-filename', async () => {
filename = await fileComboWidget.getValue()
})
expect(filename!).toBe('image32x32.webp')
await perfMonitor.finishMonitoring(testName)
// Expect the filename combo value to be updated
const filename = await fileComboWidget.getValue()
expect(filename).toBe('image32x32.webp')
})
})
@@ -456,165 +222,91 @@ test.describe('Animated image widget', () => {
)
})
test.skip('@perf Can drag-and-drop animated webp image', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'animated-image-widget-drag-drop'
test('Can drag-and-drop animated webp image', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/load_animated_webp')
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation(
'load-animated-webp-workflow',
async () => {
await comfyPage.loadWorkflow('widgets/load_animated_webp')
}
// Get position of the load animated webp node
const nodes = await comfyPage.getNodeRefsByType(
'DevToolsLoadAnimatedImageTest'
)
const loadAnimatedWebpNode = nodes[0]
const { x, y } = await loadAnimatedWebpNode.getPosition()
let nodes: any
let loadAnimatedWebpNode: any
let position: any
await perfMonitor.measureOperation('get-animated-webp-node', async () => {
nodes = await comfyPage.getNodeRefsByType('DevToolsLoadAnimatedImageTest')
loadAnimatedWebpNode = nodes[0]
position = await loadAnimatedWebpNode.getPosition()
// Drag and drop image file onto the load animated webp node
await comfyPage.dragAndDropFile('animated_webp.webp', {
dropPosition: { x, y }
})
await perfMonitor.measureOperation('drag-drop-animated-webp', async () => {
await comfyPage.dragAndDropFile('animated_webp.webp', {
dropPosition: { x: position.x, y: position.y }
})
})
let fileComboWidget: any
let filename: any
await perfMonitor.measureOperation('get-updated-filename', async () => {
fileComboWidget = await loadAnimatedWebpNode.getWidget(0)
filename = await fileComboWidget.getValue()
})
expect(filename!).toContain('animated_webp.webp')
await perfMonitor.finishMonitoring(testName)
// Expect the filename combo value to be updated
const fileComboWidget = await loadAnimatedWebpNode.getWidget(0)
const filename = await fileComboWidget.getValue()
expect(filename).toContain('animated_webp.webp')
})
test('@perf Can preview saved animated webp image', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'animated-image-widget-save-preview'
test('Can preview saved animated webp image', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/save_animated_webp')
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation(
'load-save-animated-workflow',
async () => {
await comfyPage.loadWorkflow('widgets/save_animated_webp')
}
// Get position of the load animated webp node
const loadNodes = await comfyPage.getNodeRefsByType(
'DevToolsLoadAnimatedImageTest'
)
const loadAnimatedWebpNode = loadNodes[0]
const { x, y } = await loadAnimatedWebpNode.getPosition()
let loadNodes: any
let loadAnimatedWebpNode: any
let position: any
await perfMonitor.measureOperation('get-load-node', async () => {
loadNodes = await comfyPage.getNodeRefsByType(
'DevToolsLoadAnimatedImageTest'
)
loadAnimatedWebpNode = loadNodes[0]
position = await loadAnimatedWebpNode.getPosition()
// Drag and drop image file onto the load animated webp node
await comfyPage.dragAndDropFile('animated_webp.webp', {
dropPosition: { x, y }
})
await comfyPage.nextFrame()
await perfMonitor.measureOperation('drag-drop-animated-file', async () => {
await comfyPage.dragAndDropFile('animated_webp.webp', {
dropPosition: { x: position.x, y: position.y }
})
await comfyPage.nextFrame()
})
// Get the SaveAnimatedWEBP node
const saveNodes = await comfyPage.getNodeRefsByType('SaveAnimatedWEBP')
const saveAnimatedWebpNode = saveNodes[0]
if (!saveAnimatedWebpNode)
throw new Error('SaveAnimatedWEBP node not found')
let saveNodes: any
let saveAnimatedWebpNode: any
await perfMonitor.measureOperation('get-save-node', async () => {
saveNodes = await comfyPage.getNodeRefsByType('SaveAnimatedWEBP')
saveAnimatedWebpNode = saveNodes[0]
if (!saveAnimatedWebpNode)
throw new Error('SaveAnimatedWEBP node not found')
})
// Simulate the graph executing
await comfyPage.page.evaluate(
([loadId, saveId]) => {
// Set the output of the SaveAnimatedWEBP node to equal the loader node's image
window['app'].nodeOutputs[saveId] = window['app'].nodeOutputs[loadId]
},
[loadAnimatedWebpNode.id, saveAnimatedWebpNode.id]
)
await comfyPage.nextFrame()
await perfMonitor.measureOperation('simulate-graph-execution', async () => {
await comfyPage.page.evaluate(
([loadId, saveId]) => {
// Set the output of the SaveAnimatedWEBP node to equal the loader node's image
window['app'].nodeOutputs[saveId] = window['app'].nodeOutputs[loadId]
},
[loadAnimatedWebpNode.id, saveAnimatedWebpNode.id]
)
await comfyPage.nextFrame()
})
// Wait for animation to go to next frame
await comfyPage.page.waitForTimeout(512)
await perfMonitor.measureOperation('wait-for-animation-frame', async () => {
await comfyPage.page.waitForTimeout(512)
})
await perfMonitor.measureOperation('trigger-render', async () => {
await comfyPage.page.mouse.click(64, 64)
})
// Move mouse and click on canvas to trigger render
await comfyPage.page.mouse.click(64, 64)
// Expect the SaveAnimatedWEBP node to have an output preview
await expect(comfyPage.canvas).toHaveScreenshot(
'animated_image_preview_saved_webp.png'
)
await perfMonitor.finishMonitoring(testName)
})
})
test.describe('Load audio widget', () => {
// Skip because fails with vue widget nodes (reason not investigated)
test.skip('@perf Can load audio', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'audio-widget-load'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('load-audio-workflow', async () => {
await comfyPage.loadWorkflow('widgets/load_audio_widget')
})
test('Can load audio', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/load_audio_widget')
await expect(comfyPage.canvas).toHaveScreenshot('load_audio_widget.png')
await perfMonitor.finishMonitoring(testName)
})
})
test.describe('Unserialized widgets', () => {
test.skip('@perf Unserialized widgets values do not mark graph as modified', async ({
test('Unserialized widgets values do not mark graph as modified', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'unserialized-widget-modification-check'
// Add workflow w/ LoadImage node, which contains file upload and image preview widgets (not serialized)
await comfyPage.loadWorkflow('widgets/load_image_widget')
await perfMonitor.startMonitoring(testName)
// Move mouse and click to trigger the `graphEqual` check in `changeTracker.ts`
await comfyPage.page.mouse.move(10, 10)
await comfyPage.page.mouse.click(10, 10)
await perfMonitor.measureOperation(
'load-image-widget-workflow',
async () => {
await comfyPage.loadWorkflow('widgets/load_image_widget')
}
)
await perfMonitor.measureOperation(
'trigger-graph-equal-check',
async () => {
await comfyPage.page.mouse.move(10, 10)
await comfyPage.page.mouse.click(10, 10)
}
)
let isModified: any
await perfMonitor.measureOperation(
'check-workflow-modified-status',
async () => {
isModified = await comfyPage.isCurrentWorkflowModified()
}
)
expect(isModified!).toBe(false)
await perfMonitor.finishMonitoring(testName)
// Expect the graph to not be modified
expect(await comfyPage.isCurrentWorkflowModified()).toBe(false)
})
})

View File

@@ -1,89 +0,0 @@
#!/bin/bash
# Script to copy vue-widget-conversion folder and .claude/commands/create-widget.md
# to another local copy of the same repository
# Check if destination directory was provided
if [ $# -eq 0 ]; then
echo "Usage: $0 <destination-repo-path>"
echo "Example: $0 /home/c_byrne/projects/comfyui-frontend-testing/ComfyUI_frontend-clone-8"
exit 1
fi
# Get the destination directory from first argument
DEST_DIR="$1"
# Source files/directories (relative to script location)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SOURCE_WIDGET_DIR="$SCRIPT_DIR/vue-widget-conversion"
SOURCE_COMMAND_FILE="$SCRIPT_DIR/.claude/commands/create-widget.md"
# Destination paths
DEST_WIDGET_DIR="$DEST_DIR/vue-widget-conversion"
DEST_COMMAND_DIR="$DEST_DIR/.claude/commands"
DEST_COMMAND_FILE="$DEST_COMMAND_DIR/create-widget.md"
# Check if destination directory exists
if [ ! -d "$DEST_DIR" ]; then
echo "Error: Destination directory does not exist: $DEST_DIR"
exit 1
fi
# Check if source vue-widget-conversion directory exists
if [ ! -d "$SOURCE_WIDGET_DIR" ]; then
echo "Error: Source vue-widget-conversion directory not found: $SOURCE_WIDGET_DIR"
exit 1
fi
# Check if source command file exists
if [ ! -f "$SOURCE_COMMAND_FILE" ]; then
echo "Error: Source command file not found: $SOURCE_COMMAND_FILE"
exit 1
fi
echo "Copying widget resources to: $DEST_DIR"
# Copy vue-widget-conversion directory
echo "Copying vue-widget-conversion directory..."
if [ -d "$DEST_WIDGET_DIR" ]; then
echo " Warning: Destination vue-widget-conversion already exists. Overwriting..."
rm -rf "$DEST_WIDGET_DIR"
fi
cp -r "$SOURCE_WIDGET_DIR" "$DEST_WIDGET_DIR"
echo " ✓ Copied vue-widget-conversion directory"
# Create .claude/commands directory if it doesn't exist
echo "Creating .claude/commands directory structure..."
mkdir -p "$DEST_COMMAND_DIR"
echo " ✓ Created .claude/commands directory"
# Copy create-widget.md command
echo "Copying create-widget.md command..."
cp "$SOURCE_COMMAND_FILE" "$DEST_COMMAND_FILE"
echo " ✓ Copied create-widget.md command"
# Verify the copy was successful
echo ""
echo "Verification:"
if [ -d "$DEST_WIDGET_DIR" ] && [ -f "$DEST_WIDGET_DIR/vue-widget-guide.md" ] && [ -f "$DEST_WIDGET_DIR/primevue-components.md" ]; then
echo " ✓ vue-widget-conversion directory copied successfully"
echo " - vue-widget-guide.md exists"
echo " - primevue-components.md exists"
if [ -f "$DEST_WIDGET_DIR/primevue-components.json" ]; then
echo " - primevue-components.json exists"
fi
else
echo " ✗ Error: vue-widget-conversion directory copy may have failed"
fi
if [ -f "$DEST_COMMAND_FILE" ]; then
echo " ✓ create-widget.md command copied successfully"
else
echo " ✗ Error: create-widget.md command copy may have failed"
fi
echo ""
echo "Copy complete! Widget resources are now available in: $DEST_DIR"
echo ""
echo "You can now use the widget creation command in the destination repo:"
echo " /project:create-widget <widget specification>"

View File

@@ -24,7 +24,7 @@ export default [
},
parser: tseslint.parser,
parserOptions: {
project: ['./tsconfig.json', './tsconfig.eslint.json'],
project: './tsconfig.json',
ecmaVersion: 2020,
sourceType: 'module',
extraFileExtensions: ['.vue']

View File

@@ -4,18 +4,9 @@
<meta charset="UTF-8">
<title>ComfyUI</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<link rel="stylesheet" type="text/css" href="materialdesignicons.min.css" />
<link rel="stylesheet" type="text/css" href="user.css" />
<link rel="stylesheet" type="text/css" href="api/userdata/user.css" />
<!-- Fullscreen mode on iOS -->
<meta name="apple-mobile-web-app-capable" content="yes">
<!-- Status bar style (eg. black or transparent) -->
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<link rel="manifest" href="manifest.json">
<link rel="stylesheet" type="text/css" href="materialdesignicons.min.css" />
</head>
<body class="litegraph grid">
<div id="vue-app"></div>
<script type="module" src="src/main.ts"></script>

View File

@@ -1,9 +0,0 @@
{
"name": "ComfyUI",
"short_name": "ComfyUI",
"description": "ComfyUI: AI image generation platform",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#000000"
}

3604
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.22.1",
"version": "1.19.4",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -29,13 +29,11 @@
},
"devDependencies": {
"@eslint/js": "^9.8.0",
"@executeautomation/playwright-mcp-server": "^1.0.5",
"@iconify/json": "^2.2.245",
"@lobehub/i18n-cli": "^1.20.0",
"@pinia/testing": "^0.1.5",
"@playwright/test": "^1.52.0",
"@playwright/test": "^1.44.1",
"@trivago/prettier-plugin-sort-imports": "^5.2.0",
"@types/dompurify": "^3.0.5",
"@types/fs-extra": "^11.0.4",
"@types/lodash": "^4.17.6",
"@types/node": "^20.14.8",
@@ -65,8 +63,6 @@
"unplugin-vue-components": "^0.27.4",
"vite": "^5.4.19",
"vite-plugin-dts": "^4.3.0",
"vite-plugin-html": "^3.2.2",
"vite-plugin-vue-devtools": "^7.7.6",
"vitest": "^2.0.0",
"vue-tsc": "^2.1.10",
"zip-dir": "^2.0.0",
@@ -76,7 +72,7 @@
"@alloc/quick-lru": "^5.2.0",
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "^0.4.43",
"@comfyorg/litegraph": "^0.15.15",
"@comfyorg/litegraph": "^0.15.3",
"@primevue/forms": "^4.2.5",
"@primevue/themes": "^4.2.5",
"@sentry/vue": "^8.48.0",
@@ -93,14 +89,12 @@
"@xterm/xterm": "^5.5.0",
"algoliasearch": "^5.21.0",
"axios": "^1.8.2",
"dompurify": "^3.2.5",
"dotenv": "^16.4.5",
"firebase": "^11.6.0",
"fuse.js": "^7.0.0",
"jsondiffpatch": "^0.6.0",
"lodash": "^4.17.21",
"loglevel": "^1.9.2",
"marked": "^15.0.11",
"pinia": "^2.1.7",
"primeicons": "^7.0.0",
"primevue": "^4.2.5",

View File

@@ -1,12 +0,0 @@
# Run performance tests with more detailed output
npx playwright test --workers 1 --project=performance --reporter=line --ignore-snapshots --ui
# Run performance tests on specific files
#npx playwright test --workers 1 --project=performance interaction.spec.ts
# Run performance tests with trace for debugging
#npx playwright test --workers 1 --project=performance --trace=on
# Run performance tests and update any snapshots
#npx playwright test --workers 1 --project=performance --update-snapshots

View File

@@ -1,12 +0,0 @@
# Run performance tests with more detailed output
npx playwright test --workers 1 --project=performance --reporter=line --ignore-snapshots
# Run performance tests on specific files
#npx playwright test --workers 1 --project=performance interaction.spec.ts
# Run performance tests with trace for debugging
#npx playwright test --workers 1 --project=performance --trace=on
# Run performance tests and update any snapshots
#npx playwright test --workers 1 --project=performance --update-snapshots

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