diff --git a/.env_example b/.env_example index 07eda86be..30f18e874 100644 --- a/.env_example +++ b/.env_example @@ -18,3 +18,14 @@ TEST_COMFYUI_DIR=/home/ComfyUI # Whether to enable minification of the frontend code. ENABLE_MINIFY=true + +# Whether to disable proxying the `/templates` route. If true, allows you to +# serve templates from the ComfyUI_frontend/public/templates folder (for +# locally testing changes to templates). When false or nonexistent, the +# 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 diff --git a/.github/workflows/dev-release.yaml b/.github/workflows/dev-release.yaml new file mode 100644 index 000000000..3e0ff9ffc --- /dev/null +++ b/.github/workflows/dev-release.yaml @@ -0,0 +1,72 @@ +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@release/v1 + with: + password: ${{ secrets.PYPI_TOKEN }} + packages-dir: comfyui_frontend_package/dist diff --git a/.github/workflows/test-ui.yaml b/.github/workflows/test-ui.yaml index 538eb13c9..79e4af102 100644 --- a/.github/workflows/test-ui.yaml +++ b/.github/workflows/test-ui.yaml @@ -30,7 +30,7 @@ jobs: with: repository: 'Comfy-Org/ComfyUI_devtools' path: 'ComfyUI/custom_nodes/ComfyUI_devtools' - ref: '9d2421fd3208a310e4d0f71fca2ea0c985759c33' + ref: 'd05fd48dd787a4192e16802d4244cfcc0e2f9684' - uses: actions/setup-node@v4 with: diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..707ca1a5e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,38 @@ +- 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 speicifc 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 refrence 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. diff --git a/README.md b/README.md index 3763f29ee..02377c293 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,449 @@ The development of successive minor versions overlaps. For example, while versio | 3 | Mar 15-21 | Released | Feature Freeze | Development | 1.1.7 through 1.1.13 (daily)
1.2.0 through 1.2.6 (daily) | | 4 | Mar 22-28 | - | Released | Feature Freeze | 1.2.7 through 1.2.13 (daily)
1.3.0 through 1.3.6 (daily) | +## Release Summary + +### Major features + +
+ v1.5: Native translation (i18n) + + ComfyUI now includes built-in translation support, replacing the need for third-party translation extensions. Select your language + in `Comfy > Locale > Language` to translate the interface into English, Chinese (Simplified), Russian, Japanese, or Korean. This native + implementation offers better performance, reliability, and maintainability compared to previous solutions.
+ + More details available here: https://blog.comfy.org/p/native-localization-support-i18n +
+ +
+ v1.4: New mask editor + + https://github.com/Comfy-Org/ComfyUI_frontend/pull/1284 implements a new mask editor. + + ![image](https://github.com/user-attachments/assets/f0ea6ee5-00ee-4e5d-a09c-6938e86a1f17) +
+ +
+ v1.3.22: Integrated server terminal + +Press Ctrl + ` to toggle integrated terminal. + +https://github.com/user-attachments/assets/eddedc6a-07a3-4a83-9475-63b3977f6d94 +
+ +
+ v1.3.7: Keybinding customization + +## Basic UI +![image](https://github.com/user-attachments/assets/c84a1609-3880-48e0-a746-011f36beda68) + +## Reset button +![image](https://github.com/user-attachments/assets/4d2922da-bb4f-4f90-8017-a8e4a0db07c7) + +## Edit Keybinding +![image](https://github.com/user-attachments/assets/77626b7a-cb46-48f8-9465-e03120aac66a) +![image](https://github.com/user-attachments/assets/79131a4e-75c6-4715-bd11-c6aaed887779) + +[rec.webm](https://github.com/user-attachments/assets/a3984ed9-eb28-4d47-86c0-7fc3efc2b5d0) + +
+ +
+ v1.2.4: Node library sidebar tab + + #### Drag & Drop +https://github.com/user-attachments/assets/853e20b7-bc0e-49c9-bbce-a2ba7566f92f + + #### Filter +https://github.com/user-attachments/assets/4bbca3ee-318f-4cf0-be32-a5a5541066cf +
+ +
+ v1.2.0: Queue/History sidebar tab + + https://github.com/user-attachments/assets/86e264fe-4d26-4f07-aa9a-83bdd2d02b8f +
+ + + +### QoL changes + +
+ v1.3.32: **Litegraph** Nested group + +https://github.com/user-attachments/assets/f51adeb1-028e-40af-81e4-0ac13075198a +
+ +
+ v1.3.24: **Litegraph** Group selection + +https://github.com/user-attachments/assets/e6230a94-411e-4fba-90cb-6c694200adaa +
+ + + +
+ v1.3.4: **Litegraph** Auto widget to input conversion + +Dropping a link of correct type on node widget will automatically convert the widget to input. + +[rec.webm](https://github.com/user-attachments/assets/15cea0b0-b225-4bec-af50-2cdb16dc46bf) + +
+ +
+ v1.3.4: **Litegraph** Canvas pan mode + +The canvas becomes readonly in pan mode. Pan mode is activated by clicking the pan mode button on the canvas menu +or by holding the space key. + +[rec.webm](https://github.com/user-attachments/assets/c7872532-a2ac-44c1-9e7d-9e03b5d1a80b) + +
+ + + +
+ v1.2.62: **Litegraph** Show optional input slots as donuts + +![GYEIRidb0AYGO-v](https://github.com/user-attachments/assets/e6cde0b6-654b-4afd-a117-133657a410b1) + +
+ +
+ v1.2.44: **Litegraph** Double click group title to edit + +https://github.com/user-attachments/assets/5bf0e2b6-8b3a-40a7-b44f-f0879e9ad26f + +
+ +
+ v1.2.39: **Litegraph** Group selected nodes with Ctrl + G + +https://github.com/user-attachments/assets/7805dc54-0854-4a28-8bcd-4b007fa01151 + +
+ +
+ v1.2.38: **Litegraph** Double click node title to edit + +https://github.com/user-attachments/assets/d61d5d0e-f200-4153-b293-3e3f6a212b30 + +
+ + + + + +
+ v1.1.8: **Litegraph** hides text overflow on widget value + + https://github.com/user-attachments/assets/5696a89d-4a47-4fcc-9e8c-71e1264943f2 +
+ +### Developer APIs + +
+ v1.6.13: prompt/confirm/alert replacements for ComfyUI desktop + +Several browser-only APIs are not available in ComfyUI desktop's electron environment. + +- `window.prompt` +- `window.confirm` +- `window.alert` + +Please use the following APIs as replacements. + +```js +// window.prompt +window['app'].extensionManager.dialog + .prompt({ + title: 'Test Prompt', + message: 'Test Prompt Message' + }) + .then((value: string) => { + // Do something with the value user entered + }) +``` + +![image](https://github.com/user-attachments/assets/c73f74d0-9bb4-4555-8d56-83f1be4a1d7e) + +```js +// window.confirm +window['app'].extensionManager.dialog + .confirm({ + title: 'Test Confirm', + message: 'Test Confirm Message' + }) + .then((value: boolean) => { + // Do something with the value user entered + }) +``` + +![image](https://github.com/user-attachments/assets/8dec7a42-7443-4245-85be-ceefb1116e96) + +```js +// window.alert +window['app'].extensionManager.toast + .addAlert("Test Alert") +``` + +![image](https://github.com/user-attachments/assets/9b18bdca-76ef-4432-95de-5cd2369684f2) + +
+ +
+ v1.3.34: Register about panel badges + +```js +app.registerExtension({ + name: 'TestExtension1', + aboutPageBadges: [ + { + label: 'Test Badge', + url: 'https://example.com', + icon: 'pi pi-box' + } + ] +}) +``` + +![image](https://github.com/user-attachments/assets/099e77ee-16ad-4141-b2fc-5e9d5075188b) + +
+ +
+ v1.3.22: Register bottom panel tabs + +```js +app.registerExtension({ + name: 'TestExtension', + bottomPanelTabs: [ + { + id: 'TestTab', + title: 'Test Tab', + type: 'custom', + render: (el) => { + el.innerHTML = '
Custom tab
' + } + } + ] +}) +``` + +![image](https://github.com/user-attachments/assets/2114f8b8-2f55-414b-b027-78e61c870b64) + +
+ +
+ v1.3.22: New settings API + +Legacy settings API. + +```js +// Register a new setting +app.ui.settings.addSetting({ + id: 'TestSetting', + name: 'Test Setting', + type: 'text', + defaultValue: 'Hello, world!' +}) + +// Get the value of a setting +const value = app.ui.settings.getSettingValue('TestSetting') + +// Set the value of a setting +app.ui.settings.setSettingValue('TestSetting', 'Hello, universe!') +``` + +New settings API. + +```js +// Register a new setting +app.registerExtension({ + name: 'TestExtension1', + settings: [ + { + id: 'TestSetting', + name: 'Test Setting', + type: 'text', + defaultValue: 'Hello, world!' + } + ] +}) + +// Get the value of a setting +const value = app.extensionManager.setting.get('TestSetting') + +// Set the value of a setting +app.extensionManager.setting.set('TestSetting', 'Hello, universe!') +``` + +
+ +
+ v1.3.7: Register commands and keybindings + + Extensions can call the following API to register commands and keybindings. Do + note that keybindings defined in core cannot be overwritten, and some keybindings + are reserved by the browser. + +```js + app.registerExtension({ + name: 'TestExtension1', + commands: [ + { + id: 'TestCommand', + function: () => { + alert('TestCommand') + } + } + ], + keybindings: [ + { + combo: { key: 'k' }, + commandId: 'TestCommand' + } + ] + }) +``` + +
+ +
+ v1.3.1: Extension API to register custom topbar menu items + + Extensions can call the following API to register custom topbar menu items. + +```js + app.registerExtension({ + name: 'TestExtension1', + commands: [ + { + id: 'foo-id', + label: 'foo', + function: () => { + alert(1) + } + } + ], + menuCommands: [ + { + path: ['ext', 'ext2'], + commands: ['foo-id'] + } + ] + }) +``` + +![image](https://github.com/user-attachments/assets/ae7b082f-7ce9-4549-a446-4563567102fe) +
+ +
+ v1.2.27: Extension API to add toast messagei + + Extensions can call the following API to add toast messages. + +```js + app.extensionManager.toast.add({ + severity: 'info', + summary: 'Loaded!', + detail: 'Extension loaded!', + life: 3000 + }) +``` +Documentation of all supported options can be found here: + +![image](https://github.com/user-attachments/assets/de02cd7e-cd81-43d1-a0b0-bccef92ff487) +
+ +
+ v1.2.4: Extension API to register custom sidebar tab + + Extensions now can call the following API to register a sidebar tab. + +```js + app.extensionManager.registerSidebarTab({ + id: "search", + icon: "pi pi-search", + title: "search", + tooltip: "search", + type: "custom", + render: (el) => { + el.innerHTML = "
Custom search tab
"; + }, + }); +``` + +The list of supported icons can be found here: + +We will support custom icons later. + +![image](https://github.com/user-attachments/assets/7bff028a-bf91-4cab-bf97-55c243b3f5e0) +
+ +
+ v1.10.9: Selection Toolbox API + +Extensions can register commands that appear in the selection toolbox when specific items are selected on the canvas. + +```js +app.registerExtension({ + name: 'TestExtension1', + commands: [ + { + id: 'test.selection.command', + label: 'Test Command', + icon: 'pi pi-star', + function: () => { + // Command logic here + } + } + ], + // Return an array of command IDs to show in the selection toolbox + // when an item is selected + getSelectionToolboxCommands: (selectedItem) => ['test.selection.command'] +}) +``` + +The selection toolbox will display the command button when items are selected: +![Image](https://github.com/user-attachments/assets/28d91267-c0a9-4bd5-a7c4-36e8ec44c9bd) + +
+ ## Contributing We're building this frontend together and would love your help — no matter how you'd like to pitch in! You don't need to write code to make a difference. @@ -83,14 +526,46 @@ Have another idea? Drop into Discord or open an issue, and let's chat! ## Development -### Tech Stack +### Prerequisites & Technology 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 +- **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 +``` ### Git pre-commit hooks @@ -132,7 +607,7 @@ navigate to `http://: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. +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. ### Unit Test @@ -163,3 +638,103 @@ 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 + +> **Note**: For comprehensive troubleshooting and how-to guides, please refer to our [official documentation](https://docs.comfy.org/). This section covers only the most common issues related to frontend development. + +> **Desktop Users**: For issues specific to the desktop application, please refer to the [ComfyUI desktop repository](https://github.com/Comfy-Org/desktop). + +### Debugging Custom Node (Extension) Issues + +If you're experiencing crashes, errors, or unexpected behavior with ComfyUI, it's often caused by custom nodes (extensions). Follow these steps to identify and resolve the issues: + +#### Step 1: Verify if custom nodes are causing the problem + +Run ComfyUI with the `--disable-all-custom-nodes` flag: + +```bash +python main.py --disable-all-custom-nodes +``` + +If the issue disappears, a custom node is the culprit. Proceed to the next step. + +#### Step 2: Identify the problematic custom node using binary search + +Rather than disabling nodes one by one, use this more efficient approach: + +1. Temporarily move half of your custom nodes out of the `custom_nodes` directory + ```bash + # Create a temporary directory + # Linux/Mac + mkdir ~/custom_nodes_disabled + + # Windows + mkdir %USERPROFILE%\custom_nodes_disabled + + # Move half of your custom nodes (assuming you have node1 through node8) + # Linux/Mac + mv custom_nodes/node1 custom_nodes/node2 custom_nodes/node3 custom_nodes/node4 ~/custom_nodes_disabled/ + + # Windows + move custom_nodes\node1 custom_nodes\node2 custom_nodes\node3 custom_nodes\node4 %USERPROFILE%\custom_nodes_disabled\ + ``` + +2. Run ComfyUI again + - If the issue persists: The problem is in nodes 5-8 (the remaining half) + - If the issue disappears: The problem is in nodes 1-4 (the moved half) + +3. Let's assume the issue disappeared, so the problem is in nodes 1-4. Move half of these for the next test: + ```bash + # Move nodes 3-4 back to custom_nodes + # Linux/Mac + mv ~/custom_nodes_disabled/node3 ~/custom_nodes_disabled/node4 custom_nodes/ + + # Windows + move %USERPROFILE%\custom_nodes_disabled\node3 %USERPROFILE%\custom_nodes_disabled\node4 custom_nodes\ + ``` + +4. Run ComfyUI again + - If the issue reappears: The problem is in nodes 3-4 + - If issue still gone: The problem is in nodes 1-2 + +5. Let's assume the issue reappeared, so the problem is in nodes 3-4. Test each one: + ```bash + # Move node 3 back to disabled + # Linux/Mac + mv custom_nodes/node3 ~/custom_nodes_disabled/ + + # Windows + move custom_nodes\node3 %USERPROFILE%\custom_nodes_disabled\ + ``` + +6. Run ComfyUI again + - If the issue disappears: node3 is the problem + - If issue persists: node4 is the problem + +7. Repeat until you identify the specific problematic node + +#### Step 3: Update or replace the problematic node + +Once identified: +1. Check for updates to the problematic custom node +2. Consider alternatives with similar functionality +3. Report the issue to the custom node developer with specific details + +### Common Issues and Solutions + +- **"Module not found" errors**: Usually indicates missing Python dependencies. Check the custom node's `requirements.txt` file for required packages and install them: + ```bash + pip install -r custom_nodes/problematic_node/requirements.txt + ``` + +- **Frontend or Templates Package Not Updated**: After updating ComfyUI via Git, ensure you update the frontend dependencies: + ```bash + pip install -r requirements.txt + ``` + +- **Can't Find Custom Node**: Make sure to disable node validation in ComfyUI settings. + +- **Error Toast About Workflow Failing Validation**: Report the issue to the ComfyUI team. As a temporary workaround, disable workflow validation in settings. + +- **Login Issues When Not on Localhost**: Normal login is only available when accessing from localhost. If you're running ComfyUI via LAN, another domain, or headless, you can use our API key feature to authenticate. The API key lets you log in normally through the UI. Generate an API key at [platform.comfy.org/login](https://platform.comfy.org/login) and use it in the API Key field in the login dialog or with the `--api-key` command line argument. Refer to our [API Key Integration Guide](https://docs.comfy.org/essentials/comfyui-server/api-key-integration#integration-of-api-key-to-use-comfyui-api-nodes) for complete setup instructions. \ No newline at end of file diff --git a/browser_tests/README.md b/browser_tests/README.md index 307a4e527..0860f8c80 100644 --- a/browser_tests/README.md +++ b/browser_tests/README.md @@ -1,6 +1,6 @@ # Playwright Testing for ComfyUI_frontend -This document outlines the setup and usage of Playwright for testing the ComfyUI_frontend project. +This document outlines the setup, usage, and common patterns for Playwright browser tests in 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 two ways to run the tests: +There are multiple ways to run the tests: 1. **Headless mode with report generation:** ```bash @@ -47,14 +47,239 @@ There are two 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: -- 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. +- **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 -> **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. +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. \ No newline at end of file diff --git a/browser_tests/assets/collapsed_multiline.json b/browser_tests/assets/collapsed_multiline.json index d2977074d..2999e994a 100644 --- a/browser_tests/assets/collapsed_multiline.json +++ b/browser_tests/assets/collapsed_multiline.json @@ -33,5 +33,11 @@ "links": [], "groups": [], "config": {}, + "extra": { + "ds": { + "offset": [0, 0], + "scale": 1 + } + }, "version": 0.4 } diff --git a/browser_tests/assets/default.json b/browser_tests/assets/default.json index dfb7078a6..dd083e568 100644 --- a/browser_tests/assets/default.json +++ b/browser_tests/assets/default.json @@ -130,6 +130,11 @@ ], "groups": [], "config": {}, - "extra": {}, + "extra": { + "ds": { + "offset": [0, 0], + "scale": 1 + } + }, "version": 0.4 } diff --git a/browser_tests/assets/default_input.json b/browser_tests/assets/default_input.json index eff7b3236..a9291ab82 100644 --- a/browser_tests/assets/default_input.json +++ b/browser_tests/assets/default_input.json @@ -42,12 +42,12 @@ "config": {}, "extra": { "ds": { - "scale": 2.1600300525920346, + "scale": 1, "offset": [ - 63.071794466403446, - 75.18055335968394 + 0, + 0 ] } }, "version": 0.4 -} \ No newline at end of file +} diff --git a/browser_tests/assets/every_node_color.json b/browser_tests/assets/every_node_color.json index 79c067382..b64da88bd 100644 --- a/browser_tests/assets/every_node_color.json +++ b/browser_tests/assets/every_node_color.json @@ -499,6 +499,11 @@ ], "groups": [], "config": {}, - "extra": {}, + "extra": { + "ds": { + "offset": [0, 0], + "scale": 1 + } + }, "version": 0.4 -} \ No newline at end of file +} diff --git a/browser_tests/assets/execution/partial_execution.json b/browser_tests/assets/execution/partial_execution.json new file mode 100644 index 000000000..4669b44fd --- /dev/null +++ b/browser_tests/assets/execution/partial_execution.json @@ -0,0 +1,85 @@ +{ + "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 +} diff --git a/browser_tests/assets/group_node_identical_nodes_hidden_inputs.json b/browser_tests/assets/group_node_identical_nodes_hidden_inputs.json index db6b85c69..e423546f3 100644 --- a/browser_tests/assets/group_node_identical_nodes_hidden_inputs.json +++ b/browser_tests/assets/group_node_identical_nodes_hidden_inputs.json @@ -160,4 +160,4 @@ } }, "version": 0.4 -} \ No newline at end of file +} diff --git a/browser_tests/assets/group_node_v1.3.3.json b/browser_tests/assets/group_node_v1.3.3.json index 7afd9d7c8..1c04ec8bc 100644 --- a/browser_tests/assets/group_node_v1.3.3.json +++ b/browser_tests/assets/group_node_v1.3.3.json @@ -43,6 +43,10 @@ "groups": [], "config": {}, "extra": { + "ds": { + "offset": [0, 0], + "scale": 1 + }, "groupNodes": { "group_node": { "nodes": [ @@ -401,4 +405,4 @@ } }, "version": 0.4 -} \ No newline at end of file +} diff --git a/browser_tests/assets/legacy_group_node.json b/browser_tests/assets/legacy_group_node.json index e64f2f785..0e883872e 100644 --- a/browser_tests/assets/legacy_group_node.json +++ b/browser_tests/assets/legacy_group_node.json @@ -110,6 +110,10 @@ "groups": [], "config": {}, "extra": { + "ds": { + "offset": [0, 0], + "scale": 1 + }, "groupNodes": { "hello": { "nodes": [ @@ -249,4 +253,4 @@ } }, "version": 0.4 -} \ No newline at end of file +} diff --git a/browser_tests/assets/missing_models_from_node_properties.json b/browser_tests/assets/missing_models_from_node_properties.json index cf7b1e198..b1a8e5a67 100644 --- a/browser_tests/assets/missing_models_from_node_properties.json +++ b/browser_tests/assets/missing_models_from_node_properties.json @@ -44,6 +44,11 @@ "links": [], "groups": [], "config": {}, - "extra": {}, + "extra": { + "ds": { + "offset": [0, 0], + "scale": 1 + } + }, "version": 0.4 } diff --git a/browser_tests/assets/missing_nodes_converted_widget.json b/browser_tests/assets/missing_nodes_converted_widget.json index 5237f0f9f..b7c3a73aa 100644 --- a/browser_tests/assets/missing_nodes_converted_widget.json +++ b/browser_tests/assets/missing_nodes_converted_widget.json @@ -61,6 +61,11 @@ "links": [], "groups": [], "config": {}, - "extra": {}, + "extra": { + "ds": { + "offset": [0, 0], + "scale": 1 + } + }, "version": 0.4 -} \ No newline at end of file +} diff --git a/browser_tests/assets/node_with_v2_combo_input.json b/browser_tests/assets/node_with_v2_combo_input.json new file mode 100644 index 000000000..a50270ba1 --- /dev/null +++ b/browser_tests/assets/node_with_v2_combo_input.json @@ -0,0 +1,36 @@ +{ + "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 +} diff --git a/browser_tests/assets/note_nodes.json b/browser_tests/assets/note_nodes.json index 280c44135..95d8b149d 100644 --- a/browser_tests/assets/note_nodes.json +++ b/browser_tests/assets/note_nodes.json @@ -50,6 +50,11 @@ "links": [], "groups": [], "config": {}, - "extra": {}, + "extra": { + "ds": { + "offset": [0, 0], + "scale": 1 + } + }, "version": 0.4 -} \ No newline at end of file +} diff --git a/browser_tests/assets/only_optional_inputs.json b/browser_tests/assets/only_optional_inputs.json index 3f4198756..fa926118a 100644 --- a/browser_tests/assets/only_optional_inputs.json +++ b/browser_tests/assets/only_optional_inputs.json @@ -38,7 +38,11 @@ "groups": [], "config": {}, "extra": { - "groupNodes": {} + "groupNodes": {}, + "ds": { + "offset": [0, 0], + "scale": 1 + } }, "version": 0.4 -} \ No newline at end of file +} diff --git a/browser_tests/assets/primitive/primitive_node_unconnected_dom_widget.json b/browser_tests/assets/primitive/primitive_node_unconnected_dom_widget.json index 48d95d3ff..5ed8d6d2c 100644 --- a/browser_tests/assets/primitive/primitive_node_unconnected_dom_widget.json +++ b/browser_tests/assets/primitive/primitive_node_unconnected_dom_widget.json @@ -54,6 +54,11 @@ "links": [], "groups": [], "config": {}, - "extra": {}, + "extra": { + "ds": { + "offset": [0, 0], + "scale": 1 + } + }, "version": 0.4 } diff --git a/browser_tests/assets/renamed_converted_widget.json b/browser_tests/assets/renamed_converted_widget.json index 37975583e..439018ec0 100644 --- a/browser_tests/assets/renamed_converted_widget.json +++ b/browser_tests/assets/renamed_converted_widget.json @@ -92,10 +92,14 @@ "groups": [], "config": {}, "extra": { + "ds": { + "offset": [0, 0], + "scale": 1 + }, "VHS_latentpreview": true, "VHS_latentpreviewrate": 0, "VHS_MetadataImage": false, "VHS_KeepIntermediate": false }, "version": 0.4 -} \ No newline at end of file +} diff --git a/browser_tests/assets/reroute/native_reroute.json b/browser_tests/assets/reroute/native_reroute.json index 993d478ac..af7510069 100644 --- a/browser_tests/assets/reroute/native_reroute.json +++ b/browser_tests/assets/reroute/native_reroute.json @@ -84,6 +84,10 @@ "groups": [], "config": {}, "extra": { + "ds": { + "offset": [0, 0], + "scale": 1 + }, "reroutes": [ { "id": 1, diff --git a/browser_tests/assets/single_connected_reroute_node.json b/browser_tests/assets/single_connected_reroute_node.json index c81c1204c..9c7347e1f 100644 --- a/browser_tests/assets/single_connected_reroute_node.json +++ b/browser_tests/assets/single_connected_reroute_node.json @@ -106,10 +106,7 @@ "extra": { "ds": { "scale": 1, - "offset": { - "0": 0, - "1": 0 - } + "offset": [0, 0] } }, "version": 0.4 diff --git a/browser_tests/assets/string_node_id.json b/browser_tests/assets/string_node_id.json index 275f81177..a4ed79ba7 100644 --- a/browser_tests/assets/string_node_id.json +++ b/browser_tests/assets/string_node_id.json @@ -368,10 +368,10 @@ "ds": { "scale": 1, "offset": [ - 149.9747408641311, - 383.8593224280729 + 0, + 0 ] } }, "version": 0.4 -} \ No newline at end of file +} diff --git a/browser_tests/assets/widgets/boolean_widget.json b/browser_tests/assets/widgets/boolean_widget.json index c60aee7d0..2c7d415f2 100644 --- a/browser_tests/assets/widgets/boolean_widget.json +++ b/browser_tests/assets/widgets/boolean_widget.json @@ -31,5 +31,11 @@ "links": [], "groups": [], "config": {}, + "extra": { + "ds": { + "offset": [0, 0], + "scale": 1 + } + }, "version": 0.4 -} \ No newline at end of file +} diff --git a/browser_tests/assets/widgets/load_animated_webp.json b/browser_tests/assets/widgets/load_animated_webp.json index 561da3147..a9d7f3cc9 100644 --- a/browser_tests/assets/widgets/load_animated_webp.json +++ b/browser_tests/assets/widgets/load_animated_webp.json @@ -8,4 +8,4 @@ "title": "Load Animated Image" } } -} \ No newline at end of file +} diff --git a/browser_tests/assets/widgets/load_audio_widget.json b/browser_tests/assets/widgets/load_audio_widget.json index c40440a1c..128720f61 100644 --- a/browser_tests/assets/widgets/load_audio_widget.json +++ b/browser_tests/assets/widgets/load_audio_widget.json @@ -27,6 +27,11 @@ "links": [], "groups": [], "config": {}, - "extra": {}, + "extra": { + "ds": { + "offset": [0, 0], + "scale": 1 + } + }, "version": 0.4 } diff --git a/browser_tests/assets/widgets/load_image_widget.json b/browser_tests/assets/widgets/load_image_widget.json index 844b77d53..1ba9e4158 100644 --- a/browser_tests/assets/widgets/load_image_widget.json +++ b/browser_tests/assets/widgets/load_image_widget.json @@ -41,6 +41,11 @@ "links": [], "groups": [], "config": {}, - "extra": {}, + "extra": { + "ds": { + "offset": [0, 0], + "scale": 1 + } + }, "version": 0.4 -} \ No newline at end of file +} diff --git a/browser_tests/assets/widgets/save_animated_webp.json b/browser_tests/assets/widgets/save_animated_webp.json index 362a56cec..664362f66 100644 --- a/browser_tests/assets/widgets/save_animated_webp.json +++ b/browser_tests/assets/widgets/save_animated_webp.json @@ -54,7 +54,11 @@ "groups": [], "config": {}, "extra": { - "frontendVersion": "1.17.0" + "frontendVersion": "1.17.0", + "ds": { + "offset": [0, 0], + "scale": 1 + } }, "version": 0.4 } diff --git a/browser_tests/fixtures/ComfyPage.ts b/browser_tests/fixtures/ComfyPage.ts index e27f440cd..2f6fc0b14 100644 --- a/browser_tests/fixtures/ComfyPage.ts +++ b/browser_tests/fixtures/ComfyPage.ts @@ -133,6 +133,9 @@ export class ComfyPage { // Inputs public readonly workflowUploadInput: Locator + // Toasts + public readonly visibleToasts: Locator + // Components public readonly searchBox: ComfyNodeSearchBox public readonly menu: ComfyMenu @@ -159,6 +162,8 @@ 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) @@ -396,6 +401,30 @@ 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() @@ -412,7 +441,20 @@ export class ComfyPage { } async getVisibleToastCount() { - return await this.page.locator('.p-toast-message:visible').count() + 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) } async clickTextEncodeNode1() { diff --git a/browser_tests/tests/backgroundImageUpload.spec.ts b/browser_tests/tests/backgroundImageUpload.spec.ts new file mode 100644 index 000000000..24af9e8ac --- /dev/null +++ b/browser_tests/tests/backgroundImageUpload.spec.ts @@ -0,0 +1,251 @@ +import { expect } from '@playwright/test' + +import { comfyPageFixture as test } from '../fixtures/ComfyPage' + +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('should upload image file and set as background', 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' + ) + // Click the upload button to trigger file input + const uploadButton = backgroundImageSetting.locator( + 'button:has(.pi-upload)' + ) + + // Set up file upload handler + const fileChooserPromise = comfyPage.page.waitForEvent('filechooser') + await uploadButton.click() + const fileChooser = await fileChooserPromise + + // Upload the test image + await fileChooser.setFiles(comfyPage.assetPath('image32x32.webp')) + + // Wait for upload to complete and verify the setting was updated + await comfyPage.page.waitForTimeout(500) // Give time for file reading + + // Verify the URL input now has an API URL + const urlInput = backgroundImageSetting.locator('input[type="text"]') + const inputValue = await urlInput.inputValue() + expect(inputValue).toMatch(/^\/api\/view\?.*subfolder=backgrounds/) + + // 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/) + }) + + test('should accept URL input for background image', async ({ + comfyPage + }) => { + 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 urlInput.fill(testImageUrl) + + // Trigger blur event to ensure the value is set + 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) + }) + + test('should clear background image when clear button is clicked', async ({ + comfyPage + }) => { + const testImageUrl = 'https://example.com/test-image.png' + + // First set a background image + 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 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('') + }) + + 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('should maintain reactive updates between URL input and clear button state', 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' + ) + 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 urlInput.fill('test') + await expect(clearButton).toBeEnabled() + + // Clear the text manually - clear button should become disabled again + await urlInput.fill('') + await expect(clearButton).toBeDisabled() + + // Add text again - clear button should become enabled + await urlInput.fill('https://example.com/image.png') + await expect(clearButton).toBeEnabled() + + // Use clear button - should clear input and disable itself + await clearButton.click() + await expect(urlInput).toHaveValue('') + await expect(clearButton).toBeDisabled() + }) +}) diff --git a/browser_tests/tests/chatHistory.spec.ts b/browser_tests/tests/chatHistory.spec.ts new file mode 100644 index 000000000..db3397514 --- /dev/null +++ b/browser_tests/tests/chatHistory.spec.ts @@ -0,0 +1,139 @@ +import { Page, expect } from '@playwright/test' + +import { comfyPageFixture as test } from '../fixtures/ComfyPage' + +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') + }) + + test('displays chat history when receiving display_component message', async ({ + comfyPage + }) => { + // Verify the chat history is displayed correctly + await expect(comfyPage.page.getByText('Hello')).toBeVisible() + await expect(comfyPage.page.getByText('World')).toBeVisible() + }) + + test('handles message editing interaction', async ({ comfyPage }) => { + // Get first node's ID + 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 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 comfyPage.page.getByLabel('Edit').first().click() + await comfyPage.nextFrame() + + // Verify cancel button appears + await expect(comfyPage.page.getByLabel('Cancel')).toBeVisible() + + // Click cancel edit + await comfyPage.page.getByLabel('Cancel').click() + + // Verify prompt input is restored + await expect(comfyPage.page.getByPlaceholder('text').nth(1)).toHaveValue( + originalTextAreaInput + ) + }) + + test('handles real-time updates to chat history', async ({ comfyPage }) => { + // Send initial history + await renderChatHistory(comfyPage.page, [ + { + prompt: 'Initial message', + response: 'Initial response', + response_id: '123' + } + ]) + await comfyPage.page.waitForSelector('.pi-pencil') + + // Update history with additional messages + 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 comfyPage.page.mouse.move(100, 100) + await comfyPage.nextFrame() + + // Verify new messages appear + await expect(comfyPage.page.getByText('Follow-up')).toBeVisible() + await expect(comfyPage.page.getByText('New response')).toBeVisible() + }) +}) diff --git a/browser_tests/tests/colorPalette.spec.ts-snapshots/custom-color-palette-light-red-chromium-linux.png b/browser_tests/tests/colorPalette.spec.ts-snapshots/custom-color-palette-light-red-chromium-linux.png index 789bdd1c5..df9170149 100644 Binary files a/browser_tests/tests/colorPalette.spec.ts-snapshots/custom-color-palette-light-red-chromium-linux.png and b/browser_tests/tests/colorPalette.spec.ts-snapshots/custom-color-palette-light-red-chromium-linux.png differ diff --git a/browser_tests/tests/colorPalette.spec.ts-snapshots/custom-color-palette-obsidian-dark-all-colors-chromium-linux.png b/browser_tests/tests/colorPalette.spec.ts-snapshots/custom-color-palette-obsidian-dark-all-colors-chromium-linux.png index f24114330..5471f705a 100644 Binary files a/browser_tests/tests/colorPalette.spec.ts-snapshots/custom-color-palette-obsidian-dark-all-colors-chromium-linux.png and b/browser_tests/tests/colorPalette.spec.ts-snapshots/custom-color-palette-obsidian-dark-all-colors-chromium-linux.png differ diff --git a/browser_tests/tests/colorPalette.spec.ts-snapshots/custom-color-palette-obsidian-dark-chromium-linux.png b/browser_tests/tests/colorPalette.spec.ts-snapshots/custom-color-palette-obsidian-dark-chromium-linux.png index b633dc04d..b4154f3a1 100644 Binary files a/browser_tests/tests/colorPalette.spec.ts-snapshots/custom-color-palette-obsidian-dark-chromium-linux.png and b/browser_tests/tests/colorPalette.spec.ts-snapshots/custom-color-palette-obsidian-dark-chromium-linux.png differ diff --git a/browser_tests/tests/colorPalette.spec.ts-snapshots/default-color-palette-chromium-linux.png b/browser_tests/tests/colorPalette.spec.ts-snapshots/default-color-palette-chromium-linux.png index 834c9d608..ce189d75d 100644 Binary files a/browser_tests/tests/colorPalette.spec.ts-snapshots/default-color-palette-chromium-linux.png and b/browser_tests/tests/colorPalette.spec.ts-snapshots/default-color-palette-chromium-linux.png differ diff --git a/browser_tests/tests/colorPalette.spec.ts-snapshots/node-lightened-colors-chromium-linux.png b/browser_tests/tests/colorPalette.spec.ts-snapshots/node-lightened-colors-chromium-linux.png index 789bdd1c5..df9170149 100644 Binary files a/browser_tests/tests/colorPalette.spec.ts-snapshots/node-lightened-colors-chromium-linux.png and b/browser_tests/tests/colorPalette.spec.ts-snapshots/node-lightened-colors-chromium-linux.png differ diff --git a/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-0-2-arc-theme-chromium-linux.png b/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-0-2-arc-theme-chromium-linux.png index 215f1d63a..fac5562ef 100644 Binary files a/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-0-2-arc-theme-chromium-linux.png and b/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-0-2-arc-theme-chromium-linux.png differ diff --git a/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-0-3-color-changed-chromium-linux.png b/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-0-3-color-changed-chromium-linux.png index 8a801dacd..f3532da7e 100644 Binary files a/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-0-3-color-changed-chromium-linux.png and b/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-0-3-color-changed-chromium-linux.png differ diff --git a/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-0-3-color-removed-chromium-linux.png b/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-0-3-color-removed-chromium-linux.png index afe7d39cf..66cbbc66a 100644 Binary files a/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-0-3-color-removed-chromium-linux.png and b/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-0-3-color-removed-chromium-linux.png differ diff --git a/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-0-5-chromium-linux.png b/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-0-5-chromium-linux.png index dbeafc2c3..9fa9f52c6 100644 Binary files a/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-0-5-chromium-linux.png and b/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-0-5-chromium-linux.png differ diff --git a/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-1-chromium-linux.png b/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-1-chromium-linux.png index 834c9d608..ce189d75d 100644 Binary files a/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-1-chromium-linux.png and b/browser_tests/tests/colorPalette.spec.ts-snapshots/node-opacity-1-chromium-linux.png differ diff --git a/browser_tests/tests/copyPaste.spec.ts-snapshots/copied-node-chromium-linux.png b/browser_tests/tests/copyPaste.spec.ts-snapshots/copied-node-chromium-linux.png index 3d749cc1e..23d3bf5b2 100644 Binary files a/browser_tests/tests/copyPaste.spec.ts-snapshots/copied-node-chromium-linux.png and b/browser_tests/tests/copyPaste.spec.ts-snapshots/copied-node-chromium-linux.png differ diff --git a/browser_tests/tests/copyPaste.spec.ts-snapshots/copied-node-with-link-chromium-linux.png b/browser_tests/tests/copyPaste.spec.ts-snapshots/copied-node-with-link-chromium-linux.png index bc7f94ccb..e661976cf 100644 Binary files a/browser_tests/tests/copyPaste.spec.ts-snapshots/copied-node-with-link-chromium-linux.png and b/browser_tests/tests/copyPaste.spec.ts-snapshots/copied-node-with-link-chromium-linux.png differ diff --git a/browser_tests/tests/copyPaste.spec.ts-snapshots/copied-widget-value-chromium-linux.png b/browser_tests/tests/copyPaste.spec.ts-snapshots/copied-widget-value-chromium-linux.png index bd9af5209..c5d5c4822 100644 Binary files a/browser_tests/tests/copyPaste.spec.ts-snapshots/copied-widget-value-chromium-linux.png and b/browser_tests/tests/copyPaste.spec.ts-snapshots/copied-widget-value-chromium-linux.png differ diff --git a/browser_tests/tests/copyPaste.spec.ts-snapshots/drag-copy-copied-node-chromium-linux.png b/browser_tests/tests/copyPaste.spec.ts-snapshots/drag-copy-copied-node-chromium-linux.png index 06ec2cd71..753449fc9 100644 Binary files a/browser_tests/tests/copyPaste.spec.ts-snapshots/drag-copy-copied-node-chromium-linux.png and b/browser_tests/tests/copyPaste.spec.ts-snapshots/drag-copy-copied-node-chromium-linux.png differ diff --git a/browser_tests/tests/copyPaste.spec.ts-snapshots/no-node-copied-chromium-linux.png b/browser_tests/tests/copyPaste.spec.ts-snapshots/no-node-copied-chromium-linux.png index dfbb44d83..bee4d8dbd 100644 Binary files a/browser_tests/tests/copyPaste.spec.ts-snapshots/no-node-copied-chromium-linux.png and b/browser_tests/tests/copyPaste.spec.ts-snapshots/no-node-copied-chromium-linux.png differ diff --git a/browser_tests/tests/copyPaste.spec.ts-snapshots/paste-in-text-area-with-node-previously-copied-chromium-linux.png b/browser_tests/tests/copyPaste.spec.ts-snapshots/paste-in-text-area-with-node-previously-copied-chromium-linux.png index 1b93bc9bd..66afcb941 100644 Binary files a/browser_tests/tests/copyPaste.spec.ts-snapshots/paste-in-text-area-with-node-previously-copied-chromium-linux.png and b/browser_tests/tests/copyPaste.spec.ts-snapshots/paste-in-text-area-with-node-previously-copied-chromium-linux.png differ diff --git a/browser_tests/tests/domWidget.spec.ts-snapshots/focus-mode-on-chromium-linux.png b/browser_tests/tests/domWidget.spec.ts-snapshots/focus-mode-on-chromium-linux.png index 47ff3702b..a0daef11e 100644 Binary files a/browser_tests/tests/domWidget.spec.ts-snapshots/focus-mode-on-chromium-linux.png and b/browser_tests/tests/domWidget.spec.ts-snapshots/focus-mode-on-chromium-linux.png differ diff --git a/browser_tests/tests/execution.spec.ts b/browser_tests/tests/execution.spec.ts index 1352d792f..4adab98b6 100644 --- a/browser_tests/tests/execution.spec.ts +++ b/browser_tests/tests/execution.spec.ts @@ -18,3 +18,27 @@ 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('') + }) +}) diff --git a/browser_tests/tests/execution.spec.ts-snapshots/execution-error-unconnected-slot-chromium-linux.png b/browser_tests/tests/execution.spec.ts-snapshots/execution-error-unconnected-slot-chromium-linux.png index 7eb5a84da..390eeb97c 100644 Binary files a/browser_tests/tests/execution.spec.ts-snapshots/execution-error-unconnected-slot-chromium-linux.png and b/browser_tests/tests/execution.spec.ts-snapshots/execution-error-unconnected-slot-chromium-linux.png differ diff --git a/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-hidden-links-chromium-linux.png b/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-hidden-links-chromium-linux.png index d37348608..87fd8aef9 100644 Binary files a/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-hidden-links-chromium-linux.png and b/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-hidden-links-chromium-linux.png differ diff --git a/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-visible-links-chromium-linux.png b/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-visible-links-chromium-linux.png index 6dd56615f..5c1003ca1 100644 Binary files a/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-visible-links-chromium-linux.png and b/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-visible-links-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts b/browser_tests/tests/interaction.spec.ts index b803578e4..552878a26 100644 --- a/browser_tests/tests/interaction.spec.ts +++ b/browser_tests/tests/interaction.spec.ts @@ -1,6 +1,6 @@ import { expect } from '@playwright/test' -import { comfyPageFixture as test } from '../fixtures/ComfyPage' +import { type ComfyPage, comfyPageFixture as test } from '../fixtures/ComfyPage' test.describe('Item Interaction', () => { test('Can select/delete all items', async ({ comfyPage }) => { @@ -666,6 +666,12 @@ test.describe('Load workflow', () => { expect(activeWorkflowName).toEqual(workflowPathB) }) }) + + test('Auto fit view after loading workflow', async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.EnableWorkflowViewRestore', false) + await comfyPage.loadWorkflow('single_ksampler') + await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler_fit.png') + }) }) test.describe('Load duplicate workflow', () => { @@ -683,3 +689,42 @@ test.describe('Load duplicate workflow', () => { expect(await comfyPage.getGraphNodesCount()).toBe(1) }) }) + +test.describe('Viewport settings', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.setSetting('Comfy.Workflow.WorkflowTabsPosition', 'Topbar') + + await comfyPage.setupWorkflowsDirectory({}) + }) + + test('Keeps viewport settings when changing tabs', async ({ + comfyPage, + comfyMouse + }) => { + // Screenshot the canvas element + await comfyPage.menu.topbar.saveWorkflow('Workflow A') + await expect(comfyPage.canvas).toHaveScreenshot('viewport-workflow-a.png') + + // Save workflow as a new file, then zoom out before screen shot + await comfyPage.menu.topbar.saveWorkflowAs('Workflow B') + await comfyMouse.move(comfyPage.emptySpace) + for (let i = 0; i < 4; i++) { + await comfyMouse.wheel(0, 60) + } + await expect(comfyPage.canvas).toHaveScreenshot('viewport-workflow-b.png') + + const tabA = comfyPage.menu.topbar.getWorkflowTab('Workflow A') + const tabB = comfyPage.menu.topbar.getWorkflowTab('Workflow B') + + // Go back to Workflow A + await tabA.click() + await comfyPage.nextFrame() + await expect(comfyPage.canvas).toHaveScreenshot('viewport-workflow-a.png') + + // And back to Workflow B + await tabB.click() + await comfyPage.nextFrame() + await expect(comfyPage.canvas).toHaveScreenshot('viewport-workflow-b.png') + }) +}) diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/adjusted-widget-value-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/adjusted-widget-value-chromium-linux.png index 7bf2ebd44..35848d4a7 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/adjusted-widget-value-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/adjusted-widget-value-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/batch-disconnect-links-disconnected-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/batch-disconnect-links-disconnected-chromium-linux.png index 97838c2a5..93643be02 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/batch-disconnect-links-disconnected-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/batch-disconnect-links-disconnected-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/batch-move-links-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/batch-move-links-chromium-linux.png index 5bd872932..3aeda00d7 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/batch-move-links-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/batch-move-links-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/batch-move-links-moved-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/batch-move-links-moved-chromium-linux.png index a5d028197..5f2617b5b 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/batch-move-links-moved-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/batch-move-links-moved-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/default-chromium-2x-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/default-chromium-2x-linux.png index f24e77e8f..2202fa81f 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/default-chromium-2x-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/default-chromium-2x-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/default-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/default-chromium-linux.png index 450657cfc..1ceb3d26c 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/default-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/default-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/disconnected-edge-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/disconnected-edge-chromium-linux.png index d8be3e4e7..50e68c1ef 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/disconnected-edge-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/disconnected-edge-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/dragged-node1-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/dragged-node1-chromium-linux.png index 31c24de31..54134a628 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/dragged-node1-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/dragged-node1-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/dragging-link1-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/dragging-link1-chromium-linux.png index f3f25e6b9..9813cd227 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/dragging-link1-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/dragging-link1-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/dragging-link2-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/dragging-link2-chromium-linux.png index 879c642bd..c5cf94808 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/dragging-link2-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/dragging-link2-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/group-selected-nodes-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/group-selected-nodes-chromium-linux.png index dfe628e55..0dfd3537b 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/group-selected-nodes-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/group-selected-nodes-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/moved-link-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/moved-link-chromium-linux.png index f8ee1a42d..74b4b16f6 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/moved-link-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/moved-link-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/nodes-bypassed-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/nodes-bypassed-chromium-linux.png index 42f6201e3..1925bc133 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/nodes-bypassed-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/nodes-bypassed-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/nodes-pinned-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/nodes-pinned-chromium-linux.png index 9bdaeda52..6f655b1c6 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/nodes-pinned-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/nodes-pinned-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/nodes-unbypassed-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/nodes-unbypassed-chromium-linux.png index 85a2968d1..50cda8cd8 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/nodes-unbypassed-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/nodes-unbypassed-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/nodes-unpinned-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/nodes-unpinned-chromium-linux.png index 85a2968d1..50cda8cd8 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/nodes-unpinned-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/nodes-unpinned-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/panned-back-to-one-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/panned-back-to-one-chromium-linux.png index 450657cfc..1ceb3d26c 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/panned-back-to-one-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/panned-back-to-one-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/panning-when-dragging-link-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/panning-when-dragging-link-chromium-linux.png index 7846d2e5b..fdc88e692 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/panning-when-dragging-link-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/panning-when-dragging-link-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-closed-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-closed-chromium-linux.png index 450657cfc..1ceb3d26c 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-closed-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-closed-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-opened-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-opened-chromium-linux.png index 813986967..61a5ad35c 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-opened-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-opened-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/selected-node1-chromium-2x-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/selected-node1-chromium-2x-linux.png index 22af4f67e..ff6a2a097 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/selected-node1-chromium-2x-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/selected-node1-chromium-2x-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/selected-node1-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/selected-node1-chromium-linux.png index 1f15bb04e..acf78fa5c 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/selected-node1-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/selected-node1-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/selected-node2-chromium-2x-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/selected-node2-chromium-2x-linux.png index f135dfc36..1e0164e84 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/selected-node2-chromium-2x-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/selected-node2-chromium-2x-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/selected-node2-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/selected-node2-chromium-linux.png index c995ac384..53a4d5832 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/selected-node2-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/selected-node2-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/single-ksampler-fit-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/single-ksampler-fit-chromium-linux.png new file mode 100644 index 000000000..e8ef3a6e2 Binary files /dev/null and b/browser_tests/tests/interaction.spec.ts-snapshots/single-ksampler-fit-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/snapped-highlighted-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/snapped-highlighted-chromium-linux.png index 38c7c1efa..3d92a8d61 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/snapped-highlighted-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/snapped-highlighted-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/text-encode-toggled-back-open-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/text-encode-toggled-back-open-chromium-linux.png index 1d14012c3..48f0aae81 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/text-encode-toggled-back-open-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/text-encode-toggled-back-open-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/text-encode-toggled-off-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/text-encode-toggled-off-chromium-linux.png index 2f7db2566..c560eec76 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/text-encode-toggled-off-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/text-encode-toggled-off-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/viewport-workflow-a-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/viewport-workflow-a-chromium-linux.png new file mode 100644 index 000000000..a6b932e14 Binary files /dev/null and b/browser_tests/tests/interaction.spec.ts-snapshots/viewport-workflow-a-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/viewport-workflow-b-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/viewport-workflow-b-chromium-linux.png new file mode 100644 index 000000000..2c7d67ed8 Binary files /dev/null and b/browser_tests/tests/interaction.spec.ts-snapshots/viewport-workflow-b-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-back-in-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-back-in-chromium-linux.png index 450657cfc..1ceb3d26c 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-back-in-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-back-in-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-default-ctrl-shift-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-default-ctrl-shift-chromium-linux.png index 39b80ac4d..7d4ca01da 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-default-ctrl-shift-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-default-ctrl-shift-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-in-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-in-chromium-linux.png index 91767924b..64b974541 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-in-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-in-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-in-low-zoom-speed-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-in-low-zoom-speed-chromium-linux.png index c6fe5bce7..4e889c59e 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-in-low-zoom-speed-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-in-low-zoom-speed-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-out-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-out-chromium-linux.png index 450657cfc..1ceb3d26c 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-out-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-out-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-out-low-zoom-speed-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-out-low-zoom-speed-chromium-linux.png index 9afb488d4..4ccd658b3 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-out-low-zoom-speed-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/zoomed-out-low-zoom-speed-chromium-linux.png differ diff --git a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/edited-workflow-webp-chromium-linux.png b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/edited-workflow-webp-chromium-linux.png index 0c77cfec2..473c5a249 100644 Binary files a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/edited-workflow-webp-chromium-linux.png and b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/edited-workflow-webp-chromium-linux.png differ diff --git a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/large-workflow-webp-chromium-linux.png b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/large-workflow-webp-chromium-linux.png index 0b51d89b3..c0e3df7c2 100644 Binary files a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/large-workflow-webp-chromium-linux.png and b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/large-workflow-webp-chromium-linux.png differ diff --git a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/no-workflow-webp-chromium-linux.png b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/no-workflow-webp-chromium-linux.png index f999e5886..8f759e14f 100644 Binary files a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/no-workflow-webp-chromium-linux.png and b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/no-workflow-webp-chromium-linux.png differ diff --git a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-m4v-chromium-linux.png b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-m4v-chromium-linux.png index 000a91e3e..0a06172ed 100644 Binary files a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-m4v-chromium-linux.png and b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-m4v-chromium-linux.png differ diff --git a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-mov-chromium-linux.png b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-mov-chromium-linux.png index 000a91e3e..0a06172ed 100644 Binary files a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-mov-chromium-linux.png and b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-mov-chromium-linux.png differ diff --git a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-mp4-chromium-linux.png b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-mp4-chromium-linux.png index 000a91e3e..0a06172ed 100644 Binary files a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-mp4-chromium-linux.png and b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-mp4-chromium-linux.png differ diff --git a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-svg-chromium-linux.png b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-svg-chromium-linux.png index 0c77cfec2..473c5a249 100644 Binary files a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-svg-chromium-linux.png and b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-svg-chromium-linux.png differ diff --git a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-webm-chromium-linux.png b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-webm-chromium-linux.png index a408b7288..f5285ca2e 100644 Binary files a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-webm-chromium-linux.png and b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-webm-chromium-linux.png differ diff --git a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-webp-chromium-linux.png b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-webp-chromium-linux.png index 0c77cfec2..473c5a249 100644 Binary files a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-webp-chromium-linux.png and b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-webp-chromium-linux.png differ diff --git a/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-chromium-linux.png b/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-chromium-linux.png index 420d85e2d..e7dab643e 100644 Binary files a/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-chromium-linux.png and b/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-chromium-linux.png differ diff --git a/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-left-chromium-linux.png b/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-left-chromium-linux.png index c2b22b0ac..18dffb1dc 100644 Binary files a/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-left-chromium-linux.png and b/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-left-chromium-linux.png differ diff --git a/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-light-color-palette-chromium-linux.png b/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-light-color-palette-chromium-linux.png index 5fc84fdd1..1553c2713 100644 Binary files a/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-light-color-palette-chromium-linux.png and b/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-light-color-palette-chromium-linux.png differ diff --git a/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-multiple-chromium-linux.png b/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-multiple-chromium-linux.png index 4460bd34b..3ce00943c 100644 Binary files a/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-multiple-chromium-linux.png and b/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-multiple-chromium-linux.png differ diff --git a/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-unknown-color-palette-chromium-linux.png b/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-unknown-color-palette-chromium-linux.png index 1716c43a7..04f8ca04e 100644 Binary files a/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-unknown-color-palette-chromium-linux.png and b/browser_tests/tests/nodeBadge.spec.ts-snapshots/node-badge-unknown-color-palette-chromium-linux.png differ diff --git a/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/added-node-chromium-linux.png b/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/added-node-chromium-linux.png index fb5a6aa47..dca7b447a 100644 Binary files a/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/added-node-chromium-linux.png and b/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/added-node-chromium-linux.png differ diff --git a/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/added-node-no-connection-chromium-linux.png b/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/added-node-no-connection-chromium-linux.png index 510dac62c..5854ba91f 100644 Binary files a/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/added-node-no-connection-chromium-linux.png and b/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/added-node-no-connection-chromium-linux.png differ diff --git a/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/auto-linked-node-batch-chromium-linux.png b/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/auto-linked-node-batch-chromium-linux.png index e7ab2600a..675ac2042 100644 Binary files a/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/auto-linked-node-batch-chromium-linux.png and b/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/auto-linked-node-batch-chromium-linux.png differ diff --git a/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/auto-linked-node-chromium-linux.png b/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/auto-linked-node-chromium-linux.png index cd144c3a7..60b4ae4fb 100644 Binary files a/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/auto-linked-node-chromium-linux.png and b/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/auto-linked-node-chromium-linux.png differ diff --git a/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/link-context-menu-search-chromium-linux.png b/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/link-context-menu-search-chromium-linux.png index cd144c3a7..60b4ae4fb 100644 Binary files a/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/link-context-menu-search-chromium-linux.png and b/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/link-context-menu-search-chromium-linux.png differ diff --git a/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/link-release-context-menu-chromium-linux.png b/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/link-release-context-menu-chromium-linux.png index 69b011bfa..aee756600 100644 Binary files a/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/link-release-context-menu-chromium-linux.png and b/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/link-release-context-menu-chromium-linux.png differ diff --git a/browser_tests/tests/primitiveNode.spec.ts-snapshots/primitive-node-connected-chromium-linux.png b/browser_tests/tests/primitiveNode.spec.ts-snapshots/primitive-node-connected-chromium-linux.png index 6d2ff680d..37600013f 100644 Binary files a/browser_tests/tests/primitiveNode.spec.ts-snapshots/primitive-node-connected-chromium-linux.png and b/browser_tests/tests/primitiveNode.spec.ts-snapshots/primitive-node-connected-chromium-linux.png differ diff --git a/browser_tests/tests/primitiveNode.spec.ts-snapshots/static-primitive-connected-chromium-linux.png b/browser_tests/tests/primitiveNode.spec.ts-snapshots/static-primitive-connected-chromium-linux.png index 314c61495..53ce62dfd 100644 Binary files a/browser_tests/tests/primitiveNode.spec.ts-snapshots/static-primitive-connected-chromium-linux.png and b/browser_tests/tests/primitiveNode.spec.ts-snapshots/static-primitive-connected-chromium-linux.png differ diff --git a/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-alt-click-chromium-linux.png b/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-alt-click-chromium-linux.png index 581bb1e3a..a2dff8a4b 100644 Binary files a/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-alt-click-chromium-linux.png and b/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-alt-click-chromium-linux.png differ diff --git a/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-chromium-linux.png b/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-chromium-linux.png index a8e495818..2db4387ff 100644 Binary files a/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-chromium-linux.png and b/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-chromium-linux.png differ diff --git a/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-context-menu-chromium-linux.png b/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-context-menu-chromium-linux.png index c76d231a6..e9bd067bb 100644 Binary files a/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-context-menu-chromium-linux.png and b/browser_tests/tests/rerouteNode.spec.ts-snapshots/native-reroute-context-menu-chromium-linux.png differ diff --git a/browser_tests/tests/rerouteNode.spec.ts-snapshots/reroute-inserted-chromium-linux.png b/browser_tests/tests/rerouteNode.spec.ts-snapshots/reroute-inserted-chromium-linux.png index 9100281f0..e059ce642 100644 Binary files a/browser_tests/tests/rerouteNode.spec.ts-snapshots/reroute-inserted-chromium-linux.png and b/browser_tests/tests/rerouteNode.spec.ts-snapshots/reroute-inserted-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/add-group-group-added-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/add-group-group-added-chromium-linux.png index fc3804537..b29ad2e1c 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/add-group-group-added-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/add-group-group-added-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/add-node-node-added-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/add-node-node-added-chromium-linux.png index ca6f9d6fc..3dcfb4ea1 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/add-node-node-added-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/add-node-node-added-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/node-pinned-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/node-pinned-chromium-linux.png index b7f6a32d8..36421f65e 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/node-pinned-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/node-pinned-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-menu-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-menu-chromium-linux.png index 93ee7bb94..49926184b 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-menu-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-menu-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-bypassed-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-bypassed-chromium-linux.png index 8b8681694..16a97cdcb 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-bypassed-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-bypassed-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-chromium-linux.png index 5e41ad2ed..1ebee0fd6 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-collapsed-badge-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-collapsed-badge-chromium-linux.png index ac023b3c2..364ce155c 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-collapsed-badge-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-collapsed-badge-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-collapsed-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-collapsed-chromium-linux.png index 6c29d043a..77b01c30a 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-collapsed-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-collapsed-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-group-node-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-group-node-chromium-linux.png index 497e79d86..855c5a15a 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-group-node-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-node-group-node-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-pinned-node-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-pinned-node-chromium-linux.png index 4eaf3ff2b..acf5ee2d4 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-pinned-node-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-pinned-node-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-unpinned-node-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-unpinned-node-chromium-linux.png index 5e41ad2ed..1ebee0fd6 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-unpinned-node-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-unpinned-node-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-unpinned-node-moved-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-unpinned-node-moved-chromium-linux.png index f12888d1d..8bc71316f 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-unpinned-node-moved-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-unpinned-node-moved-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-2-nodes-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-2-nodes-chromium-linux.png index 85a2968d1..50cda8cd8 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-2-nodes-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-2-nodes-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-nodes-pinned-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-nodes-pinned-chromium-linux.png index be3431519..32dbf1740 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-nodes-pinned-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-nodes-pinned-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-nodes-unpinned-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-nodes-unpinned-chromium-linux.png index f4cbf7424..234300f15 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-nodes-unpinned-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-nodes-unpinned-chromium-linux.png differ diff --git a/browser_tests/tests/templates.spec.ts b/browser_tests/tests/templates.spec.ts index a891cc098..f2c2e2bb5 100644 --- a/browser_tests/tests/templates.spec.ts +++ b/browser_tests/tests/templates.spec.ts @@ -32,7 +32,9 @@ test.describe('Templates', () => { } }) - test('should have all required thumbnail media for each template', async ({ + // 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 ({ comfyPage }) => { test.slow() @@ -142,4 +144,136 @@ 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' + ) + }) }) diff --git a/browser_tests/tests/templates.spec.ts-snapshots/template-card-after-hover-chromium-linux.png b/browser_tests/tests/templates.spec.ts-snapshots/template-card-after-hover-chromium-linux.png new file mode 100644 index 000000000..b767dbe20 Binary files /dev/null and b/browser_tests/tests/templates.spec.ts-snapshots/template-card-after-hover-chromium-linux.png differ diff --git a/browser_tests/tests/templates.spec.ts-snapshots/template-card-before-hover-chromium-linux.png b/browser_tests/tests/templates.spec.ts-snapshots/template-card-before-hover-chromium-linux.png new file mode 100644 index 000000000..cfebed5da Binary files /dev/null and b/browser_tests/tests/templates.spec.ts-snapshots/template-card-before-hover-chromium-linux.png differ diff --git a/browser_tests/tests/templates.spec.ts-snapshots/template-grid-desktop-chromium-linux.png b/browser_tests/tests/templates.spec.ts-snapshots/template-grid-desktop-chromium-linux.png new file mode 100644 index 000000000..ecf8658c8 Binary files /dev/null and b/browser_tests/tests/templates.spec.ts-snapshots/template-grid-desktop-chromium-linux.png differ diff --git a/browser_tests/tests/templates.spec.ts-snapshots/template-grid-mobile-chromium-linux.png b/browser_tests/tests/templates.spec.ts-snapshots/template-grid-mobile-chromium-linux.png new file mode 100644 index 000000000..9bad33cd8 Binary files /dev/null and b/browser_tests/tests/templates.spec.ts-snapshots/template-grid-mobile-chromium-linux.png differ diff --git a/browser_tests/tests/templates.spec.ts-snapshots/template-grid-tablet-chromium-linux.png b/browser_tests/tests/templates.spec.ts-snapshots/template-grid-tablet-chromium-linux.png new file mode 100644 index 000000000..448ae25cc Binary files /dev/null and b/browser_tests/tests/templates.spec.ts-snapshots/template-grid-tablet-chromium-linux.png differ diff --git a/browser_tests/tests/templates.spec.ts-snapshots/template-grid-varying-content-chromium-linux.png b/browser_tests/tests/templates.spec.ts-snapshots/template-grid-varying-content-chromium-linux.png new file mode 100644 index 000000000..40452d452 Binary files /dev/null and b/browser_tests/tests/templates.spec.ts-snapshots/template-grid-varying-content-chromium-linux.png differ diff --git a/browser_tests/tests/widget.spec.ts b/browser_tests/tests/widget.spec.ts index ba517cf2d..bf1461a47 100644 --- a/browser_tests/tests/widget.spec.ts +++ b/browser_tests/tests/widget.spec.ts @@ -53,6 +53,26 @@ test.describe('Combo text widget', () => { const refreshedComboValues = await getComboValues() expect(refreshedComboValues).not.toEqual(initialComboValues) }) + + test('Should refresh combo values of nodes with v2 combo input spec', async ({ + comfyPage + }) => { + await comfyPage.loadWorkflow('node_with_v2_combo_input') + // click canvas to focus + await comfyPage.page.mouse.click(400, 300) + // press R to trigger refresh + await comfyPage.page.keyboard.press('r') + // wait for nodes' widgets to be updated + await comfyPage.page.mouse.click(400, 300) + await comfyPage.nextFrame() + // get the combo widget's values + const 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']) + }) }) test.describe('Boolean widget', () => { diff --git a/browser_tests/tests/widget.spec.ts-snapshots/animated-image-preview-saved-webp-chromium-linux.png b/browser_tests/tests/widget.spec.ts-snapshots/animated-image-preview-saved-webp-chromium-linux.png index 5392271d8..c1fb924d4 100644 Binary files a/browser_tests/tests/widget.spec.ts-snapshots/animated-image-preview-saved-webp-chromium-linux.png and b/browser_tests/tests/widget.spec.ts-snapshots/animated-image-preview-saved-webp-chromium-linux.png differ diff --git a/browser_tests/tests/widget.spec.ts-snapshots/empty-latent-resized-80-percent-chromium-linux.png b/browser_tests/tests/widget.spec.ts-snapshots/empty-latent-resized-80-percent-chromium-linux.png index af0ba7df6..a0c6e35ac 100644 Binary files a/browser_tests/tests/widget.spec.ts-snapshots/empty-latent-resized-80-percent-chromium-linux.png and b/browser_tests/tests/widget.spec.ts-snapshots/empty-latent-resized-80-percent-chromium-linux.png differ diff --git a/browser_tests/tests/widget.spec.ts-snapshots/ksampler-resized-min-width-chromium-linux.png b/browser_tests/tests/widget.spec.ts-snapshots/ksampler-resized-min-width-chromium-linux.png index c0d26b325..063f8da7e 100644 Binary files a/browser_tests/tests/widget.spec.ts-snapshots/ksampler-resized-min-width-chromium-linux.png and b/browser_tests/tests/widget.spec.ts-snapshots/ksampler-resized-min-width-chromium-linux.png differ diff --git a/browser_tests/tests/widget.spec.ts-snapshots/load-checkpoint-resized-min-width-chromium-linux.png b/browser_tests/tests/widget.spec.ts-snapshots/load-checkpoint-resized-min-width-chromium-linux.png index 6da898f2f..9477bfd69 100644 Binary files a/browser_tests/tests/widget.spec.ts-snapshots/load-checkpoint-resized-min-width-chromium-linux.png and b/browser_tests/tests/widget.spec.ts-snapshots/load-checkpoint-resized-min-width-chromium-linux.png differ diff --git a/browser_tests/tests/widget.spec.ts-snapshots/resized-to-original-chromium-linux.png b/browser_tests/tests/widget.spec.ts-snapshots/resized-to-original-chromium-linux.png index d85a847bf..9c6af8a06 100644 Binary files a/browser_tests/tests/widget.spec.ts-snapshots/resized-to-original-chromium-linux.png and b/browser_tests/tests/widget.spec.ts-snapshots/resized-to-original-chromium-linux.png differ diff --git a/build/plugins/addElementVnodeExportPlugin.ts b/build/plugins/addElementVnodeExportPlugin.ts deleted file mode 100644 index 1266d13e2..000000000 --- a/build/plugins/addElementVnodeExportPlugin.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Plugin } from 'vite' - -/** - * Vite plugin that adds an alias export for Vue's createBaseVNode as createElementVNode. - * - * This plugin addresses compatibility issues where some components or libraries - * might be using the older createElementVNode function name instead of createBaseVNode. - * It modifies the Vue vendor chunk during build to add the alias export. - * - * @returns {Plugin} A Vite plugin that modifies the Vue vendor chunk exports - */ -export function addElementVnodeExportPlugin(): Plugin { - return { - name: 'add-element-vnode-export-plugin', - - renderChunk(code, chunk, _options) { - if (chunk.name.startsWith('vendor-vue')) { - const exportRegex = /(export\s*\{)([^}]*)(\}\s*;?\s*)$/ - const match = code.match(exportRegex) - - if (match) { - const existingExports = match[2].trim() - const exportsArray = existingExports - .split(',') - .map((e) => e.trim()) - .filter(Boolean) - - const hasCreateBaseVNode = exportsArray.some((e) => - e.startsWith('createBaseVNode') - ) - const hasCreateElementVNode = exportsArray.some((e) => - e.includes('createElementVNode') - ) - - if (hasCreateBaseVNode && !hasCreateElementVNode) { - const newExportStatement = `${match[1]} ${existingExports ? existingExports + ',' : ''} createBaseVNode as createElementVNode ${match[3]}` - const newCode = code.replace(exportRegex, newExportStatement) - - console.log( - `[add-element-vnode-export-plugin] Added 'createBaseVNode as createElementVNode' export to vendor-vue chunk.` - ) - - return { code: newCode, map: null } - } else if (!hasCreateBaseVNode) { - console.warn( - `[add-element-vnode-export-plugin] Warning: 'createBaseVNode' not found in exports of vendor-vue chunk. Cannot add alias.` - ) - } - } else { - console.warn( - `[add-element-vnode-export-plugin] Warning: Could not find expected export block format in vendor-vue chunk.` - ) - } - } - - return null - } - } -} diff --git a/build/plugins/generateImportMapPlugin.ts b/build/plugins/generateImportMapPlugin.ts index c6661a811..80ccb6c9f 100644 --- a/build/plugins/generateImportMapPlugin.ts +++ b/build/plugins/generateImportMapPlugin.ts @@ -1,9 +1,24 @@ -import type { OutputOptions } from 'rollup' -import { HtmlTagDescriptor, Plugin } from 'vite' +import glob from 'fast-glob' +import fs from 'fs-extra' +import { dirname, join } from 'node:path' +import { HtmlTagDescriptor, Plugin, normalizePath } from 'vite' -interface VendorLibrary { +interface ImportMapSource { name: string - pattern: RegExp + pattern: string | RegExp + entry: string + recursiveDependence?: boolean + override?: Record> +} + +const parseDeps = (root: string, pkg: string) => { + const pkgPath = join(root, 'node_modules', pkg, 'package.json') + if (fs.existsSync(pkgPath)) { + const content = fs.readFileSync(pkgPath, 'utf-8') + const pkg = JSON.parse(content) + return Object.keys(pkg.dependencies || {}) + } + return [] } /** @@ -23,53 +38,89 @@ interface VendorLibrary { * @returns {Plugin} A Vite plugin that generates and injects an import map */ export function generateImportMapPlugin( - vendorLibraries: VendorLibrary[] + importMapSources: ImportMapSource[] ): Plugin { const importMapEntries: Record = {} + const resolvedImportMapSources: Map = new Map() + const assetDir = 'assets/lib' + let root: string return { name: 'generate-import-map-plugin', // Configure manual chunks during the build process configResolved(config) { + root = config.root + if (config.build) { // Ensure rollupOptions exists if (!config.build.rollupOptions) { config.build.rollupOptions = {} } - const outputOptions: OutputOptions = { - manualChunks: (id: string) => { - for (const lib of vendorLibraries) { - if (lib.pattern.test(id)) { - return `vendor-${lib.name}` - } + for (const source of importMapSources) { + resolvedImportMapSources.set(source.name, source) + if (source.recursiveDependence) { + const deps = parseDeps(root, source.name) + + while (deps.length) { + const dep = deps.shift()! + const depSource = Object.assign({}, source, { + name: dep, + pattern: dep, + ...source.override?.[dep] + }) + resolvedImportMapSources.set(depSource.name, depSource) + + const _deps = parseDeps(root, depSource.name) + deps.unshift(..._deps) } - return null - }, - // Disable minification of internal exports to preserve function names - minifyInternalExports: false + } } - config.build.rollupOptions.output = outputOptions + + const external: (string | RegExp)[] = [] + for (const [, source] of resolvedImportMapSources) { + external.push(source.pattern) + } + config.build.rollupOptions.external = external } }, - generateBundle(_options, bundle) { - for (const fileName in bundle) { - const chunk = bundle[fileName] - if (chunk.type === 'chunk' && !chunk.isEntry) { - // Find matching vendor library by chunk name - const vendorLib = vendorLibraries.find( - (lib) => chunk.name === `vendor-${lib.name}` - ) + generateBundle(_options) { + for (const [, source] of resolvedImportMapSources) { + if (source.entry) { + const moduleFile = join(source.name, source.entry) + const sourceFile = join(root, 'node_modules', moduleFile) + const targetFile = join(root, 'dist', assetDir, moduleFile) - if (vendorLib) { - const relativePath = `./${chunk.fileName.replace(/\\/g, '/')}` - importMapEntries[vendorLib.name] = relativePath + importMapEntries[source.name] = + './' + normalizePath(join(assetDir, moduleFile)) - console.log( - `[ImportMap Plugin] Found chunk: ${chunk.name} -> Mapped '${vendorLib.name}' to '${relativePath}'` - ) + const targetDir = dirname(targetFile) + if (!fs.existsSync(targetDir)) { + fs.mkdirSync(targetDir, { recursive: true }) + } + fs.copyFileSync(sourceFile, targetFile) + } + + if (source.recursiveDependence) { + const files = glob.sync(['**/*.{js,mjs}'], { + cwd: join(root, 'node_modules', source.name) + }) + + for (const file of files) { + const moduleFile = join(source.name, file) + const sourceFile = join(root, 'node_modules', moduleFile) + const targetFile = join(root, 'dist', assetDir, moduleFile) + + importMapEntries[normalizePath(join(source.name, dirname(file)))] = + './' + normalizePath(join(assetDir, moduleFile)) + + const targetDir = dirname(targetFile) + if (!fs.existsSync(targetDir)) { + fs.mkdirSync(targetDir, { recursive: true }) + } + fs.copyFileSync(sourceFile, targetFile) } } } diff --git a/build/plugins/index.ts b/build/plugins/index.ts index c67473f7a..f8c2d695c 100644 --- a/build/plugins/index.ts +++ b/build/plugins/index.ts @@ -1,3 +1,2 @@ -export { addElementVnodeExportPlugin } from './addElementVnodeExportPlugin' export { comfyAPIPlugin } from './comfyAPIPlugin' export { generateImportMapPlugin } from './generateImportMapPlugin' diff --git a/eslint.config.js b/eslint.config.js index 2b8b77f3c..9f212cfc1 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -24,7 +24,7 @@ export default [ }, parser: tseslint.parser, parserOptions: { - project: './tsconfig.json', + project: ['./tsconfig.json', './tsconfig.eslint.json'], ecmaVersion: 2020, sourceType: 'module', extraFileExtensions: ['.vue'] diff --git a/index.html b/index.html index 7a321795c..de7710c63 100644 --- a/index.html +++ b/index.html @@ -4,9 +4,18 @@ ComfyUI - + + + + + + + + + +
diff --git a/manifest.json b/manifest.json new file mode 100644 index 000000000..d906b2e23 --- /dev/null +++ b/manifest.json @@ -0,0 +1,9 @@ +{ + "name": "ComfyUI", + "short_name": "ComfyUI", + "description": "ComfyUI: AI image generation platform", + "start_url": "/", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#000000" +} diff --git a/package-lock.json b/package-lock.json index 55f4c16f6..17336afdf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,18 @@ { "name": "@comfyorg/comfyui-frontend", - "version": "1.18.6", + "version": "1.21.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@comfyorg/comfyui-frontend", - "version": "1.18.6", + "version": "1.21.2", "license": "GPL-3.0-only", "dependencies": { "@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.0-1", + "@comfyorg/litegraph": "^0.15.14", "@primevue/forms": "^4.2.5", "@primevue/themes": "^4.2.5", "@sentry/vue": "^8.48.0", @@ -83,6 +83,8 @@ "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", @@ -342,29 +344,66 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/generator": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", - "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", + "node_modules/@babel/compat-data": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz", + "integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", + "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/parser": "^7.26.3", - "@babel/types": "^7.26.3", + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helpers": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/template": "^7.27.1", + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", + "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.27.1", + "@babel/types": "^7.27.1", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -373,31 +412,193 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.1.tgz", + "integrity": "sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==", + "dev": true, + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz", + "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.27.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", + "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz", + "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "dev": true, + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", - "license": "MIT", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", - "license": "MIT", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", + "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1" + }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", - "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", - "license": "MIT", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", + "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", "dependencies": { - "@babel/types": "^7.26.3" + "@babel/types": "^7.27.1" }, "bin": { "parser": "bin/babel-parser.js" @@ -406,6 +607,114 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.27.1.tgz", + "integrity": "sha512-DTxe4LBPrtFdsWzgpmbBKevg3e9PBy+dXRt19kSbucbZvL2uqtdqwwpluL1jfxYE0wIDTFp1nTy/q6gNLsxXrg==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-decorators": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.27.1.tgz", + "integrity": "sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.27.1.tgz", + "integrity": "sha512-Q5sT5+O4QUebHdbwKedFBEwRLb02zJ7r4A5Gg2hUoLuU3FjdMcyqcywqUrLCaDsFCxzokf7u9kuy7qz51YUuAg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/runtime": { "version": "7.26.10", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", @@ -419,32 +728,30 @@ } }, "node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.26.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", - "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz", + "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.3", - "@babel/parser": "^7.26.3", - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.3", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -463,13 +770,12 @@ } }, "node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", - "license": "MIT", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -482,9 +788,9 @@ "license": "GPL-3.0-only" }, "node_modules/@comfyorg/litegraph": { - "version": "0.15.0-1", - "resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.15.0-1.tgz", - "integrity": "sha512-6fwTWZYbqylmTKDVHf0AKBgvcMQXMv1whiM7ig6NqBTFmAApqv66Hk8QS/iTZN1wWhNO55MHgor95CkHN5XGQg==", + "version": "0.15.14", + "resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.15.14.tgz", + "integrity": "sha512-9yERUwRVFPFspXowyg5z97QyF6+UbHG6ZNygvxSOisTCVSPOUeX/E02xcnhB5BHk0bTZCJGg9v2iztXBE5brnA==", "license": "MIT" }, "node_modules/@cspotcode/source-map-support": { @@ -1915,6 +2221,16 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", @@ -2038,19 +2354,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@lobehub/cli-ui/node_modules/is-unicode-supported": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@lobehub/i18n-cli": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/@lobehub/i18n-cli/-/i18n-cli-1.20.0.tgz", @@ -2434,6 +2737,12 @@ "node": ">=12" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true + }, "node_modules/@primeuix/forms": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/@primeuix/forms/-/forms-0.0.2.tgz", @@ -2623,14 +2932,14 @@ "license": "MIT" }, "node_modules/@rollup/pluginutils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", - "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", + "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", "dev": true, "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", - "picomatch": "^2.3.1" + "picomatch": "^4.0.2" }, "engines": { "node": ">=14.0.0" @@ -2644,6 +2953,18 @@ } } }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.22.4", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", @@ -3017,16 +3338,6 @@ } } }, - "node_modules/@rushstack/terminal/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/@rushstack/terminal/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -3056,6 +3367,12 @@ "string-argv": "~0.3.1" } }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true + }, "node_modules/@sentry-internal/browser-utils": { "version": "8.48.0", "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.48.0.tgz", @@ -3153,6 +3470,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@tiptap/core": { "version": "2.10.4", "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.10.4.tgz", @@ -4181,6 +4510,56 @@ "vscode-uri": "^3.0.8" } }, + "node_modules/@vue/babel-helper-vue-transform-on": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.4.0.tgz", + "integrity": "sha512-mCokbouEQ/ocRce/FpKCRItGo+013tHg7tixg3DUNS+6bmIchPt66012kBMm476vyEIJPafrvOf4E5OYj3shSw==", + "dev": true + }, + "node_modules/@vue/babel-plugin-jsx": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.4.0.tgz", + "integrity": "sha512-9zAHmwgMWlaN6qRKdrg1uKsBKHvnUU+Py+MOCTuYZBoZsopa90Di10QRjB+YPnVss0BZbG/H5XFwJY1fTxJWhA==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/plugin-syntax-jsx": "^7.25.9", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.9", + "@babel/types": "^7.26.9", + "@vue/babel-helper-vue-transform-on": "1.4.0", + "@vue/babel-plugin-resolve-type": "1.4.0", + "@vue/shared": "^3.5.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + } + } + }, + "node_modules/@vue/babel-plugin-resolve-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.4.0.tgz", + "integrity": "sha512-4xqDRRbQQEWHQyjlYSgZsWj44KfiF6D+ktCuXyZ8EnVDYV3pztmXJDf1HveAjUAXxAnR8daCQT51RneWWxtTyQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/parser": "^7.26.9", + "@vue/compiler-sfc": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@vue/compiler-core": { "version": "3.5.13", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz", @@ -4247,6 +4626,71 @@ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.3.tgz", "integrity": "sha512-0MiMsFma/HqA6g3KLKn+AGpL1kgKhFWszC9U29NfpWK5LE7bjeXxySWJrOJ77hBz+TBrBQ7o4QJqbPbqbs8rJw==" }, + "node_modules/@vue/devtools-core": { + "version": "7.7.6", + "resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-7.7.6.tgz", + "integrity": "sha512-ghVX3zjKPtSHu94Xs03giRIeIWlb9M+gvDRVpIZ/cRIxKHdW6HE/sm1PT3rUYS3aV92CazirT93ne+7IOvGUWg==", + "dev": true, + "dependencies": { + "@vue/devtools-kit": "^7.7.6", + "@vue/devtools-shared": "^7.7.6", + "mitt": "^3.0.1", + "nanoid": "^5.1.0", + "pathe": "^2.0.3", + "vite-hot-client": "^2.0.4" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/@vue/devtools-core/node_modules/nanoid": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz", + "integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/@vue/devtools-core/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.6", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.6.tgz", + "integrity": "sha512-geu7ds7tem2Y7Wz+WgbnbZ6T5eadOvozHZ23Atk/8tksHMFOFylKi1xgGlQlVn0wlkEf4hu+vd5ctj1G4kFtwA==", + "dev": true, + "dependencies": { + "@vue/devtools-shared": "^7.7.6", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.6", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.6.tgz", + "integrity": "sha512-yFEgJZ/WblEsojQQceuyK6FzpFDx4kqrz2ohInxNj5/DnhoX023upTv4OD6lNPLAA5LLkbwPVb10o/7b+Y4FVA==", + "dev": true, + "dependencies": { + "rfdc": "^1.4.1" + } + }, "node_modules/@vue/language-core": { "version": "2.1.10", "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.1.10.tgz", @@ -4457,9 +4901,9 @@ } }, "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -4887,6 +5331,15 @@ "integrity": "sha512-PJvH288AWQhKs2v9zyfYdPzlPqf5bXbGMmhmUIY9x4dAUGIWgomO771oBQNwJnMQSnUIXhKu6sgzpBRXTlvb8Q==", "license": "MIT" }, + "node_modules/birpc": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.3.0.tgz", + "integrity": "sha512-ijbtkn/F3Pvzb6jHypHRyve2QApOCZDR25D/VnkY2G/lBNcXCTsnsCxgY4k4PkVB7zfwzYbY3O9Lcqe3xufS5g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -4982,9 +5435,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", - "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "version": "4.24.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", + "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", "dev": true, "funding": [ { @@ -5001,10 +5454,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001629", - "electron-to-chromium": "^1.4.796", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.16" + "caniuse-lite": "^1.0.30001716", + "electron-to-chromium": "^1.5.149", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" @@ -5013,6 +5466,27 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -5052,6 +5526,16 @@ "node": ">=6" } }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -5062,9 +5546,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001690", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz", - "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==", + "version": "1.0.30001718", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", + "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", "dev": true, "funding": [ { @@ -5079,8 +5563,7 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ], - "license": "CC-BY-4.0" + ] }, "node_modules/ccount": { "version": "2.0.1", @@ -5181,6 +5664,18 @@ "node": ">=8" } }, + "node_modules/clean-css": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", + "dev": true, + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, "node_modules/cli-boxes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", @@ -5533,6 +6028,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/connect-history-api-fallback": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, "node_modules/consola": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", @@ -5543,6 +6047,12 @@ "node": "^14.18.0 || >=16.10.0" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, "node_modules/convert-to-spaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", @@ -5553,6 +6063,21 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "dev": true, + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -5645,6 +6170,34 @@ "node": ">= 8" } }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -5830,6 +6383,34 @@ "node": ">=0.10.0" } }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -5848,6 +6429,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -5947,6 +6540,41 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "dev": true }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, "node_modules/domexception": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", @@ -5962,6 +6590,45 @@ "node": ">=12" } }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/dot-prop": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-8.0.2.tgz", @@ -6002,6 +6669,15 @@ "url": "https://dotenvx.com" } }, + "node_modules/dotenv-expand": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-8.0.3.tgz", + "integrity": "sha512-SErOMvge0ZUyWd5B0NXMQlDkN+8r+HhVUsxgOO7IoPDOdDRD2JjExpN6y3KnFR66jsJMwSn1pqIivhU5rcJiNg==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -6076,10 +6752,25 @@ "node": ">=10" } }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/electron-to-chromium": { - "version": "1.4.817", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.817.tgz", - "integrity": "sha512-3znu+lZMIbTe8ZOs360OMJvVroVF2NpNI8T5jfLnDetVvj0uNmIucZzQVYMSJfsu9f47Ssox1Gt46PR+R+1JUg==", + "version": "1.5.154", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.154.tgz", + "integrity": "sha512-G4VCFAyKbp1QJ+sWdXYIRYsPGvlV5sDACfCmoMFog3rjm1syLhI41WXm/swZypwCIWIm4IFLWzHY14joWMQ5Fw==", "dev": true }, "node_modules/emoji-regex": { @@ -6121,6 +6812,15 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/error-stack-parser-es": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-0.1.5.tgz", + "integrity": "sha512-xHku1X40RO+fO8yJ8Wh2f2rZWVjqyhb1zgq1yZ8aZRQkv6OOKhKWRUaht3eSCUbAOBaKIgM+ykwFLE+QUxgGeg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/es-define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", @@ -6183,9 +6883,9 @@ } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "engines": { "node": ">=6" } @@ -6525,16 +7225,6 @@ "node": ">=10.13.0" } }, - "node_modules/eslint/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/eslint/node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -6567,19 +7257,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/espree": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz", @@ -6909,6 +7586,36 @@ "node": ">=16.0.0" } }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -7158,6 +7865,15 @@ "node": ">=10" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -7367,6 +8083,15 @@ "integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==", "dev": true }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", @@ -7428,6 +8153,12 @@ "he": "bin/he" } }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "dev": true + }, "node_modules/html-encoding-sniffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", @@ -7442,6 +8173,36 @@ "node": ">=12" } }, + "node_modules/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "dev": true, + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-minifier-terser/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, "node_modules/http-parser-js": { "version": "0.5.10", "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", @@ -7914,6 +8675,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", @@ -7973,6 +8749,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-installed-globally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz", @@ -8069,6 +8863,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-upper-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-upper-case/-/is-upper-case-2.0.2.tgz", @@ -8079,6 +8885,33 @@ "tslib": "^2.0.3" } }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "dev": true, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -8109,6 +8942,55 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jake/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/javascript-natural-sort": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", @@ -8360,6 +9242,18 @@ "dev": true, "license": "MIT" }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/jsondiffpatch": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/jsondiffpatch/-/jsondiffpatch-0.6.0.tgz", @@ -9116,6 +10010,24 @@ "dev": true, "license": "MIT" }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", @@ -10119,6 +11031,12 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true + }, "node_modules/mlly": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.1.tgz", @@ -10141,6 +11059,15 @@ "node": ">=4" } }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -10199,6 +11126,16 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -10265,10 +11202,20 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/node-html-parser": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-5.4.2.tgz", + "integrity": "sha512-RaBPP3+51hPne/OolXxcz89iYvQvKOydaqoePpOgXcrOKZhjVIzmpKZz+Hd/RBO2/zN2q6CNJhQzucVz+u3Jyw==", + "dev": true, + "dependencies": { + "css-select": "^4.2.1", + "he": "1.2.0" + } + }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", "dev": true }, "node_modules/nopt": { @@ -10398,6 +11345,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz", + "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==", + "dev": true, + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/openai": { "version": "4.73.1", "resolved": "https://registry.npmjs.org/openai/-/openai-4.73.1.tgz", @@ -10615,6 +11580,16 @@ "pangu": "dist/node/cli.js" } }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -10646,6 +11621,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse5": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", @@ -10660,6 +11647,16 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/patch-console": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz", @@ -10753,6 +11750,12 @@ "node": ">= 14.16" } }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "dev": true + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -11072,6 +12075,21 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/pretty-ms": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.2.0.tgz", + "integrity": "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==", + "dev": true, + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/primeicons": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz", @@ -11550,6 +12568,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/remark-frontmatter": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-4.0.1.tgz", @@ -11959,6 +12986,18 @@ "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", "license": "MIT" }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -12046,6 +13085,15 @@ "node": ">=4" } }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -12104,6 +13152,20 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/sirv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz", + "integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==", + "dev": true, + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -12158,6 +13220,25 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -12440,6 +13521,30 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/superjson": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz", + "integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==", + "dev": true, + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -12555,6 +13660,30 @@ "node": ">=10" } }, + "node_modules/terser": { + "version": "5.39.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz", + "integrity": "sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.14.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -12675,6 +13804,15 @@ "node": ">=8.0" } }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/tough-cookie": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", @@ -12896,6 +14034,18 @@ "string.fromcodepoint": "^0.2.1" } }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", @@ -13154,9 +14304,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", - "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "dev": true, "funding": [ { @@ -13173,8 +14323,8 @@ } ], "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -13445,6 +14595,18 @@ } } }, + "node_modules/vite-hot-client": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/vite-hot-client/-/vite-hot-client-2.0.4.tgz", + "integrity": "sha512-W9LOGAyGMrbGArYJN4LBCdOC5+Zwh7dHvOHC0KmGKkJhsOzaKbpo/jEjpPKVHIW0/jBWj8RZG0NUxfgA8BxgAg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0" + } + }, "node_modules/vite-node": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.0.tgz", @@ -13549,6 +14711,279 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/vite-plugin-html": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/vite-plugin-html/-/vite-plugin-html-3.2.2.tgz", + "integrity": "sha512-vb9C9kcdzcIo/Oc3CLZVS03dL5pDlOFuhGlZYDCJ840BhWl/0nGeZWf3Qy7NlOayscY4Cm/QRgULCQkEZige5Q==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^4.2.0", + "colorette": "^2.0.16", + "connect-history-api-fallback": "^1.6.0", + "consola": "^2.15.3", + "dotenv": "^16.0.0", + "dotenv-expand": "^8.0.2", + "ejs": "^3.1.6", + "fast-glob": "^3.2.11", + "fs-extra": "^10.0.1", + "html-minifier-terser": "^6.1.0", + "node-html-parser": "^5.3.3", + "pathe": "^0.2.0" + }, + "peerDependencies": { + "vite": ">=2.0.0" + } + }, + "node_modules/vite-plugin-html/node_modules/@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/vite-plugin-html/node_modules/consola": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", + "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", + "dev": true + }, + "node_modules/vite-plugin-html/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-plugin-html/node_modules/pathe": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-0.2.0.tgz", + "integrity": "sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw==", + "dev": true + }, + "node_modules/vite-plugin-html/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/vite-plugin-inspect": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/vite-plugin-inspect/-/vite-plugin-inspect-0.8.9.tgz", + "integrity": "sha512-22/8qn+LYonzibb1VeFZmISdVao5kC22jmEKm24vfFE8siEn47EpVcCLYMv6iKOYMJfjSvSJfueOwcFCkUnV3A==", + "dev": true, + "dependencies": { + "@antfu/utils": "^0.7.10", + "@rollup/pluginutils": "^5.1.3", + "debug": "^4.3.7", + "error-stack-parser-es": "^0.1.5", + "fs-extra": "^11.2.0", + "open": "^10.1.0", + "perfect-debounce": "^1.0.0", + "picocolors": "^1.1.1", + "sirv": "^3.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.1" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + } + } + }, + "node_modules/vite-plugin-vue-devtools": { + "version": "7.7.6", + "resolved": "https://registry.npmjs.org/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-7.7.6.tgz", + "integrity": "sha512-L7nPVM5a7lgit/Z+36iwoqHOaP3wxqVi1UvaDJwGCfblS9Y6vNqf32ILlzJVH9c47aHu90BhDXeZc+rgzHRHcw==", + "dev": true, + "dependencies": { + "@vue/devtools-core": "^7.7.6", + "@vue/devtools-kit": "^7.7.6", + "@vue/devtools-shared": "^7.7.6", + "execa": "^9.5.2", + "sirv": "^3.0.1", + "vite-plugin-inspect": "0.8.9", + "vite-plugin-vue-inspector": "^5.3.1" + }, + "engines": { + "node": ">=v14.21.3" + }, + "peerDependencies": { + "vite": "^3.1.0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0" + } + }, + "node_modules/vite-plugin-vue-devtools/node_modules/execa": { + "version": "9.5.3", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.5.3.tgz", + "integrity": "sha512-QFNnTvU3UjgWFy8Ef9iDHvIdcgZ344ebkwYx4/KLbR+CKQA4xBaHzv+iRpp86QfMHP8faFQLh8iOc57215y4Rg==", + "dev": true, + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.3", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.0", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/vite-plugin-vue-devtools/node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vite-plugin-vue-devtools/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vite-plugin-vue-devtools/node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/vite-plugin-vue-devtools/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vite-plugin-vue-devtools/node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vite-plugin-vue-devtools/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vite-plugin-vue-devtools/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/vite-plugin-vue-devtools/node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vite-plugin-vue-inspector": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-5.3.1.tgz", + "integrity": "sha512-cBk172kZKTdvGpJuzCCLg8lJ909wopwsu3Ve9FsL1XsnLBiRT9U3MePcqrgGHgCX2ZgkqZmAGR8taxw+TV6s7A==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.0", + "@babel/plugin-proposal-decorators": "^7.23.0", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-transform-typescript": "^7.22.15", + "@vue/babel-plugin-jsx": "^1.1.5", + "@vue/compiler-dom": "^3.3.4", + "kolorist": "^1.8.0", + "magic-string": "^0.30.4" + }, + "peerDependencies": { + "vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0" + } + }, "node_modules/vitest": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.0.tgz", @@ -14258,6 +15693,12 @@ "node": ">=10" } }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, "node_modules/yaml": { "version": "2.4.5", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz", @@ -14344,6 +15785,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz", + "integrity": "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/yoga-wasm-web": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz", diff --git a/package.json b/package.json index 35ae62f14..686a91389 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@comfyorg/comfyui-frontend", "private": true, - "version": "1.18.6", + "version": "1.21.2", "type": "module", "repository": "https://github.com/Comfy-Org/ComfyUI_frontend", "homepage": "https://comfy.org", @@ -63,6 +63,8 @@ "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", @@ -72,7 +74,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.0-1", + "@comfyorg/litegraph": "^0.15.14", "@primevue/forms": "^4.2.5", "@primevue/themes": "^4.2.5", "@sentry/vue": "^8.48.0", diff --git a/public/assets/favicon.ico b/public/assets/favicon.ico new file mode 100644 index 000000000..2e66ad0e4 Binary files /dev/null and b/public/assets/favicon.ico differ diff --git a/public/assets/images/Comfy_Logo_x32.png b/public/assets/images/Comfy_Logo_x32.png deleted file mode 100644 index a535a4dab..000000000 Binary files a/public/assets/images/Comfy_Logo_x32.png and /dev/null differ diff --git a/public/assets/images/comfy-logo-mono.svg b/public/assets/images/comfy-logo-mono.svg new file mode 100644 index 000000000..1dc4021d2 --- /dev/null +++ b/public/assets/images/comfy-logo-mono.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/assets/images/comfy-logo-single.svg b/public/assets/images/comfy-logo-single.svg new file mode 100644 index 000000000..482367dca --- /dev/null +++ b/public/assets/images/comfy-logo-single.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/assets/images/favicon_progress_16x16/frame_0.png b/public/assets/images/favicon_progress_16x16/frame_0.png new file mode 100644 index 000000000..15b27f5a6 Binary files /dev/null and b/public/assets/images/favicon_progress_16x16/frame_0.png differ diff --git a/public/assets/images/favicon_progress_16x16/frame_1.png b/public/assets/images/favicon_progress_16x16/frame_1.png new file mode 100644 index 000000000..f82c375c9 Binary files /dev/null and b/public/assets/images/favicon_progress_16x16/frame_1.png differ diff --git a/public/assets/images/favicon_progress_16x16/frame_2.png b/public/assets/images/favicon_progress_16x16/frame_2.png new file mode 100644 index 000000000..a75dcf838 Binary files /dev/null and b/public/assets/images/favicon_progress_16x16/frame_2.png differ diff --git a/public/assets/images/favicon_progress_16x16/frame_3.png b/public/assets/images/favicon_progress_16x16/frame_3.png new file mode 100644 index 000000000..a53315bba Binary files /dev/null and b/public/assets/images/favicon_progress_16x16/frame_3.png differ diff --git a/public/assets/images/favicon_progress_16x16/frame_4.png b/public/assets/images/favicon_progress_16x16/frame_4.png new file mode 100644 index 000000000..349c3a1c2 Binary files /dev/null and b/public/assets/images/favicon_progress_16x16/frame_4.png differ diff --git a/public/assets/images/favicon_progress_16x16/frame_5.png b/public/assets/images/favicon_progress_16x16/frame_5.png new file mode 100644 index 000000000..d48690744 Binary files /dev/null and b/public/assets/images/favicon_progress_16x16/frame_5.png differ diff --git a/public/assets/images/favicon_progress_16x16/frame_6.png b/public/assets/images/favicon_progress_16x16/frame_6.png new file mode 100644 index 000000000..71e26301f Binary files /dev/null and b/public/assets/images/favicon_progress_16x16/frame_6.png differ diff --git a/public/assets/images/favicon_progress_16x16/frame_7.png b/public/assets/images/favicon_progress_16x16/frame_7.png new file mode 100644 index 000000000..32964243a Binary files /dev/null and b/public/assets/images/favicon_progress_16x16/frame_7.png differ diff --git a/public/assets/images/favicon_progress_16x16/frame_8.png b/public/assets/images/favicon_progress_16x16/frame_8.png new file mode 100644 index 000000000..221c75a03 Binary files /dev/null and b/public/assets/images/favicon_progress_16x16/frame_8.png differ diff --git a/public/assets/images/favicon_progress_16x16/frame_9.png b/public/assets/images/favicon_progress_16x16/frame_9.png new file mode 100644 index 000000000..741abad60 Binary files /dev/null and b/public/assets/images/favicon_progress_16x16/frame_9.png differ diff --git a/public/user.css b/public/user.css deleted file mode 100644 index 8b1af3868..000000000 --- a/public/user.css +++ /dev/null @@ -1 +0,0 @@ -/* Put custom styles here */ \ No newline at end of file diff --git a/src/components/BrowserTabTitle.vue b/src/components/BrowserTabTitle.vue deleted file mode 100644 index 2f548c943..000000000 --- a/src/components/BrowserTabTitle.vue +++ /dev/null @@ -1,58 +0,0 @@ - - - diff --git a/src/components/actionbar/ComfyActionbar.vue b/src/components/actionbar/ComfyActionbar.vue index f046a3137..c9d75d009 100644 --- a/src/components/actionbar/ComfyActionbar.vue +++ b/src/components/actionbar/ComfyActionbar.vue @@ -63,15 +63,6 @@ watchDebounced( // Set initial position to bottom center const setInitialPosition = () => { - if (x.value !== 0 || y.value !== 0) { - return - } - if (storedPosition.value.x !== 0 || storedPosition.value.y !== 0) { - x.value = storedPosition.value.x - y.value = storedPosition.value.y - captureLastDragState() - return - } if (panelRef.value) { const screenWidth = window.innerWidth const screenHeight = window.innerHeight @@ -82,9 +73,25 @@ const setInitialPosition = () => { return } - x.value = (screenWidth - menuWidth) / 2 - y.value = screenHeight - menuHeight - 10 // 10px margin from bottom - captureLastDragState() + // Check if stored position exists and is within bounds + if (storedPosition.value.x !== 0 || storedPosition.value.y !== 0) { + // Ensure stored position is within screen bounds + x.value = clamp(storedPosition.value.x, 0, screenWidth - menuWidth) + y.value = clamp(storedPosition.value.y, 0, screenHeight - menuHeight) + captureLastDragState() + return + } + + // If no stored position or current position, set to bottom center + if (x.value === 0 && y.value === 0) { + x.value = clamp((screenWidth - menuWidth) / 2, 0, screenWidth - menuWidth) + y.value = clamp( + screenHeight - menuHeight - 10, + 0, + screenHeight - menuHeight + ) + captureLastDragState() + } } } onMounted(setInitialPosition) diff --git a/src/components/common/BackgroundImageUpload.vue b/src/components/common/BackgroundImageUpload.vue new file mode 100644 index 000000000..7c20cbab1 --- /dev/null +++ b/src/components/common/BackgroundImageUpload.vue @@ -0,0 +1,103 @@ + + + diff --git a/src/components/common/FileDownload.vue b/src/components/common/FileDownload.vue index 44e0d3d3b..f54c2dc8e 100644 --- a/src/components/common/FileDownload.vue +++ b/src/components/common/FileDownload.vue @@ -30,6 +30,15 @@ @click="download.triggerBrowserDownload" /> +
+
@@ -38,6 +47,7 @@ import Button from 'primevue/button' import Message from 'primevue/message' import { computed } from 'vue' +import { useCopyToClipboard } from '@/composables/useCopyToClipboard' import { useDownload } from '@/composables/useDownload' import { formatSize } from '@/utils/formatUtil' @@ -49,9 +59,15 @@ const props = defineProps<{ }>() const label = computed(() => props.label || props.url.split('/').pop()) + const hint = computed(() => props.hint || props.url) const download = useDownload(props.url) const fileSize = computed(() => download.fileSize.value ? formatSize(download.fileSize.value) : '?' ) +const copyURL = async () => { + await copyToClipboard(props.url) +} + +const { copyToClipboard } = useCopyToClipboard() diff --git a/src/components/common/FormItem.vue b/src/components/common/FormItem.vue index c57a2df76..4de5ddd4c 100644 --- a/src/components/common/FormItem.vue +++ b/src/components/common/FormItem.vue @@ -36,6 +36,7 @@ import Select from 'primevue/select' import ToggleSwitch from 'primevue/toggleswitch' import { type Component, markRaw } from 'vue' +import BackgroundImageUpload from '@/components/common/BackgroundImageUpload.vue' import CustomFormValue from '@/components/common/CustomFormValue.vue' import FormColorPicker from '@/components/common/FormColorPicker.vue' import FormImageUpload from '@/components/common/FormImageUpload.vue' @@ -102,6 +103,8 @@ function getFormComponent(item: FormItem): Component { return FormColorPicker case 'url': return UrlInput + case 'backgroundImage': + return BackgroundImageUpload default: return InputText } diff --git a/src/components/common/NoResultsPlaceholder.vue b/src/components/common/NoResultsPlaceholder.vue index 26277f88d..fbf8ca83d 100644 --- a/src/components/common/NoResultsPlaceholder.vue +++ b/src/components/common/NoResultsPlaceholder.vue @@ -5,7 +5,7 @@

{{ title }}

-

+

{{ message }}

@@ -42,6 +42,7 @@ import { computed } from 'vue' import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue' import { useDialogService } from '@/services/dialogService' +import { useAboutPanelStore } from '@/stores/aboutPanelStore' import type { MissingNodeType } from '@/types/comfy' import { ManagerTab } from '@/types/comfyManagerTypes' @@ -49,6 +50,19 @@ const props = defineProps<{ missingNodeTypes: MissingNodeType[] }>() +const aboutPanelStore = useAboutPanelStore() + +// Determines if ComfyUI-Manager is installed by checking for its badge in the about panel +// This allows us to conditionally show the Manager button only when the extension is available +// TODO: Remove this check when Manager functionality is fully migrated into core +const isManagerInstalled = computed(() => { + return aboutPanelStore.badges.some( + (badge) => + badge.label.includes('ComfyUI-Manager') || + badge.url.includes('ComfyUI-Manager') + ) +}) + const uniqueNodes = computed(() => { const seenTypes = new Set() return props.missingNodeTypes diff --git a/src/components/dialog/content/SettingDialogContent.vue b/src/components/dialog/content/SettingDialogContent.vue index b6e0e504c..fc22316cd 100644 --- a/src/components/dialog/content/SettingDialogContent.vue +++ b/src/components/dialog/content/SettingDialogContent.vue @@ -67,9 +67,9 @@ import Tabs from 'primevue/tabs' import { computed, watch } from 'vue' import SearchBox from '@/components/common/SearchBox.vue' +import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions' import { useSettingSearch } from '@/composables/setting/useSettingSearch' import { useSettingUI } from '@/composables/setting/useSettingUI' -import { useFirebaseAuthService } from '@/services/firebaseAuthService' import { SettingTreeNode } from '@/stores/settingStore' import { ISettingGroup, SettingParams } from '@/types/settingTypes' import { flattenTree } from '@/utils/treeUtil' @@ -107,7 +107,7 @@ const { getSearchResults } = useSettingSearch() -const authService = useFirebaseAuthService() +const authActions = useFirebaseAuthActions() // Sort groups for a category const sortedGroups = (category: SettingTreeNode): ISettingGroup[] => { @@ -140,7 +140,7 @@ watch(activeCategory, (_, oldValue) => { activeCategory.value = oldValue } if (activeCategory.value?.key === 'credits') { - void authService.fetchBalance() + void authActions.fetchBalance() } }) diff --git a/src/components/dialog/content/SignInContent.vue b/src/components/dialog/content/SignInContent.vue index 9501531d7..05303ce69 100644 --- a/src/components/dialog/content/SignInContent.vue +++ b/src/components/dialog/content/SignInContent.vue @@ -1,95 +1,141 @@ @@ -97,13 +143,15 @@ import Button from 'primevue/button' import Divider from 'primevue/divider' import Message from 'primevue/message' -import { onMounted, ref } from 'vue' +import { onMounted, onUnmounted, ref } from 'vue' import { useI18n } from 'vue-i18n' +import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions' +import { COMFY_PLATFORM_BASE_URL } from '@/config/comfyApi' import { SignInData, SignUpData } from '@/schemas/signInSchema' -import { useFirebaseAuthService } from '@/services/firebaseAuthService' import { isInChina } from '@/utils/networkUtil' +import ApiKeyForm from './signin/ApiKeyForm.vue' import SignInForm from './signin/SignInForm.vue' import SignUpForm from './signin/SignUpForm.vue' @@ -112,33 +160,36 @@ const { onSuccess } = defineProps<{ }>() const { t } = useI18n() -const authService = useFirebaseAuthService() +const authActions = useFirebaseAuthActions() const isSecureContext = window.isSecureContext const isSignIn = ref(true) +const showApiKeyForm = ref(false) + const toggleState = () => { isSignIn.value = !isSignIn.value + showApiKeyForm.value = false } const signInWithGoogle = async () => { - if (await authService.signInWithGoogle()) { + if (await authActions.signInWithGoogle()) { onSuccess() } } const signInWithGithub = async () => { - if (await authService.signInWithGithub()) { + if (await authActions.signInWithGithub()) { onSuccess() } } const signInWithEmail = async (values: SignInData) => { - if (await authService.signInWithEmail(values.email, values.password)) { + if (await authActions.signInWithEmail(values.email, values.password)) { onSuccess() } } const signUpWithEmail = async (values: SignUpData) => { - if (await authService.signUpWithEmail(values.email, values.password)) { + if (await authActions.signUpWithEmail(values.email, values.password)) { onSuccess() } } @@ -147,4 +198,8 @@ const userIsInChina = ref(false) onMounted(async () => { userIsInChina.value = await isInChina() }) + +onUnmounted(() => { + authActions.accessError.value = false +}) diff --git a/src/components/dialog/content/TopUpCreditsDialogContent.vue b/src/components/dialog/content/TopUpCreditsDialogContent.vue index 11fd1505b..d15b75289 100644 --- a/src/components/dialog/content/TopUpCreditsDialogContent.vue +++ b/src/components/dialog/content/TopUpCreditsDialogContent.vue @@ -51,7 +51,7 @@ import Button from 'primevue/button' import UserCredit from '@/components/common/UserCredit.vue' -import { useFirebaseAuthService } from '@/services/firebaseAuthService' +import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions' import CreditTopUpOption from './credit/CreditTopUpOption.vue' @@ -65,9 +65,9 @@ const { preselectedAmountOption?: number }>() -const authService = useFirebaseAuthService() +const authActions = useFirebaseAuthActions() const handleSeeDetails = async () => { - await authService.accessBillingPortal() + await authActions.accessBillingPortal() } diff --git a/src/components/dialog/content/UpdatePasswordContent.vue b/src/components/dialog/content/UpdatePasswordContent.vue index 470a56725..55611c00c 100644 --- a/src/components/dialog/content/UpdatePasswordContent.vue +++ b/src/components/dialog/content/UpdatePasswordContent.vue @@ -23,10 +23,10 @@ import Button from 'primevue/button' import { ref } from 'vue' import PasswordFields from '@/components/dialog/content/signin/PasswordFields.vue' +import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions' import { updatePasswordSchema } from '@/schemas/signInSchema' -import { useFirebaseAuthService } from '@/services/firebaseAuthService' -const authService = useFirebaseAuthService() +const authActions = useFirebaseAuthActions() const loading = ref(false) const { onSuccess } = defineProps<{ @@ -37,7 +37,7 @@ const onSubmit = async (event: FormSubmitEvent) => { if (event.valid) { loading.value = true try { - await authService.updatePassword(event.values.password) + await authActions.updatePassword(event.values.password) onSuccess() } finally { loading.value = false diff --git a/src/components/dialog/content/credit/CreditTopUpOption.vue b/src/components/dialog/content/credit/CreditTopUpOption.vue index 1f2719ca1..95ea29fc2 100644 --- a/src/components/dialog/content/credit/CreditTopUpOption.vue +++ b/src/components/dialog/content/credit/CreditTopUpOption.vue @@ -41,9 +41,9 @@ import ProgressSpinner from 'primevue/progressspinner' import Tag from 'primevue/tag' import { onBeforeUnmount, ref } from 'vue' -import { useFirebaseAuthService } from '@/services/firebaseAuthService' +import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions' -const authService = useFirebaseAuthService() +const authActions = useFirebaseAuthActions() const { amount, @@ -61,7 +61,7 @@ const loading = ref(false) const handleBuyNow = async () => { loading.value = true - await authService.purchaseCredits(editable ? customAmount.value : amount) + await authActions.purchaseCredits(editable ? customAmount.value : amount) loading.value = false didClickBuyNow.value = true } @@ -69,7 +69,7 @@ const handleBuyNow = async () => { onBeforeUnmount(() => { if (didClickBuyNow.value) { // If clicked buy now, then returned back to the dialog and closed, fetch the balance - void authService.fetchBalance() + void authActions.fetchBalance() } }) diff --git a/src/components/dialog/content/error/ReportIssuePanel.vue b/src/components/dialog/content/error/ReportIssuePanel.vue index 79bc5aa53..b6fc5748a 100644 --- a/src/components/dialog/content/error/ReportIssuePanel.vue +++ b/src/components/dialog/content/error/ReportIssuePanel.vue @@ -124,12 +124,16 @@ :aria-label="$t('issueReport.provideAdditionalDetails')" /> - {{ t('issueReport.validation.maxLength') }} + {{ + $field.value + ? t('issueReport.validation.maxLength') + : t('issueReport.validation.descriptionRequired') + }} diff --git a/src/components/dialog/content/manager/ManagerDialogContent.vue b/src/components/dialog/content/manager/ManagerDialogContent.vue index 2b7f86310..f2fe93982 100644 --- a/src/components/dialog/content/manager/ManagerDialogContent.vue +++ b/src/components/dialog/content/manager/ManagerDialogContent.vue @@ -55,6 +55,7 @@ />
packs.filter((pack) => !comfyManagerStore.isPackInstalled(pack.id)) +whenever(selectedTab, () => { + pageNumber.value = 0 +}) + const isUpdateAvailableTab = computed( () => selectedTab.value?.id === ManagerTab.UpdateAvailable ) @@ -419,6 +424,17 @@ whenever(selectedNodePack, async () => { } }) +let gridContainer: HTMLElement | null = null +onMounted(() => { + gridContainer = document.getElementById('results-grid') +}) +watch(searchQuery, () => { + gridContainer ??= document.getElementById('results-grid') + if (gridContainer) { + gridContainer.scrollTop = 0 + } +}) + onUnmounted(() => { getPackById.cancel() }) diff --git a/src/components/dialog/content/manager/registrySearchBar/RegistrySearchBar.vue b/src/components/dialog/content/manager/registrySearchBar/RegistrySearchBar.vue index 9fd0cfa3c..37a748161 100644 --- a/src/components/dialog/content/manager/registrySearchBar/RegistrySearchBar.vue +++ b/src/components/dialog/content/manager/registrySearchBar/RegistrySearchBar.vue @@ -20,6 +20,7 @@ style: 'display: none' } }" + :show-empty-message="false" @complete="stubTrue" @option-select="onOptionSelect" /> diff --git a/src/components/dialog/content/setting/CreditsPanel.vue b/src/components/dialog/content/setting/CreditsPanel.vue index 31aa4c851..ed83ef9c0 100644 --- a/src/components/dialog/content/setting/CreditsPanel.vue +++ b/src/components/dialog/content/setting/CreditsPanel.vue @@ -36,7 +36,7 @@ text size="small" severity="secondary" - @click="() => authService.fetchBalance()" + @click="() => authActions.fetchBalance()" />
@@ -112,8 +112,8 @@ import { computed, ref } from 'vue' import { useI18n } from 'vue-i18n' import UserCredit from '@/components/common/UserCredit.vue' +import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions' import { useDialogService } from '@/services/dialogService' -import { useFirebaseAuthService } from '@/services/firebaseAuthService' import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore' import { formatMetronomeCurrency } from '@/utils/formatUtil' @@ -127,7 +127,7 @@ interface CreditHistoryItemData { const { t } = useI18n() const dialogService = useDialogService() const authStore = useFirebaseAuthStore() -const authService = useFirebaseAuthService() +const authActions = useFirebaseAuthActions() const loading = computed(() => authStore.loading) const balanceLoading = computed(() => authStore.isFetchingBalance) @@ -142,7 +142,7 @@ const handlePurchaseCreditsClick = () => { } const handleCreditsHistoryClick = async () => { - await authService.accessBillingPortal() + await authActions.accessBillingPortal() } const handleMessageSupport = () => { diff --git a/src/components/dialog/content/setting/SettingItem.spec.ts b/src/components/dialog/content/setting/SettingItem.spec.ts index 22ab1e4df..9020c5417 100644 --- a/src/components/dialog/content/setting/SettingItem.spec.ts +++ b/src/components/dialog/content/setting/SettingItem.spec.ts @@ -1,6 +1,8 @@ import { mount } from '@vue/test-utils' import { createPinia } from 'pinia' import PrimeVue from 'primevue/config' +import Tag from 'primevue/tag' +import Tooltip from 'primevue/tooltip' import { describe, expect, it, vi } from 'vitest' import { createI18n } from 'vue-i18n' @@ -19,7 +21,16 @@ describe('SettingItem', () => { const mountComponent = (props: any, options = {}): any => { return mount(SettingItem, { global: { - plugins: [PrimeVue, i18n, createPinia()] + plugins: [PrimeVue, i18n, createPinia()], + components: { + Tag + }, + directives: { + tooltip: Tooltip + }, + stubs: { + 'i-material-symbols:experiment-outline': true + } }, props, ...options diff --git a/src/components/dialog/content/setting/UserPanel.vue b/src/components/dialog/content/setting/UserPanel.vue index 2529261d7..147625b32 100644 --- a/src/components/dialog/content/setting/UserPanel.vue +++ b/src/components/dialog/content/setting/UserPanel.vue @@ -4,13 +4,13 @@

{{ $t('userSettings.title') }}

-
- +
+
@@ -18,7 +18,7 @@ {{ $t('userSettings.name') }}
- {{ user.displayName || $t('userSettings.notSet') }} + {{ userDisplayName || $t('userSettings.notSet') }}
@@ -26,9 +26,9 @@

{{ $t('userSettings.email') }}

- - {{ user.email }} - + + {{ userEmail }} +
@@ -87,55 +87,26 @@ diff --git a/src/components/dialog/content/signin/ApiKeyForm.test.ts b/src/components/dialog/content/signin/ApiKeyForm.test.ts new file mode 100644 index 000000000..bf1ec2cdd --- /dev/null +++ b/src/components/dialog/content/signin/ApiKeyForm.test.ts @@ -0,0 +1,117 @@ +import { Form } from '@primevue/forms' +import { mount } from '@vue/test-utils' +import { createPinia } from 'pinia' +import Button from 'primevue/button' +import PrimeVue from 'primevue/config' +import InputText from 'primevue/inputtext' +import Message from 'primevue/message' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createApp } from 'vue' +import { createI18n } from 'vue-i18n' + +import { COMFY_PLATFORM_BASE_URL } from '@/config/comfyApi' + +import ApiKeyForm from './ApiKeyForm.vue' + +const mockStoreApiKey = vi.fn() +const mockLoading = vi.fn(() => false) + +vi.mock('@/stores/firebaseAuthStore', () => ({ + useFirebaseAuthStore: vi.fn(() => ({ + loading: mockLoading() + })) +})) + +vi.mock('@/stores/apiKeyAuthStore', () => ({ + useApiKeyAuthStore: vi.fn(() => ({ + storeApiKey: mockStoreApiKey + })) +})) + +const i18n = createI18n({ + legacy: false, + locale: 'en', + messages: { + en: { + auth: { + apiKey: { + title: 'API Key', + label: 'API Key', + placeholder: 'Enter your API Key', + error: 'Invalid API Key', + helpText: 'Need an API key?', + generateKey: 'Get one here', + whitelistInfo: 'About non-whitelisted sites', + description: 'Use your Comfy API key to enable API Nodes' + } + }, + g: { + back: 'Back', + save: 'Save', + learnMore: 'Learn more' + } + } + } +}) + +describe('ApiKeyForm', () => { + beforeEach(() => { + const app = createApp({}) + app.use(PrimeVue) + vi.clearAllMocks() + mockStoreApiKey.mockReset() + mockLoading.mockReset() + }) + + const mountComponent = (props: any = {}) => { + return mount(ApiKeyForm, { + global: { + plugins: [PrimeVue, createPinia(), i18n], + components: { Button, Form, InputText, Message } + }, + props + }) + } + + it('renders correctly with all required elements', () => { + const wrapper = mountComponent() + + expect(wrapper.find('h1').text()).toBe('API Key') + expect(wrapper.find('label').text()).toBe('API Key') + expect(wrapper.findComponent(InputText).exists()).toBe(true) + expect(wrapper.findComponent(Button).exists()).toBe(true) + }) + + it('emits back event when back button is clicked', async () => { + const wrapper = mountComponent() + + await wrapper.findComponent(Button).trigger('click') + expect(wrapper.emitted('back')).toBeTruthy() + }) + + it('shows loading state when submitting', async () => { + mockLoading.mockReturnValue(true) + const wrapper = mountComponent() + const input = wrapper.findComponent(InputText) + + await input.setValue( + 'comfyui-123456789012345678901234567890123456789012345678901234567890123456789012' + ) + await wrapper.find('form').trigger('submit') + + const submitButton = wrapper + .findAllComponents(Button) + .find((btn) => btn.text() === 'Save') + expect(submitButton?.props('loading')).toBe(true) + }) + + it('displays help text and links correctly', () => { + const wrapper = mountComponent() + + const helpText = wrapper.find('small') + expect(helpText.text()).toContain('Need an API key?') + expect(helpText.find('a').attributes('href')).toBe( + `${COMFY_PLATFORM_BASE_URL}/login` + ) + }) +}) diff --git a/src/components/dialog/content/signin/ApiKeyForm.vue b/src/components/dialog/content/signin/ApiKeyForm.vue new file mode 100644 index 000000000..01e2e0a3f --- /dev/null +++ b/src/components/dialog/content/signin/ApiKeyForm.vue @@ -0,0 +1,112 @@ + + + diff --git a/src/components/dialog/content/signin/SignInForm.spec.ts b/src/components/dialog/content/signin/SignInForm.spec.ts new file mode 100644 index 000000000..1e5b6304e --- /dev/null +++ b/src/components/dialog/content/signin/SignInForm.spec.ts @@ -0,0 +1,293 @@ +import { Form } from '@primevue/forms' +import { VueWrapper, mount } from '@vue/test-utils' +import Button from 'primevue/button' +import PrimeVue from 'primevue/config' +import InputText from 'primevue/inputtext' +import Password from 'primevue/password' +import ProgressSpinner from 'primevue/progressspinner' +import ToastService from 'primevue/toastservice' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { nextTick } from 'vue' +import { createI18n } from 'vue-i18n' + +import enMessages from '@/locales/en/main.json' + +import SignInForm from './SignInForm.vue' + +type ComponentInstance = InstanceType + +// Mock firebase auth modules +vi.mock('firebase/app', () => ({ + initializeApp: vi.fn(), + getApp: vi.fn() +})) + +vi.mock('firebase/auth', () => ({ + getAuth: vi.fn(), + setPersistence: vi.fn(), + browserLocalPersistence: {}, + onAuthStateChanged: vi.fn(), + signInWithEmailAndPassword: vi.fn(), + signOut: vi.fn(), + sendPasswordResetEmail: vi.fn() +})) + +// Mock the auth composables and stores +const mockSendPasswordReset = vi.fn() +vi.mock('@/composables/auth/useFirebaseAuthActions', () => ({ + useFirebaseAuthActions: vi.fn(() => ({ + sendPasswordReset: mockSendPasswordReset + })) +})) + +let mockLoading = false +vi.mock('@/stores/firebaseAuthStore', () => ({ + useFirebaseAuthStore: vi.fn(() => ({ + get loading() { + return mockLoading + } + })) +})) + +// Mock toast +const mockToastAdd = vi.fn() +vi.mock('primevue/usetoast', () => ({ + useToast: vi.fn(() => ({ + add: mockToastAdd + })) +})) + +describe('SignInForm', () => { + beforeEach(() => { + vi.clearAllMocks() + mockSendPasswordReset.mockReset() + mockToastAdd.mockReset() + mockLoading = false + }) + + const mountComponent = ( + props = {}, + options = {} + ): VueWrapper => { + const i18n = createI18n({ + legacy: false, + locale: 'en', + messages: { en: enMessages } + }) + + return mount(SignInForm, { + global: { + plugins: [PrimeVue, i18n, ToastService], + components: { + Form, + Button, + InputText, + Password, + ProgressSpinner + } + }, + props, + ...options + }) + } + + describe('Forgot Password Link', () => { + it('shows disabled style when email is empty', async () => { + const wrapper = mountComponent() + await nextTick() + + const forgotPasswordSpan = wrapper.find( + 'span.text-muted.text-base.font-medium.cursor-pointer' + ) + + expect(forgotPasswordSpan.classes()).toContain('text-link-disabled') + }) + + it('shows toast and focuses email input when clicked while disabled', async () => { + const wrapper = mountComponent() + const forgotPasswordSpan = wrapper.find( + 'span.text-muted.text-base.font-medium.cursor-pointer' + ) + + // Mock getElementById to track focus + const mockFocus = vi.fn() + const mockElement = { focus: mockFocus } + vi.spyOn(document, 'getElementById').mockReturnValue(mockElement as any) + + // Click forgot password link while email is empty + await forgotPasswordSpan.trigger('click') + await nextTick() + + // Should show toast warning + expect(mockToastAdd).toHaveBeenCalledWith({ + severity: 'warn', + summary: enMessages.auth.login.emailPlaceholder, + life: 5000 + }) + + // Should focus email input + expect(document.getElementById).toHaveBeenCalledWith( + 'comfy-org-sign-in-email' + ) + expect(mockFocus).toHaveBeenCalled() + + // Should NOT call sendPasswordReset + expect(mockSendPasswordReset).not.toHaveBeenCalled() + }) + + it('calls handleForgotPassword with email when link is clicked', async () => { + const wrapper = mountComponent() + const component = wrapper.vm as any + + // Spy on handleForgotPassword + const handleForgotPasswordSpy = vi.spyOn( + component, + 'handleForgotPassword' + ) + + const forgotPasswordSpan = wrapper.find( + 'span.text-muted.text-base.font-medium.cursor-pointer' + ) + + // Click the forgot password link + await forgotPasswordSpan.trigger('click') + + // Should call handleForgotPassword + expect(handleForgotPasswordSpy).toHaveBeenCalled() + }) + }) + + describe('Form Submission', () => { + it('emits submit event when onSubmit is called with valid data', async () => { + const wrapper = mountComponent() + const component = wrapper.vm as any + + // Call onSubmit directly with valid data + component.onSubmit({ + valid: true, + values: { email: 'test@example.com', password: 'password123' } + }) + + // Check emitted event + expect(wrapper.emitted('submit')).toBeTruthy() + expect(wrapper.emitted('submit')?.[0]).toEqual([ + { + email: 'test@example.com', + password: 'password123' + } + ]) + }) + + it('does not emit submit event when form is invalid', async () => { + const wrapper = mountComponent() + const component = wrapper.vm as any + + // Call onSubmit with invalid form + component.onSubmit({ valid: false, values: {} }) + + // Should not emit submit event + expect(wrapper.emitted('submit')).toBeFalsy() + }) + }) + + describe('Loading State', () => { + it('shows spinner when loading', async () => { + mockLoading = true + + try { + const wrapper = mountComponent() + await nextTick() + + expect(wrapper.findComponent(ProgressSpinner).exists()).toBe(true) + expect(wrapper.findComponent(Button).exists()).toBe(false) + } catch (error) { + // Fallback test - check HTML content if component rendering fails + mockLoading = true + const wrapper = mountComponent() + expect(wrapper.html()).toContain('p-progressspinner') + expect(wrapper.html()).not.toContain(' { + mockLoading = false + + const wrapper = mountComponent() + + expect(wrapper.findComponent(ProgressSpinner).exists()).toBe(false) + expect(wrapper.findComponent(Button).exists()).toBe(true) + }) + }) + + describe('Component Structure', () => { + it('renders email input with correct attributes', () => { + const wrapper = mountComponent() + const emailInput = wrapper.findComponent(InputText) + + expect(emailInput.attributes('id')).toBe('comfy-org-sign-in-email') + expect(emailInput.attributes('autocomplete')).toBe('email') + expect(emailInput.attributes('name')).toBe('email') + expect(emailInput.attributes('type')).toBe('text') + }) + + it('renders password input with correct attributes', () => { + const wrapper = mountComponent() + const passwordInput = wrapper.findComponent(Password) + + // Check props instead of attributes for Password component + expect(passwordInput.props('inputId')).toBe('comfy-org-sign-in-password') + // Password component passes name as prop, not attribute + expect(passwordInput.props('name')).toBe('password') + expect(passwordInput.props('feedback')).toBe(false) + expect(passwordInput.props('toggleMask')).toBe(true) + }) + + it('renders form with correct resolver', () => { + const wrapper = mountComponent() + const form = wrapper.findComponent(Form) + + expect(form.props('resolver')).toBeDefined() + }) + }) + + describe('Focus Behavior', () => { + it('focuses email input when handleForgotPassword is called with invalid email', async () => { + const wrapper = mountComponent() + const component = wrapper.vm as any + + // Mock getElementById to track focus + const mockFocus = vi.fn() + const mockElement = { focus: mockFocus } + vi.spyOn(document, 'getElementById').mockReturnValue(mockElement as any) + + // Call handleForgotPassword with no email + await component.handleForgotPassword('', false) + + // Should focus email input + expect(document.getElementById).toHaveBeenCalledWith( + 'comfy-org-sign-in-email' + ) + expect(mockFocus).toHaveBeenCalled() + }) + + it('does not focus email input when valid email is provided', async () => { + const wrapper = mountComponent() + const component = wrapper.vm as any + + // Mock getElementById + const mockFocus = vi.fn() + const mockElement = { focus: mockFocus } + vi.spyOn(document, 'getElementById').mockReturnValue(mockElement as any) + + // Call handleForgotPassword with valid email + await component.handleForgotPassword('test@example.com', true) + + // Should NOT focus email input + expect(document.getElementById).not.toHaveBeenCalled() + expect(mockFocus).not.toHaveBeenCalled() + + // Should call sendPasswordReset + expect(mockSendPasswordReset).toHaveBeenCalledWith('test@example.com') + }) + }) +}) diff --git a/src/components/dialog/content/signin/SignInForm.vue b/src/components/dialog/content/signin/SignInForm.vue index 4881970fd..72ac23bc7 100644 --- a/src/components/dialog/content/signin/SignInForm.vue +++ b/src/components/dialog/content/signin/SignInForm.vue @@ -7,15 +7,12 @@ >
-