diff --git a/.github/actions/start-comfyui-server/action.yml b/.github/actions/start-comfyui-server/action.yml new file mode 100644 index 000000000..2af727508 --- /dev/null +++ b/.github/actions/start-comfyui-server/action.yml @@ -0,0 +1,23 @@ +name: Start ComfyUI Server +description: 'Start ComfyUI server in a container environment (assumes ComfyUI is pre-installed)' + +inputs: + front_end_root: + description: 'Path to frontend dist directory' + required: false + default: '$GITHUB_WORKSPACE/dist' + timeout: + description: 'Timeout in seconds for server startup' + required: false + default: '600' + +runs: + using: 'composite' + steps: + - name: Copy devtools and start server + shell: bash + run: | + set -euo pipefail + cp -r ./tools/devtools/* /ComfyUI/custom_nodes/ComfyUI_devtools/ + cd /ComfyUI && python3 main.py --cpu --multi-user --front-end-root "${{ inputs.front_end_root }}" & + wait-for-it --service 127.0.0.1:8188 -t ${{ inputs.timeout }} diff --git a/.github/workflows/ci-tests-e2e-forks.yaml b/.github/workflows/ci-tests-e2e-forks.yaml index c1828b7fb..8f039f1c4 100644 --- a/.github/workflows/ci-tests-e2e-forks.yaml +++ b/.github/workflows/ci-tests-e2e-forks.yaml @@ -1,9 +1,9 @@ # Description: Deploys test results from forked PRs (forks can't access deployment secrets) -name: "CI: Tests E2E (Deploy for Forks)" +name: 'CI: Tests E2E (Deploy for Forks)' on: workflow_run: - workflows: ["CI: Tests E2E"] + workflows: ['CI: Tests E2E'] types: [requested, completed] env: @@ -81,6 +81,7 @@ jobs: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} GITHUB_TOKEN: ${{ github.token }} + GITHUB_SHA: ${{ github.event.workflow_run.head_sha }} run: | # Rename merged report if exists [ -d "reports/playwright-report-chromium-merged" ] && \ diff --git a/.github/workflows/ci-tests-e2e.yaml b/.github/workflows/ci-tests-e2e.yaml index 5a2cc028b..90bf4112c 100644 --- a/.github/workflows/ci-tests-e2e.yaml +++ b/.github/workflows/ci-tests-e2e.yaml @@ -1,5 +1,5 @@ # Description: End-to-end testing with Playwright across multiple browsers, deploys test reports to Cloudflare Pages -name: "CI: Tests E2E" +name: 'CI: Tests E2E' on: push: @@ -15,66 +15,56 @@ concurrency: jobs: setup: runs-on: ubuntu-latest - outputs: - cache-key: ${{ steps.cache-key.outputs.key }} steps: - name: Checkout repository uses: actions/checkout@v5 - - # Setup Test Environment, build frontend but do not start server yet - - name: Setup ComfyUI server - uses: ./.github/actions/setup-comfyui-server - name: Setup frontend uses: ./.github/actions/setup-frontend with: include_build_step: true - - name: Setup Playwright - uses: ./.github/actions/setup-playwright # Setup Playwright and cache browsers - # Save the entire workspace as cache for later test jobs to restore - - name: Generate cache key - id: cache-key - run: echo "key=$(date +%s)" >> $GITHUB_OUTPUT - - name: Save cache - uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 + # Upload only built dist/ (containerized test jobs will pnpm install without cache) + - name: Upload built frontend + uses: actions/upload-artifact@v4 with: - path: . - key: comfyui-setup-${{ steps.cache-key.outputs.key }} + name: frontend-dist + path: dist/ + retention-days: 1 # Sharded chromium tests playwright-tests-chromium-sharded: needs: setup runs-on: ubuntu-latest timeout-minutes: 60 + container: + image: ghcr.io/comfy-org/comfyui-ci-container:0.0.10 + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} permissions: contents: read + packages: read strategy: fail-fast: false matrix: shardIndex: [1, 2, 3, 4, 5, 6, 7, 8] shardTotal: [8] steps: - # download built frontend repo from setup job - - name: Wait for cache propagation - run: sleep 10 - - name: Restore cached setup - uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 + - name: Checkout repository + uses: actions/checkout@v5 + - name: Download built frontend + uses: actions/download-artifact@v4 with: - fail-on-cache-miss: true - path: . - key: comfyui-setup-${{ needs.setup.outputs.cache-key }} + name: frontend-dist + path: dist/ - # Setup Test Environment for this runner, start server, use cached built frontend ./dist from 'setup' job - - name: Setup ComfyUI server - uses: ./.github/actions/setup-comfyui-server - with: - launch_server: true - - name: Setup nodejs, pnpm, reuse built frontend - uses: ./.github/actions/setup-frontend - - name: Setup Playwright - uses: ./.github/actions/setup-playwright + - name: Start ComfyUI server + uses: ./.github/actions/start-comfyui-server - # Run sharded tests and upload sharded reports + - name: Install frontend deps + run: pnpm install --frozen-lockfile + + # Run sharded tests (browsers pre-installed in container) - name: Run Playwright tests (Shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}) id: playwright run: pnpm exec playwright test --project=chromium --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --reporter=blob @@ -94,39 +84,37 @@ jobs: timeout-minutes: 15 needs: setup runs-on: ubuntu-latest + container: + image: ghcr.io/comfy-org/comfyui-ci-container:0.0.10 + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} permissions: contents: read + packages: read strategy: fail-fast: false matrix: browser: [chromium-2x, chromium-0.5x, mobile-chrome] steps: - # download built frontend repo from setup job - - name: Wait for cache propagation - run: sleep 10 - - name: Restore cached setup - uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 + - name: Checkout repository + uses: actions/checkout@v5 + - name: Download built frontend + uses: actions/download-artifact@v4 with: - fail-on-cache-miss: true - path: . - key: comfyui-setup-${{ needs.setup.outputs.cache-key }} + name: frontend-dist + path: dist/ - # Setup Test Environment for this runner, start server, use cached built frontend ./dist from 'setup' job - - name: Setup ComfyUI server - uses: ./.github/actions/setup-comfyui-server - with: - launch_server: true - - name: Setup nodejs, pnpm, reuse built frontend - uses: ./.github/actions/setup-frontend - - name: Setup Playwright - uses: ./.github/actions/setup-playwright + - name: Start ComfyUI server + uses: ./.github/actions/start-comfyui-server - # Run tests and upload reports + - name: Install frontend deps + run: pnpm install --frozen-lockfile + + # Run tests (browsers pre-installed in container) - name: Run Playwright tests (${{ matrix.browser }}) id: playwright - run: | - # Run tests with blob reporter first - pnpm exec playwright test --project=${{ matrix.browser }} --reporter=blob + run: pnpm exec playwright test --project=${{ matrix.browser }} --reporter=blob env: PLAYWRIGHT_BLOB_OUTPUT_DIR: ./blob-report @@ -147,7 +135,7 @@ jobs: path: ./playwright-report/ retention-days: 30 - # Merge sharded test reports + # Merge sharded test reports (no container needed - only runs CLI) merge-reports: needs: [playwright-tests-chromium-sharded] runs-on: ubuntu-latest @@ -156,11 +144,9 @@ jobs: - name: Checkout repository uses: actions/checkout@v5 - # Setup Test Environment, we only need playwright to merge reports + # Setup pnpm/node to run playwright merge-reports (no browsers needed) - name: Setup frontend uses: ./.github/actions/setup-frontend - - name: Setup Playwright - uses: ./.github/actions/setup-playwright - name: Download blob reports uses: actions/download-artifact@v4 @@ -236,6 +222,7 @@ jobs: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} GITHUB_TOKEN: ${{ github.token }} + GITHUB_SHA: ${{ github.event.pull_request.head.sha }} run: | bash ./scripts/cicd/pr-playwright-deploy-and-comment.sh \ "${{ github.event.pull_request.number }}" \ diff --git a/.github/workflows/pr-update-playwright-expectations.yaml b/.github/workflows/pr-update-playwright-expectations.yaml index d78ad96bf..0ca696721 100644 --- a/.github/workflows/pr-update-playwright-expectations.yaml +++ b/.github/workflows/pr-update-playwright-expectations.yaml @@ -25,7 +25,6 @@ jobs: ) && startsWith(github.event.comment.body, '/update-playwright') ) outputs: - cache-key: ${{ steps.cache-key.outputs.key }} pr-number: ${{ steps.pr-info.outputs.pr-number }} branch: ${{ steps.pr-info.outputs.branch }} comment-id: ${{ steps.find-update-comment.outputs.comment-id }} @@ -64,70 +63,63 @@ jobs: uses: ./.github/actions/setup-frontend with: include_build_step: true - # Save expensive build artifacts (Python env, built frontend, node_modules) - # Source code will be checked out fresh in sharded jobs - - name: Generate cache key - id: cache-key - run: echo "key=$(date +%s)" >> $GITHUB_OUTPUT - - name: Save cache - uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 + + # Upload built dist/ (containerized test jobs will pnpm install without cache) + - name: Upload built frontend + uses: actions/upload-artifact@v4 with: - path: | - ComfyUI - dist - key: comfyui-setup-${{ steps.cache-key.outputs.key }} + name: frontend-dist + path: dist/ + retention-days: 1 # Sharded snapshot updates update-snapshots-sharded: needs: setup runs-on: ubuntu-latest + container: + image: ghcr.io/comfy-org/comfyui-ci-container:0.0.10 + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + permissions: + contents: read + packages: read strategy: fail-fast: false matrix: shardIndex: [1, 2, 3, 4] shardTotal: [4] steps: - # Checkout source code fresh (not cached) - name: Checkout repository uses: actions/checkout@v5 with: ref: ${{ needs.setup.outputs.branch }} - - # Restore expensive build artifacts from setup job - - name: Restore cached artifacts - uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 + - name: Download built frontend + uses: actions/download-artifact@v4 with: - fail-on-cache-miss: true - path: | - ComfyUI - dist - key: comfyui-setup-${{ needs.setup.outputs.cache-key }} + name: frontend-dist + path: dist/ - - name: Setup ComfyUI server (from cache) - uses: ./.github/actions/setup-comfyui-server - with: - launch_server: true + - name: Start ComfyUI server + uses: ./.github/actions/start-comfyui-server - - name: Setup nodejs, pnpm, reuse built frontend - uses: ./.github/actions/setup-frontend + - name: Install frontend deps + run: pnpm install --frozen-lockfile - - name: Setup Playwright - uses: ./.github/actions/setup-playwright - - # Run sharded tests with snapshot updates + # Run sharded tests with snapshot updates (browsers pre-installed in container) - name: Update snapshots (Shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}) id: playwright-tests run: pnpm exec playwright test --update-snapshots --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} continue-on-error: true - # Identify and stage only changed snapshot files - name: Stage changed snapshot files id: changed-snapshots + shell: bash run: | set -euo pipefail - echo "==========================================" - echo "STAGING CHANGED SNAPSHOTS (Shard ${{ matrix.shardIndex }})" - echo "==========================================" + + # Configure git safe.directory for container environment + git config --global --add safe.directory "$(pwd)" # Get list of changed snapshot files (including untracked/new files) changed_files=$( ( @@ -136,43 +128,25 @@ jobs: ) | sort -u | grep -E '\-snapshots/' || true ) if [ -z "$changed_files" ]; then - echo "No snapshot changes in this shard" + echo "No snapshot changes in shard ${{ matrix.shardIndex }}" echo "has-changes=false" >> $GITHUB_OUTPUT exit 0 fi - echo "✓ Found changed files:" - echo "$changed_files" file_count=$(echo "$changed_files" | wc -l) - echo "Count: $file_count" + echo "Shard ${{ matrix.shardIndex }}: $file_count changed snapshot(s):" + echo "$changed_files" echo "has-changes=true" >> $GITHUB_OUTPUT - echo "" - # Create staging directory + # Copy changed files to staging directory mkdir -p /tmp/changed_snapshots_shard - - # Copy only changed files, preserving directory structure - # Strip 'browser_tests/' prefix to avoid double nesting - echo "Copying changed files to staging directory..." while IFS= read -r file; do - # Skip paths that no longer exist (e.g. deletions) - if [ ! -f "$file" ]; then - echo " → (skipped; not a file) $file" - continue - fi - # Remove 'browser_tests/' prefix + [ -f "$file" ] || continue file_without_prefix="${file#browser_tests/}" - # Create parent directories mkdir -p "/tmp/changed_snapshots_shard/$(dirname "$file_without_prefix")" - # Copy file cp "$file" "/tmp/changed_snapshots_shard/$file_without_prefix" - echo " → $file_without_prefix" done <<< "$changed_files" - echo "" - echo "Staged files for upload:" - find /tmp/changed_snapshots_shard -type f - # Upload ONLY the changed files from this shard - name: Upload changed snapshots uses: actions/upload-artifact@v4 @@ -213,9 +187,15 @@ jobs: echo "==========================================" echo "DOWNLOADED SNAPSHOT FILES" echo "==========================================" - find ./downloaded-snapshots -type f - echo "" - echo "Total files: $(find ./downloaded-snapshots -type f | wc -l)" + if [ -d "./downloaded-snapshots" ]; then + find ./downloaded-snapshots -type f + echo "" + echo "Total files: $(find ./downloaded-snapshots -type f | wc -l)" + else + echo "No snapshot artifacts downloaded (no changes in any shard)" + echo "" + echo "Total files: 0" + fi # Merge only the changed files into browser_tests - name: Merge changed snapshots @@ -226,6 +206,16 @@ jobs: echo "MERGING CHANGED SNAPSHOTS" echo "==========================================" + # Check if any artifacts were downloaded + if [ ! -d "./downloaded-snapshots" ]; then + echo "No snapshot artifacts to merge" + echo "==========================================" + echo "MERGE COMPLETE" + echo "==========================================" + echo "Shards merged: 0" + exit 0 + fi + # Verify target directory exists if [ ! -d "browser_tests" ]; then echo "::error::Target directory 'browser_tests' does not exist" diff --git a/.i18nrc.cjs b/.i18nrc.cjs index 53acf0546..86ce06eaa 100644 --- a/.i18nrc.cjs +++ b/.i18nrc.cjs @@ -6,11 +6,12 @@ const { defineConfig } = require('@lobehub/i18n-cli'); module.exports = defineConfig({ modelName: 'gpt-4.1', splitToken: 1024, + saveImmediately: true, entry: 'src/locales/en', entryLocale: 'en', output: 'src/locales', - outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar', 'tr', 'pt-BR'], - reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream. + outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar', 'tr', 'pt-BR', 'fa'], + reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream, Civitai, Hugging Face. 'latent' is the short form of 'latent space'. 'mask' is in the context of image processing. @@ -18,5 +19,11 @@ module.exports = defineConfig({ - For 'zh' locale: Use ONLY Simplified Chinese characters (简体中文). Common examples: 节点 (not 節點), 画布 (not 畫布), 图像 (not 圖像), 选择 (not 選擇), 减小 (not 減小). - For 'zh-TW' locale: Use ONLY Traditional Chinese characters (繁體中文) with Taiwan-specific terminology. - NEVER mix Simplified and Traditional Chinese characters within the same locale. + + IMPORTANT Persian Translation Guidelines: + - For 'fa' locale: Use formal Persian (فارسی رسمی) for professional tone throughout the UI. + - Keep commonly used technical terms in English when they are standard in Persian software (e.g., node, workflow). + - Use Arabic-Indic numerals (۰-۹) for numbers where appropriate. + - Maintain consistency with terminology used in Persian software and design applications. ` }); diff --git a/.storybook/main.ts b/.storybook/main.ts index 897094ade..5b7c126e9 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -7,7 +7,7 @@ import type { InlineConfig } from 'vite' const config: StorybookConfig = { stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], - addons: ['@storybook/addon-docs'], + addons: ['@storybook/addon-docs', '@storybook/addon-mcp'], framework: { name: '@storybook/vue3-vite', options: {} @@ -69,9 +69,32 @@ const config: StorybookConfig = { allowedHosts: true }, resolve: { - alias: { - '@': process.cwd() + '/src' - } + alias: [ + { + find: '@/composables/queue/useJobList', + replacement: process.cwd() + '/src/storybook/mocks/useJobList.ts' + }, + { + find: '@/composables/queue/useJobActions', + replacement: process.cwd() + '/src/storybook/mocks/useJobActions.ts' + }, + { + find: '@/utils/formatUtil', + replacement: + process.cwd() + + '/packages/shared-frontend-utils/src/formatUtil.ts' + }, + { + find: '@/utils/networkUtil', + replacement: + process.cwd() + + '/packages/shared-frontend-utils/src/networkUtil.ts' + }, + { + find: '@', + replacement: process.cwd() + '/src' + } + ] }, esbuild: { // Prevent minification of identifiers to preserve _sfc_main diff --git a/AGENTS.md b/AGENTS.md index af0ab12a5..743572be3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -63,6 +63,9 @@ The project uses **Nx** for build orchestration and task management - Imports: - sorted/grouped by plugin - run `pnpm format` before committing + - use separate `import type` statements, not inline `type` in mixed imports + - ✅ `import type { Foo } from './foo'` + `import { bar } from './foo'` + - ❌ `import { bar, type Foo } from './foo'` - ESLint: - Vue + TS rules - no floating promises @@ -119,7 +122,10 @@ The project uses **Nx** for build orchestration and task management - Prefer reactive props destructuring to `const props = defineProps<...>` - Do not use `withDefaults` or runtime props declaration - Do not import Vue macros unnecessarily - - Prefer `useModel` to separately defining a prop and emit + - Prefer `defineModel` to separately defining a prop and emit for v-model bindings + - Define slots via template usage, not `defineSlots` + - Use same-name shorthand for slot prop bindings: `:isExpanded` instead of `:is-expanded="isExpanded"` + - Derive component types using `vue-component-type-helpers` (`ComponentProps`, `ComponentSlots`) instead of separate type files - Be judicious with addition of new refs or other state - If it's possible to accomplish the design goals with just a prop, don't add a `ref` - If it's possible to use the `ref` or prop directly, don't add a `computed` @@ -137,7 +143,7 @@ The project uses **Nx** for build orchestration and task management 8. Implement proper error handling 9. Follow Vue 3 style guide and naming conventions 10. Use Vite for fast development and building -11. Use vue-i18n in composition API for any string literals. Place new translation entries in src/locales/en/main.json +11. Use vue-i18n in composition API for any string literals. Place new translation entries in src/locales/en/main.json. Use the plurals system in i18n instead of hardcoding pluralization in templates. 12. Avoid new usage of PrimeVue components 13. Write tests for all changes, especially bug fixes to catch future regressions 14. Write code that is expressive and self-documenting to the furthest degree possible. This reduces the need for code comments which can get out of sync with the code itself. Try to avoid comments unless absolutely necessary @@ -155,6 +161,8 @@ The project uses **Nx** for build orchestration and task management ## Testing Guidelines +See @docs/testing/*.md for detailed patterns. + - Frameworks: - Vitest (unit/component, happy-dom) - Playwright (E2E) @@ -266,3 +274,18 @@ When referencing Comfy-Org repos: - Always use `import { cn } from '@/utils/tailwindUtil'` - e.g. `
` - Use `cn()` inline in the template when feasible instead of creating a `computed` to hold the value +- NEVER use `!important` or the `!` important prefix for tailwind classes + - Find existing `!important` classes that are interfering with the styling and propose corrections of those instead. +- NEVER use arbitrary percentage values like `w-[80%]` when a Tailwind fraction utility exists + - Use `w-4/5` instead of `w-[80%]`, `w-1/2` instead of `w-[50%]`, etc. + +## Agent-only rules + +Rules for agent-based coding tasks. + +### Temporary Files + +- Put planning documents under `/temp/plans/` +- Put scripts used under `/temp/scripts/` +- Put summaries of work performed under `/temp/summaries/` +- Put TODOs and status updates under `/temp/in_progress/` diff --git a/CODEOWNERS b/CODEOWNERS index 05e8d324c..fcba1e400 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -46,7 +46,6 @@ # Mask Editor /src/extensions/core/maskeditor.ts @trsommer @brucew4yn3rp /src/extensions/core/maskEditorLayerFilenames.ts @trsommer @brucew4yn3rp -/src/extensions/core/maskEditorOld.ts @trsommer @brucew4yn3rp # 3D /src/extensions/core/load3d.ts @jtydhr88 diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 000000000..ff6a1191d --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1 @@ +AMD and the AMD Arrow logo are trademarks of Advanced Micro Devices, Inc. \ No newline at end of file diff --git a/apps/desktop-ui/package.json b/apps/desktop-ui/package.json index a3d36f1a5..ed65e99a5 100644 --- a/apps/desktop-ui/package.json +++ b/apps/desktop-ui/package.json @@ -1,6 +1,6 @@ { "name": "@comfyorg/desktop-ui", - "version": "0.0.4", + "version": "0.0.6", "type": "module", "nx": { "tags": [ diff --git a/apps/desktop-ui/public/assets/images/amd-rocm-logo.png b/apps/desktop-ui/public/assets/images/amd-rocm-logo.png new file mode 100644 index 000000000..82de495cd Binary files /dev/null and b/apps/desktop-ui/public/assets/images/amd-rocm-logo.png differ diff --git a/apps/desktop-ui/src/components/install/GpuPicker.stories.ts b/apps/desktop-ui/src/components/install/GpuPicker.stories.ts new file mode 100644 index 000000000..d49893c38 --- /dev/null +++ b/apps/desktop-ui/src/components/install/GpuPicker.stories.ts @@ -0,0 +1,84 @@ +// eslint-disable-next-line storybook/no-renderer-packages +import type { Meta, StoryObj } from '@storybook/vue3' +import type { + ElectronAPI, + TorchDeviceType +} from '@comfyorg/comfyui-electron-types' +import { ref } from 'vue' + +import GpuPicker from './GpuPicker.vue' + +type Platform = ReturnType +type ElectronAPIStub = Pick +type WindowWithElectron = Window & { electronAPI?: ElectronAPIStub } + +const meta: Meta = { + title: 'Desktop/Components/GpuPicker', + component: GpuPicker, + parameters: { + layout: 'padded', + backgrounds: { + default: 'dark', + values: [ + { name: 'dark', value: '#0a0a0a' }, + { name: 'neutral-900', value: '#171717' }, + { name: 'neutral-950', value: '#0a0a0a' } + ] + } + } +} + +export default meta +type Story = StoryObj + +function createElectronDecorator(platform: Platform) { + function getPlatform() { + return platform + } + + return function ElectronDecorator() { + const windowWithElectron = window as WindowWithElectron + windowWithElectron.electronAPI = { getPlatform } + return { template: '' } + } +} + +function renderWithDevice(device: TorchDeviceType | null) { + return function Render() { + return { + components: { GpuPicker }, + setup() { + const selected = ref(device) + return { selected } + }, + template: ` +
+ +
+ ` + } + } +} + +const windowsDecorator = createElectronDecorator('win32') +const macDecorator = createElectronDecorator('darwin') + +export const WindowsNvidiaSelected: Story = { + decorators: [windowsDecorator], + render: renderWithDevice('nvidia') +} + +export const WindowsAmdSelected: Story = { + decorators: [windowsDecorator], + render: renderWithDevice('amd') +} + +export const WindowsCpuSelected: Story = { + decorators: [windowsDecorator], + render: renderWithDevice('cpu') +} + +export const MacMpsSelected: Story = { + decorators: [macDecorator], + render: renderWithDevice('mps') +} diff --git a/apps/desktop-ui/src/components/install/GpuPicker.vue b/apps/desktop-ui/src/components/install/GpuPicker.vue index 98dddb762..217e99ce5 100644 --- a/apps/desktop-ui/src/components/install/GpuPicker.vue +++ b/apps/desktop-ui/src/components/install/GpuPicker.vue @@ -11,29 +11,32 @@ - + @@ -41,7 +44,6 @@ @@ -81,13 +83,15 @@ const selected = defineModel('device', { const electron = electronAPI() const platform = electron.getPlatform() -const showRecommendedBadge = computed( - () => selected.value === 'mps' || selected.value === 'nvidia' +const recommendedDevices: TorchDeviceType[] = ['mps', 'nvidia', 'amd'] +const showRecommendedBadge = computed(() => + selected.value ? recommendedDevices.includes(selected.value) : false ) const descriptionKeys = { mps: 'appleMetal', nvidia: 'nvidia', + amd: 'amd', cpu: 'cpu', unsupported: 'manual' } as const @@ -97,7 +101,7 @@ const descriptionText = computed(() => { return st(`install.gpuPicker.${key}Description`, '') }) -const pickGpu = (value: TorchDeviceType) => { +function pickGpu(value: TorchDeviceType) { selected.value = value } diff --git a/apps/desktop-ui/src/components/install/HardwareOption.stories.ts b/apps/desktop-ui/src/components/install/HardwareOption.stories.ts index d830af49f..fc0e56713 100644 --- a/apps/desktop-ui/src/components/install/HardwareOption.stories.ts +++ b/apps/desktop-ui/src/components/install/HardwareOption.stories.ts @@ -29,7 +29,6 @@ export const AppleMetalSelected: Story = { imagePath: '/assets/images/apple-mps-logo.png', placeholderText: 'Apple Metal', subtitle: 'Apple Metal', - value: 'mps', selected: true } } @@ -39,7 +38,6 @@ export const AppleMetalUnselected: Story = { imagePath: '/assets/images/apple-mps-logo.png', placeholderText: 'Apple Metal', subtitle: 'Apple Metal', - value: 'mps', selected: false } } @@ -48,7 +46,6 @@ export const CPUOption: Story = { args: { placeholderText: 'CPU', subtitle: 'Subtitle', - value: 'cpu', selected: false } } @@ -57,7 +54,6 @@ export const ManualInstall: Story = { args: { placeholderText: 'Manual Install', subtitle: 'Subtitle', - value: 'unsupported', selected: false } } @@ -67,7 +63,6 @@ export const NvidiaSelected: Story = { imagePath: '/assets/images/nvidia-logo-square.jpg', placeholderText: 'NVIDIA', subtitle: 'NVIDIA', - value: 'nvidia', selected: true } } diff --git a/apps/desktop-ui/src/components/install/HardwareOption.vue b/apps/desktop-ui/src/components/install/HardwareOption.vue index ae254fd8f..9acc9e79c 100644 --- a/apps/desktop-ui/src/components/install/HardwareOption.vue +++ b/apps/desktop-ui/src/components/install/HardwareOption.vue @@ -36,17 +36,13 @@ diff --git a/src/components/MenuHamburger.vue b/src/components/MenuHamburger.vue index ab914f667..fa99e032c 100644 --- a/src/components/MenuHamburger.vue +++ b/src/components/MenuHamburger.vue @@ -5,23 +5,23 @@ >
diff --git a/src/components/common/BackgroundImageUpload.vue b/src/components/common/BackgroundImageUpload.vue index 46105e537..5835aeace 100644 --- a/src/components/common/BackgroundImageUpload.vue +++ b/src/components/common/BackgroundImageUpload.vue @@ -7,20 +7,24 @@ /> + + diff --git a/src/components/common/TreeExplorerTreeNode.vue b/src/components/common/TreeExplorerTreeNode.vue index 186769c19..cea8ba451 100644 --- a/src/components/common/TreeExplorerTreeNode.vue +++ b/src/components/common/TreeExplorerTreeNode.vue @@ -28,7 +28,7 @@ />
diff --git a/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue b/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue index 5f96fe4c2..aeb98971b 100644 --- a/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue +++ b/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue @@ -175,6 +175,7 @@ { sessionStartTime.value = Date.now() }) +const systemStatsStore = useSystemStatsStore() + +const distributions = computed(() => { + // eslint-disable-next-line no-undef + switch (__DISTRIBUTION__) { + case 'cloud': + return [TemplateIncludeOnDistributionEnum.Cloud] + case 'localhost': + return [TemplateIncludeOnDistributionEnum.Local] + case 'desktop': + default: + if (systemStatsStore.systemStats?.system.os === 'darwin') { + return [ + TemplateIncludeOnDistributionEnum.Desktop, + TemplateIncludeOnDistributionEnum.Mac + ] + } + return [ + TemplateIncludeOnDistributionEnum.Desktop, + TemplateIncludeOnDistributionEnum.Windows + ] + } +}) + // Wrap onClose to track session end const onClose = () => { if (isCloud) { @@ -511,6 +538,9 @@ const allTemplates = computed(() => { return workflowTemplatesStore.enhancedTemplates }) +// Navigation +const selectedNavItem = ref('all') + // Filter templates based on selected navigation item const navigationFilteredTemplates = computed(() => { if (!selectedNavItem.value) { @@ -533,9 +563,40 @@ const { availableRunsOn, filteredCount, totalCount, - resetFilters + resetFilters, + loadFuseOptions } = useTemplateFiltering(navigationFilteredTemplates) +/** + * Coordinates state between the selected navigation item and the sort order to + * create deterministic, predictable behavior. + * @param source The origin of the change ('nav' or 'sort'). + */ +const coordinateNavAndSort = (source: 'nav' | 'sort') => { + const isPopularNav = selectedNavItem.value === 'popular' + const isPopularSort = sortBy.value === 'popular' + + if (source === 'nav') { + if (isPopularNav && !isPopularSort) { + // When navigating to 'Popular' category, automatically set sort to 'Popular'. + sortBy.value = 'popular' + } else if (!isPopularNav && isPopularSort) { + // When navigating away from 'Popular' category while sort is 'Popular', reset sort to default. + sortBy.value = 'default' + } + } else if (source === 'sort') { + // When sort is changed away from 'Popular' while in the 'Popular' category, + // reset the category to 'All Templates' to avoid a confusing state. + if (isPopularNav && !isPopularSort) { + selectedNavItem.value = 'all' + } + } +} + +// Watch for changes from the two sources ('nav' and 'sort') and trigger the coordinator. +watch(selectedNavItem, () => coordinateNavAndSort('nav')) +watch(sortBy, () => coordinateNavAndSort('sort')) + // Convert between string array and object array for MultiSelect component const selectedModelObjects = computed({ get() { @@ -578,9 +639,6 @@ const cardRefs = ref([]) // Force re-render key for templates when sorting changes const templateListKey = ref(0) -// Navigation -const selectedNavItem = ref('all') - // Search text for model filter const modelSearchText = ref('') @@ -645,11 +703,19 @@ const runsOnFilterLabel = computed(() => { // Sort options const sortOptions = computed(() => [ - { name: t('templateWorkflows.sort.newest', 'Newest'), value: 'newest' }, { name: t('templateWorkflows.sort.default', 'Default'), value: 'default' }, + { + name: t('templateWorkflows.sort.recommended', 'Recommended'), + value: 'recommended' + }, + { + name: t('templateWorkflows.sort.popular', 'Popular'), + value: 'popular' + }, + { name: t('templateWorkflows.sort.newest', 'Newest'), value: 'newest' }, { name: t('templateWorkflows.sort.vramLowToHigh', 'VRAM Usage (Low to High)'), value: 'vram-low-to-high' @@ -750,10 +816,10 @@ const pageTitle = computed(() => { // Initialize templates loading with useAsyncState const { isLoading } = useAsyncState( async () => { - // Run both operations in parallel for better performance await Promise.all([ loadTemplates(), - workflowTemplatesStore.loadWorkflowTemplates() + workflowTemplatesStore.loadWorkflowTemplates(), + loadFuseOptions() ]) return true }, @@ -763,6 +829,14 @@ const { isLoading } = useAsyncState( } ) +const isTemplateVisibleOnDistribution = (template: TemplateInfo) => { + return (template.includeOnDistributions?.length ?? 0) > 0 + ? distributions.value.some((d) => + template.includeOnDistributions?.includes(d) + ) + : true +} + onBeforeUnmount(() => { cardRefs.value = [] // Release DOM refs }) diff --git a/src/components/dialog/content/ApiNodesSignInContent.vue b/src/components/dialog/content/ApiNodesSignInContent.vue index a3139daaf..41ad903c7 100644 --- a/src/components/dialog/content/ApiNodesSignInContent.vue +++ b/src/components/dialog/content/ApiNodesSignInContent.vue @@ -11,24 +11,25 @@
-
- +
+ + diff --git a/src/components/honeyToast/HoneyToast.stories.ts b/src/components/honeyToast/HoneyToast.stories.ts new file mode 100644 index 000000000..74331d49f --- /dev/null +++ b/src/components/honeyToast/HoneyToast.stories.ts @@ -0,0 +1,293 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' +import { ref } from 'vue' + +import ProgressToastItem from '@/components/toast/ProgressToastItem.vue' +import Button from '@/components/ui/button/Button.vue' +import type { AssetDownload } from '@/stores/assetDownloadStore' +import { cn } from '@/utils/tailwindUtil' + +import HoneyToast from './HoneyToast.vue' + +function createMockJob(overrides: Partial = {}): AssetDownload { + return { + taskId: 'task-1', + assetId: 'asset-1', + assetName: 'model-v1.safetensors', + bytesTotal: 1000000, + bytesDownloaded: 0, + progress: 0, + status: 'created', + lastUpdate: Date.now(), + ...overrides + } +} + +const meta: Meta = { + title: 'Toast/HoneyToast', + component: HoneyToast, + parameters: { + layout: 'fullscreen' + }, + decorators: [ + () => ({ + template: '
' + }) + ] +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + render: () => ({ + components: { HoneyToast, Button, ProgressToastItem }, + setup() { + const isExpanded = ref(false) + const jobs = [ + createMockJob({ + taskId: 'task-1', + assetName: 'model-v1.safetensors', + status: 'completed', + progress: 1 + }), + createMockJob({ + taskId: 'task-2', + assetName: 'lora-style.safetensors', + status: 'running', + progress: 0.45 + }), + createMockJob({ + taskId: 'task-3', + assetName: 'vae-decoder.safetensors', + status: 'created' + }) + ] + return { isExpanded, cn, jobs } + }, + template: ` + + + + + + ` + }) +} + +export const Expanded: Story = { + render: () => ({ + components: { HoneyToast, Button, ProgressToastItem }, + setup() { + const isExpanded = ref(true) + const jobs = [ + createMockJob({ + taskId: 'task-1', + assetName: 'model-v1.safetensors', + status: 'completed', + progress: 1 + }), + createMockJob({ + taskId: 'task-2', + assetName: 'lora-style.safetensors', + status: 'running', + progress: 0.45 + }), + createMockJob({ + taskId: 'task-3', + assetName: 'vae-decoder.safetensors', + status: 'created' + }) + ] + return { isExpanded, cn, jobs } + }, + template: ` + + + + + + ` + }) +} + +export const Completed: Story = { + render: () => ({ + components: { HoneyToast, Button, ProgressToastItem }, + setup() { + const isExpanded = ref(false) + const jobs = [ + createMockJob({ + taskId: 'task-1', + assetName: 'model-v1.safetensors', + bytesDownloaded: 1000000, + progress: 1, + status: 'completed' + }), + createMockJob({ + taskId: 'task-2', + assetId: 'asset-2', + assetName: 'lora-style.safetensors', + bytesTotal: 500000, + bytesDownloaded: 500000, + progress: 1, + status: 'completed' + }) + ] + return { isExpanded, cn, jobs } + }, + template: ` + + + + + + ` + }) +} + +export const WithError: Story = { + render: () => ({ + components: { HoneyToast, Button, ProgressToastItem }, + setup() { + const isExpanded = ref(true) + const jobs = [ + createMockJob({ + taskId: 'task-1', + assetName: 'model-v1.safetensors', + status: 'failed', + progress: 0.23 + }), + createMockJob({ + taskId: 'task-2', + assetName: 'lora-style.safetensors', + status: 'completed', + progress: 1 + }) + ] + return { isExpanded, cn, jobs } + }, + template: ` + + + + + + ` + }) +} + +export const Hidden: Story = { + render: () => ({ + components: { HoneyToast }, + template: ` +
+

HoneyToast is hidden when visible=false. Nothing appears at the bottom.

+ + + + + +
+ ` + }) +} diff --git a/src/components/honeyToast/HoneyToast.test.ts b/src/components/honeyToast/HoneyToast.test.ts new file mode 100644 index 000000000..ada123053 --- /dev/null +++ b/src/components/honeyToast/HoneyToast.test.ts @@ -0,0 +1,137 @@ +import type { VueWrapper } from '@vue/test-utils' +import { mount } from '@vue/test-utils' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { defineComponent, h, nextTick, ref } from 'vue' + +import HoneyToast from './HoneyToast.vue' + +describe('HoneyToast', () => { + beforeEach(() => { + vi.clearAllMocks() + document.body.innerHTML = '' + }) + + function mountComponent( + props: { visible: boolean; expanded?: boolean } = { visible: true } + ): VueWrapper { + return mount(HoneyToast, { + props, + slots: { + default: (slotProps: { isExpanded: boolean }) => + h( + 'div', + { 'data-testid': 'content' }, + slotProps.isExpanded ? 'expanded' : 'collapsed' + ), + footer: (slotProps: { isExpanded: boolean; toggle: () => void }) => + h( + 'button', + { + 'data-testid': 'toggle-btn', + onClick: slotProps.toggle + }, + slotProps.isExpanded ? 'Collapse' : 'Expand' + ) + }, + attachTo: document.body + }) + } + + it('renders when visible is true', async () => { + const wrapper = mountComponent({ visible: true }) + await nextTick() + + const toast = document.body.querySelector('[role="status"]') + expect(toast).toBeTruthy() + + wrapper.unmount() + }) + + it('does not render when visible is false', async () => { + const wrapper = mountComponent({ visible: false }) + await nextTick() + + const toast = document.body.querySelector('[role="status"]') + expect(toast).toBeFalsy() + + wrapper.unmount() + }) + + it('passes is-expanded=false to slots by default', async () => { + const wrapper = mountComponent({ visible: true }) + await nextTick() + + const content = document.body.querySelector('[data-testid="content"]') + expect(content?.textContent).toBe('collapsed') + + wrapper.unmount() + }) + + it('applies collapsed max-height class when collapsed', async () => { + const wrapper = mountComponent({ visible: true, expanded: false }) + await nextTick() + + const expandableArea = document.body.querySelector( + '[role="status"] > div:first-child' + ) + expect(expandableArea?.classList.contains('max-h-0')).toBe(true) + + wrapper.unmount() + }) + + it('has aria-live="polite" for accessibility', async () => { + const wrapper = mountComponent({ visible: true }) + await nextTick() + + const toast = document.body.querySelector('[role="status"]') + expect(toast?.getAttribute('aria-live')).toBe('polite') + + wrapper.unmount() + }) + + it('supports v-model:expanded with reactive parent state', async () => { + const TestWrapper = defineComponent({ + components: { HoneyToast }, + setup() { + const expanded = ref(false) + return { expanded } + }, + template: ` + + + + + ` + }) + + const wrapper = mount(TestWrapper, { attachTo: document.body }) + await nextTick() + + const content = document.body.querySelector('[data-testid="content"]') + expect(content?.textContent).toBe('collapsed') + + const toggleBtn = document.body.querySelector( + '[data-testid="toggle-btn"]' + ) as HTMLButtonElement + expect(toggleBtn?.textContent?.trim()).toBe('Expand') + + toggleBtn?.click() + await nextTick() + + expect(content?.textContent).toBe('expanded') + expect(toggleBtn?.textContent?.trim()).toBe('Collapse') + + const expandableArea = document.body.querySelector( + '[role="status"] > div:first-child' + ) + expect(expandableArea?.classList.contains('max-h-[400px]')).toBe(true) + + wrapper.unmount() + }) +}) diff --git a/src/components/honeyToast/HoneyToast.vue b/src/components/honeyToast/HoneyToast.vue new file mode 100644 index 000000000..a7d86ba77 --- /dev/null +++ b/src/components/honeyToast/HoneyToast.vue @@ -0,0 +1,46 @@ + + + diff --git a/src/components/input/MultiSelect.vue b/src/components/input/MultiSelect.vue index aaa372d73..21ba0d6a2 100644 --- a/src/components/input/MultiSelect.vue +++ b/src/components/input/MultiSelect.vue @@ -160,7 +160,7 @@ > diff --git a/src/components/load3d/Load3D.vue b/src/components/load3d/Load3D.vue index befc1f322..f6942e7c3 100644 --- a/src/components/load3d/Load3D.vue +++ b/src/components/load3d/Load3D.vue @@ -24,6 +24,7 @@ v-model:light-config="lightConfig" :is-splat-model="isSplatModel" :is-ply-model="isPlyModel" + :has-skeleton="hasSkeleton" @update-background-image="handleBackgroundImageUpdate" @export-model="handleExportModel" /> @@ -33,6 +34,9 @@ v-model:playing="playing" v-model:selected-speed="selectedSpeed" v-model:selected-animation="selectedAnimation" + v-model:animation-progress="animationProgress" + v-model:animation-duration="animationDuration" + @seek="handleSeek" />
-
@@ -47,8 +58,10 @@ v-if="showModelControls" v-model:material-mode="modelConfig!.materialMode" v-model:up-direction="modelConfig!.upDirection" + v-model:show-skeleton="modelConfig!.showSkeleton" :hide-material-mode="isSplatModel" :is-ply-model="isPlyModel" + :has-skeleton="hasSkeleton" /> diff --git a/src/components/load3d/controls/CameraControls.vue b/src/components/load3d/controls/CameraControls.vue index fb2d153b7..7922e6f1e 100644 --- a/src/components/load3d/controls/CameraControls.vue +++ b/src/components/load3d/controls/CameraControls.vue @@ -1,13 +1,17 @@ diff --git a/src/workbench/extensions/manager/components/manager/NodeConflictFooter.vue b/src/workbench/extensions/manager/components/manager/NodeConflictFooter.vue index faeb181d9..842bcb576 100644 --- a/src/workbench/extensions/manager/components/manager/NodeConflictFooter.vue +++ b/src/workbench/extensions/manager/components/manager/NodeConflictFooter.vue @@ -2,30 +2,28 @@
diff --git a/tests-ui/tests/components/dialog/content/manager/packCard/PackCard.test.ts b/src/workbench/extensions/manager/components/manager/packCard/PackCard.test.ts similarity index 100% rename from tests-ui/tests/components/dialog/content/manager/packCard/PackCard.test.ts rename to src/workbench/extensions/manager/components/manager/packCard/PackCard.test.ts diff --git a/src/workbench/extensions/manager/components/manager/packCard/PackCardFooter.test.ts b/src/workbench/extensions/manager/components/manager/packCard/PackCardFooter.test.ts new file mode 100644 index 000000000..950f8f647 --- /dev/null +++ b/src/workbench/extensions/manager/components/manager/packCard/PackCardFooter.test.ts @@ -0,0 +1,177 @@ +import type { VueWrapper } from '@vue/test-utils' +import { mount } from '@vue/test-utils' +import { createPinia } from 'pinia' +import PrimeVue from 'primevue/config' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { ref } from 'vue' +import { createI18n } from 'vue-i18n' + +import enMessages from '@/locales/en/main.json' with { type: 'json' } +import { IsInstallingKey } from '@/workbench/extensions/manager/types/comfyManagerTypes' + +import PackCardFooter from './PackCardFooter.vue' + +// Mock the child components +vi.mock( + '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue', + () => ({ + default: { template: '
' } + }) +) + +vi.mock( + '@/workbench/extensions/manager/components/manager/button/PackEnableToggle.vue', + () => ({ + default: { template: '
' } + }) +) + +// Mock composables +const mockIsPackInstalled = vi.fn() +const mockCheckNodeCompatibility = vi.fn() + +vi.mock('@/workbench/extensions/manager/stores/comfyManagerStore', () => ({ + useComfyManagerStore: vi.fn(() => ({ + isPackInstalled: mockIsPackInstalled + })) +})) + +vi.mock( + '@/workbench/extensions/manager/composables/useConflictDetection', + () => ({ + useConflictDetection: vi.fn(() => ({ + checkNodeCompatibility: mockCheckNodeCompatibility + })) + }) +) + +// Remove the mock for injection key since we're importing it directly + +const mockNodePack = { + id: 'test-pack', + name: 'Test Pack', + downloads: 1000 +} + +describe('PackCardFooter', () => { + beforeEach(() => { + vi.clearAllMocks() + mockIsPackInstalled.mockReset() + mockCheckNodeCompatibility.mockReset() + }) + + const mountComponent = (props = {}): VueWrapper => { + const i18n = createI18n({ + legacy: false, + locale: 'en', + messages: { en: enMessages } + }) + + return mount(PackCardFooter, { + props: { + nodePack: mockNodePack, + ...props + }, + global: { + plugins: [PrimeVue, createPinia(), i18n], + provide: { + [IsInstallingKey]: ref(false) + } + } + }) + } + + describe('component rendering', () => { + it('shows download count when available', () => { + mockIsPackInstalled.mockReturnValue(false) + mockCheckNodeCompatibility.mockReturnValue({ + hasConflict: false, + conflicts: [] + }) + const wrapper = mountComponent() + + expect(wrapper.text()).toContain('1,000') + }) + + it('shows install button for uninstalled packages', () => { + mockIsPackInstalled.mockReturnValue(false) + mockCheckNodeCompatibility.mockReturnValue({ + hasConflict: false, + conflicts: [] + }) + + const wrapper = mountComponent() + + expect(wrapper.find('[data-testid="pack-install-button"]').exists()).toBe( + true + ) + expect(wrapper.find('[data-testid="pack-enable-toggle"]').exists()).toBe( + false + ) + }) + + it('shows enable toggle for installed packages', () => { + mockIsPackInstalled.mockReturnValue(true) + + const wrapper = mountComponent() + + expect(wrapper.find('[data-testid="pack-enable-toggle"]').exists()).toBe( + true + ) + expect(wrapper.find('[data-testid="pack-install-button"]').exists()).toBe( + false + ) + }) + }) + + describe('conflict detection for uninstalled packages', () => { + it('passes conflict info to install button when conflicts exist', () => { + mockIsPackInstalled.mockReturnValue(false) + mockCheckNodeCompatibility.mockReturnValue({ + hasConflict: true, + conflicts: [ + { + type: 'os_conflict', + current_value: 'windows', + required_value: 'linux' + } + ] + }) + + const wrapper = mountComponent() + + const installButton = wrapper.find('[data-testid="pack-install-button"]') + expect(installButton.exists()).toBe(true) + // The install button should receive has-conflict prop as true + expect(installButton.attributes()).toHaveProperty('has-conflict') + }) + + it('does not pass conflict info when no conflicts exist', () => { + mockIsPackInstalled.mockReturnValue(false) + mockCheckNodeCompatibility.mockReturnValue({ + hasConflict: false, + conflicts: [] + }) + + const wrapper = mountComponent() + + const installButton = wrapper.find('[data-testid="pack-install-button"]') + expect(installButton.exists()).toBe(true) + // The install button should receive has-conflict prop as false + expect(installButton.attributes()['has-conflict']).toBe('false') + }) + }) + + describe('installed packages', () => { + it('does not pass has-conflict prop to enable toggle', () => { + mockIsPackInstalled.mockReturnValue(true) + + const wrapper = mountComponent() + + const enableToggle = wrapper.find('[data-testid="pack-enable-toggle"]') + expect(enableToggle.exists()).toBe(true) + // The enable toggle should not receive has-conflict prop (removed in our fix) + expect(enableToggle.attributes()).not.toHaveProperty('has-conflict') + }) + }) +}) diff --git a/src/workbench/extensions/manager/components/manager/packCard/PackCardFooter.vue b/src/workbench/extensions/manager/components/manager/packCard/PackCardFooter.vue index 733e93bfb..ae8bb351c 100644 --- a/src/workbench/extensions/manager/components/manager/packCard/PackCardFooter.vue +++ b/src/workbench/extensions/manager/components/manager/packCard/PackCardFooter.vue @@ -13,11 +13,7 @@ :has-conflict="hasConflicts" :conflict-info="conflictInfo" /> - +
diff --git a/tests-ui/tests/composables/useMissingNodes.test.ts b/src/workbench/extensions/manager/composables/nodePack/useMissingNodes.test.ts similarity index 99% rename from tests-ui/tests/composables/useMissingNodes.test.ts rename to src/workbench/extensions/manager/composables/nodePack/useMissingNodes.test.ts index 04a4e5ffd..a467bf098 100644 --- a/tests-ui/tests/composables/useMissingNodes.test.ts +++ b/src/workbench/extensions/manager/composables/nodePack/useMissingNodes.test.ts @@ -9,8 +9,7 @@ import { useWorkflowPacks } from '@/workbench/extensions/manager/composables/nod import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore' vi.mock('@vueuse/core', async () => { - const actual = - await vi.importActual('@vueuse/core') + const actual = await vi.importActual('@vueuse/core') return { ...actual, createSharedComposable: any>(fn: Fn) => fn diff --git a/tests-ui/tests/composables/nodePack/usePacksSelection.test.ts b/src/workbench/extensions/manager/composables/nodePack/usePacksSelection.test.ts similarity index 87% rename from tests-ui/tests/composables/nodePack/usePacksSelection.test.ts rename to src/workbench/extensions/manager/composables/nodePack/usePacksSelection.test.ts index 01a635a88..c38fc9c33 100644 --- a/tests-ui/tests/composables/nodePack/usePacksSelection.test.ts +++ b/src/workbench/extensions/manager/composables/nodePack/usePacksSelection.test.ts @@ -20,7 +20,7 @@ type NodePack = components['schemas']['Node'] describe('usePacksSelection', () => { let managerStore: ReturnType - let mockIsPackInstalled: ReturnType + let mockIsPackInstalled: (packName: string | undefined) => boolean const createMockPack = (id: string): NodePack => ({ id, @@ -58,7 +58,7 @@ describe('usePacksSelection', () => { createMockPack('pack3') ]) - mockIsPackInstalled.mockImplementation((id: string) => { + vi.mocked(mockIsPackInstalled).mockImplementation((id) => { return id === 'pack1' || id === 'pack3' }) @@ -76,7 +76,7 @@ describe('usePacksSelection', () => { createMockPack('pack2') ]) - mockIsPackInstalled.mockReturnValue(false) + vi.mocked(mockIsPackInstalled).mockReturnValue(false) const { installedPacks } = usePacksSelection(nodePacks) @@ -85,7 +85,7 @@ describe('usePacksSelection', () => { it('should update when nodePacks ref changes', () => { const nodePacks = ref([createMockPack('pack1')]) - mockIsPackInstalled.mockReturnValue(true) + vi.mocked(mockIsPackInstalled).mockReturnValue(true) const { installedPacks } = usePacksSelection(nodePacks) expect(installedPacks.value).toHaveLength(1) @@ -109,7 +109,7 @@ describe('usePacksSelection', () => { createMockPack('pack3') ]) - mockIsPackInstalled.mockImplementation((id: string) => { + vi.mocked(mockIsPackInstalled).mockImplementation((id) => { return id === 'pack1' }) @@ -126,7 +126,7 @@ describe('usePacksSelection', () => { createMockPack('pack2') ]) - mockIsPackInstalled.mockReturnValue(false) + vi.mocked(mockIsPackInstalled).mockReturnValue(false) const { notInstalledPacks } = usePacksSelection(nodePacks) @@ -141,7 +141,7 @@ describe('usePacksSelection', () => { createMockPack('pack2') ]) - mockIsPackInstalled.mockReturnValue(true) + vi.mocked(mockIsPackInstalled).mockReturnValue(true) const { isAllInstalled } = usePacksSelection(nodePacks) @@ -154,7 +154,7 @@ describe('usePacksSelection', () => { createMockPack('pack2') ]) - mockIsPackInstalled.mockImplementation((id: string) => id === 'pack1') + vi.mocked(mockIsPackInstalled).mockImplementation((id) => id === 'pack1') const { isAllInstalled } = usePacksSelection(nodePacks) @@ -177,7 +177,7 @@ describe('usePacksSelection', () => { createMockPack('pack2') ]) - mockIsPackInstalled.mockReturnValue(false) + vi.mocked(mockIsPackInstalled).mockReturnValue(false) const { isNoneInstalled } = usePacksSelection(nodePacks) @@ -190,7 +190,7 @@ describe('usePacksSelection', () => { createMockPack('pack2') ]) - mockIsPackInstalled.mockImplementation((id: string) => id === 'pack1') + vi.mocked(mockIsPackInstalled).mockImplementation((id) => id === 'pack1') const { isNoneInstalled } = usePacksSelection(nodePacks) @@ -214,7 +214,7 @@ describe('usePacksSelection', () => { createMockPack('pack3') ]) - mockIsPackInstalled.mockImplementation((id: string) => { + vi.mocked(mockIsPackInstalled).mockImplementation((id) => { return id === 'pack1' || id === 'pack2' }) @@ -229,7 +229,7 @@ describe('usePacksSelection', () => { createMockPack('pack2') ]) - mockIsPackInstalled.mockReturnValue(true) + vi.mocked(mockIsPackInstalled).mockReturnValue(true) const { isMixed } = usePacksSelection(nodePacks) @@ -242,7 +242,7 @@ describe('usePacksSelection', () => { createMockPack('pack2') ]) - mockIsPackInstalled.mockReturnValue(false) + vi.mocked(mockIsPackInstalled).mockReturnValue(false) const { isMixed } = usePacksSelection(nodePacks) @@ -265,7 +265,7 @@ describe('usePacksSelection', () => { createMockPack('pack2') ]) - mockIsPackInstalled.mockReturnValue(true) + vi.mocked(mockIsPackInstalled).mockReturnValue(true) const { selectionState } = usePacksSelection(nodePacks) @@ -278,7 +278,7 @@ describe('usePacksSelection', () => { createMockPack('pack2') ]) - mockIsPackInstalled.mockReturnValue(false) + vi.mocked(mockIsPackInstalled).mockReturnValue(false) const { selectionState } = usePacksSelection(nodePacks) @@ -292,7 +292,7 @@ describe('usePacksSelection', () => { createMockPack('pack3') ]) - mockIsPackInstalled.mockImplementation((id: string) => id === 'pack1') + vi.mocked(mockIsPackInstalled).mockImplementation((id) => id === 'pack1') const { selectionState } = usePacksSelection(nodePacks) @@ -305,13 +305,13 @@ describe('usePacksSelection', () => { createMockPack('pack2') ]) - mockIsPackInstalled.mockReturnValue(false) + vi.mocked(mockIsPackInstalled).mockReturnValue(false) const { selectionState } = usePacksSelection(nodePacks) expect(selectionState.value).toBe('none-installed') // Change mock to simulate installation - mockIsPackInstalled.mockReturnValue(true) + vi.mocked(mockIsPackInstalled).mockReturnValue(true) // Force reactivity update nodePacks.value = [...nodePacks.value] @@ -327,7 +327,7 @@ describe('usePacksSelection', () => { createMockPack('pack2') ]) - mockIsPackInstalled.mockImplementation((id: string) => id === 'pack2') + vi.mocked(mockIsPackInstalled).mockImplementation((id) => id === 'pack2') const { installedPacks, notInstalledPacks } = usePacksSelection(nodePacks) @@ -347,8 +347,8 @@ describe('usePacksSelection', () => { pack2: false } - mockIsPackInstalled.mockImplementation( - (id: string) => installationStatus[id] || false + vi.mocked(mockIsPackInstalled).mockImplementation( + (id) => (id && installationStatus[id]) || false ) const { installedPacks, notInstalledPacks, selectionState } = diff --git a/tests-ui/tests/composables/nodePack/usePacksStatus.test.ts b/src/workbench/extensions/manager/composables/nodePack/usePacksStatus.test.ts similarity index 100% rename from tests-ui/tests/composables/nodePack/usePacksStatus.test.ts rename to src/workbench/extensions/manager/composables/nodePack/usePacksStatus.test.ts diff --git a/tests-ui/tests/composables/useUpdateAvailableNodes.test.ts b/src/workbench/extensions/manager/composables/nodePack/useUpdateAvailableNodes.test.ts similarity index 100% rename from tests-ui/tests/composables/useUpdateAvailableNodes.test.ts rename to src/workbench/extensions/manager/composables/nodePack/useUpdateAvailableNodes.test.ts diff --git a/tests-ui/tests/composables/useConflictAcknowledgment.test.ts b/src/workbench/extensions/manager/composables/useConflictAcknowledgment.test.ts similarity index 100% rename from tests-ui/tests/composables/useConflictAcknowledgment.test.ts rename to src/workbench/extensions/manager/composables/useConflictAcknowledgment.test.ts diff --git a/tests-ui/tests/composables/useConflictDetection.test.ts b/src/workbench/extensions/manager/composables/useConflictDetection.test.ts similarity index 97% rename from tests-ui/tests/composables/useConflictDetection.test.ts rename to src/workbench/extensions/manager/composables/useConflictDetection.test.ts index d3079219e..28fa8302c 100644 --- a/tests-ui/tests/composables/useConflictDetection.test.ts +++ b/src/workbench/extensions/manager/composables/useConflictDetection.test.ts @@ -14,6 +14,17 @@ import { useConflictDetectionStore } from '@/workbench/extensions/manager/stores import type { ConflictDetectionResult } from '@/workbench/extensions/manager/types/conflictDetectionTypes' import { checkVersionCompatibility } from '@/workbench/extensions/manager/utils/versionUtil' +// Mock @vueuse/core until function +vi.mock('@vueuse/core', async () => { + const actual = await vi.importActual('@vueuse/core') + return { + ...actual, + until: vi.fn(() => ({ + toBe: vi.fn(() => Promise.resolve()) + })) + } +}) + // Mock dependencies vi.mock('@/workbench/extensions/manager/services/comfyManagerService', () => ({ useComfyManagerService: vi.fn() @@ -159,6 +170,7 @@ describe('useConflictDetection', () => { clearConflicts: vi.fn() } as unknown as ReturnType + const mockIsInitialized = ref(true) const mockSystemStatsStore = { systemStats: { system: { @@ -171,7 +183,7 @@ describe('useConflictDetection', () => { '3.11.0 (main, Oct 13 2023, 09:34:16) [Clang 15.0.0 (clang-1500.0.40.1)]', pytorch_version: '2.1.0', embedded_python: false, - argv: [] + argv: ['--enable-manager'] }, devices: [ { @@ -185,7 +197,7 @@ describe('useConflictDetection', () => { } ] }, - isInitialized: ref(true), + isInitialized: mockIsInitialized, $state: {} as never, $patch: vi.fn(), $reset: vi.fn(), @@ -398,7 +410,7 @@ describe('useConflictDetection', () => { mockComfyManagerService.getImportFailInfoBulk ).mockResolvedValue({ 'fail-pack': { - msg: 'Import error', + error: 'Import error', name: 'fail-pack', path: '/path/to/pack' } as any // The actual API returns different structure than types @@ -416,7 +428,7 @@ describe('useConflictDetection', () => { // Import failure should match the actual implementation expect(result.results[0].conflicts).toContainEqual({ type: 'import_failed', - current_value: 'installed', + current_value: 'Import error', required_value: 'Import error' }) }) diff --git a/src/workbench/extensions/manager/composables/useConflictDetection.ts b/src/workbench/extensions/manager/composables/useConflictDetection.ts index 457b67697..aad300abe 100644 --- a/src/workbench/extensions/manager/composables/useConflictDetection.ts +++ b/src/workbench/extensions/manager/composables/useConflictDetection.ts @@ -7,6 +7,7 @@ import { useSystemStatsStore } from '@/stores/systemStatsStore' import type { components } from '@/types/comfyRegistryTypes' import { useInstalledPacks } from '@/workbench/extensions/manager/composables/nodePack/useInstalledPacks' import { useConflictAcknowledgment } from '@/workbench/extensions/manager/composables/useConflictAcknowledgment' +import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState' import { useComfyManagerService } from '@/workbench/extensions/manager/services/comfyManagerService' import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore' import { useConflictDetectionStore } from '@/workbench/extensions/manager/stores/conflictDetectionStore' @@ -87,9 +88,11 @@ export function useConflictDetection() { try { // Get system stats from store (primary source of system information) // Wait for systemStats to be initialized if not already - const { systemStats, isInitialized: systemStatsInitialized } = - useSystemStatsStore() - await until(systemStatsInitialized) + const systemStatsStore = useSystemStatsStore() + const { systemStats } = systemStatsStore + + // Wait for initialization using the store's isInitialized property (correct reactive way) + await until(() => systemStatsStore.isInitialized).toBe(true) const frontendVersion = getFrontendVersion() @@ -386,7 +389,10 @@ export function useConflictDetection() { * @returns Array of conflict detection results for failed imports */ function detectImportFailConflicts( - importFailInfo: Record + importFailInfo: Record< + string, + { error?: string; traceback?: string } | null + > ): ConflictDetectionResult[] { const results: ConflictDetectionResult[] = [] if (!importFailInfo || typeof importFailInfo !== 'object') { @@ -395,33 +401,29 @@ export function useConflictDetection() { // Process import failures for (const [packageId, failureInfo] of Object.entries(importFailInfo)) { - if (failureInfo && typeof failureInfo === 'object') { - // Extract error information from Manager API response - const errorMsg = failureInfo.msg || 'Unknown import error' - const modulePath = failureInfo.path || '' + if (!failureInfo || typeof failureInfo !== 'object') continue - results.push({ - package_id: packageId, - package_name: packageId, - has_conflict: true, - conflicts: [ - { - type: 'import_failed', - current_value: 'installed', - required_value: failureInfo.msg - } - ], - is_compatible: false - }) + const errorMsg = failureInfo.error || 'Unknown import error' + const fullErrorInfo = failureInfo.traceback || errorMsg - console.warn( - `[ConflictDetection] Python import failure detected for ${packageId}:`, + results.push({ + package_id: packageId, + package_name: packageId, + has_conflict: true, + conflicts: [ { - path: modulePath, - error: errorMsg + type: 'import_failed', + current_value: errorMsg, + required_value: fullErrorInfo } - ) - } + ], + is_compatible: false + }) + + console.warn( + `[ConflictDetection] Python import failure detected for ${packageId}:`, + errorMsg + ) } return results @@ -548,9 +550,11 @@ export function useConflictDetection() { */ async function initializeConflictDetection(): Promise { try { - // Check if manager is new Manager before proceeding - const { useManagerState } = - await import('@/workbench/extensions/manager/composables/useManagerState') + // First, wait for systemStats to be initialized + const systemStatsStore = useSystemStatsStore() + await until(() => systemStatsStore.isInitialized).toBe(true) + + // Now check if manager is new Manager const managerState = useManagerState() if (!managerState.isNewManagerUI.value) { diff --git a/tests-ui/tests/composables/useImportFailedDetection.test.ts b/src/workbench/extensions/manager/composables/useImportFailedDetection.test.ts similarity index 91% rename from tests-ui/tests/composables/useImportFailedDetection.test.ts rename to src/workbench/extensions/manager/composables/useImportFailedDetection.test.ts index 5a6614ca0..d548af8da 100644 --- a/tests-ui/tests/composables/useImportFailedDetection.test.ts +++ b/src/workbench/extensions/manager/composables/useImportFailedDetection.test.ts @@ -11,8 +11,8 @@ import * as conflictDetectionStore from '@/workbench/extensions/manager/stores/c vi.mock('@/workbench/extensions/manager/stores/comfyManagerStore') vi.mock('@/workbench/extensions/manager/stores/conflictDetectionStore') vi.mock('@/services/dialogService') -vi.mock('vue-i18n', async (importOriginal) => { - const actual = await importOriginal() +vi.mock('vue-i18n', async () => { + const actual = await vi.importActual('vue-i18n') return { ...actual, useI18n: () => ({ @@ -44,7 +44,8 @@ describe('useImportFailedDetection', () => { > mockDialogService = { - showErrorDialog: vi.fn() + showErrorDialog: vi.fn(), + showImportFailedNodeDialog: vi.fn() } as unknown as ReturnType vi.mocked(comfyManagerStore.useComfyManagerStore).mockReturnValue( @@ -226,13 +227,22 @@ describe('useImportFailedDetection', () => { showImportFailedDialog() - expect(mockDialogService.showErrorDialog).toHaveBeenCalledWith( - expect.any(Error), - { - title: 'manager.failedToInstall', - reportType: 'importFailedError' + expect(mockDialogService.showImportFailedNodeDialog).toHaveBeenCalledWith({ + conflictedPackages: expect.arrayContaining([ + expect.objectContaining({ + package_id: 'test-package', + package_name: 'Test Package', + conflicts: expect.arrayContaining([ + expect.objectContaining({ + type: 'import_failed' + }) + ]) + }) + ]), + dialogComponentProps: { + onClose: undefined } - ) + }) }) it('should handle null packageId', () => { diff --git a/src/workbench/extensions/manager/composables/useImportFailedDetection.ts b/src/workbench/extensions/manager/composables/useImportFailedDetection.ts index affd47c3a..1edc65006 100644 --- a/src/workbench/extensions/manager/composables/useImportFailedDetection.ts +++ b/src/workbench/extensions/manager/composables/useImportFailedDetection.ts @@ -1,11 +1,13 @@ import { computed, unref } from 'vue' import type { ComputedRef } from 'vue' -import { useI18n } from 'vue-i18n' import { useDialogService } from '@/services/dialogService' import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore' import { useConflictDetectionStore } from '@/workbench/extensions/manager/stores/conflictDetectionStore' -import type { ConflictDetail } from '@/workbench/extensions/manager/types/conflictDetectionTypes' +import type { + ConflictDetail, + ConflictDetectionResult +} from '@/workbench/extensions/manager/types/conflictDetectionTypes' /** * Extracting import failed conflicts from conflict list @@ -24,22 +26,18 @@ function extractImportFailedConflicts(conflicts?: ConflictDetail[] | null) { * Creating import failed dialog */ function createImportFailedDialog() { - const { t } = useI18n() - const { showErrorDialog } = useDialogService() + const { showImportFailedNodeDialog } = useDialogService() - return (importFailedInfo: ConflictDetail[] | null) => { - if (importFailedInfo) { - const errorMessage = - importFailedInfo - .map((conflict) => conflict.required_value) - .filter(Boolean) - .join('\n') || t('manager.importFailedGenericError') - - const error = new Error(errorMessage) - - showErrorDialog(error, { - title: t('manager.failedToInstall'), - reportType: 'importFailedError' + return ( + conflictedPackages: ConflictDetectionResult[] | null, + onClose?: () => void + ) => { + if (conflictedPackages && conflictedPackages.length > 0) { + showImportFailedNodeDialog({ + conflictedPackages, + dialogComponentProps: { + onClose + } }) } } @@ -74,13 +72,16 @@ export function useImportFailedDetection( return importFailedInfo.value !== null }) - const showImportFailedDialog = createImportFailedDialog() + const openDialog = createImportFailedDialog() return { importFailedInfo, importFailed, - showImportFailedDialog: () => - showImportFailedDialog(importFailedInfo.value), + showImportFailedDialog: (onClose?: () => void) => { + if (conflicts.value) { + openDialog([conflicts.value], onClose) + } + }, isInstalled } } diff --git a/tests-ui/tests/composables/useManagerQueue.test.ts b/src/workbench/extensions/manager/composables/useManagerQueue.test.ts similarity index 97% rename from tests-ui/tests/composables/useManagerQueue.test.ts rename to src/workbench/extensions/manager/composables/useManagerQueue.test.ts index 502867b8b..740a1e5cf 100644 --- a/tests-ui/tests/composables/useManagerQueue.test.ts +++ b/src/workbench/extensions/manager/composables/useManagerQueue.test.ts @@ -4,13 +4,6 @@ import { ref } from 'vue' import { useManagerQueue } from '@/workbench/extensions/manager/composables/useManagerQueue' import type { components } from '@/workbench/extensions/manager/types/generatedManagerTypes' -// Mock dialog service -vi.mock('@/services/dialogService', () => ({ - useDialogService: () => ({ - showManagerProgressDialog: vi.fn() - }) -})) - // Mock the app API vi.mock('@/scripts/app', () => ({ app: { diff --git a/src/workbench/extensions/manager/composables/useManagerQueue.ts b/src/workbench/extensions/manager/composables/useManagerQueue.ts index 3886b40bf..7ac796414 100644 --- a/src/workbench/extensions/manager/composables/useManagerQueue.ts +++ b/src/workbench/extensions/manager/composables/useManagerQueue.ts @@ -4,7 +4,6 @@ import type { Ref } from 'vue' import { computed, ref } from 'vue' import { app } from '@/scripts/app' -import { useDialogService } from '@/services/dialogService' import { normalizePackKeys } from '@/utils/packUtils' import type { components } from '@/workbench/extensions/manager/types/generatedManagerTypes' @@ -27,8 +26,6 @@ export const useManagerQueue = ( taskQueue: Ref, installedPacks: Ref> ) => { - const { showManagerProgressDialog } = useDialogService() - // Task queue state (read-only from server) const maxHistoryItems = ref(64) const isLoading = ref(false) @@ -113,15 +110,6 @@ export const useManagerQueue = ( (event: CustomEvent) => { if (event?.type === MANAGER_WS_TASK_DONE_NAME && event.detail?.state) { updateTaskState(event.detail.state) - - // If no more tasks are running/pending, hide the progress dialog - if (allTasksDone.value) { - setTimeout(() => { - if (allTasksDone.value) { - showManagerProgressDialog() - } - }, 1000) // Small delay to let users see completion - } } } ) @@ -133,9 +121,6 @@ export const useManagerQueue = ( (event: CustomEvent) => { if (event?.type === MANAGER_WS_TASK_STARTED_NAME && event.detail?.state) { updateTaskState(event.detail.state) - - // Show progress dialog when a task starts - showManagerProgressDialog() } } ) diff --git a/tests-ui/tests/composables/useManagerState.test.ts b/src/workbench/extensions/manager/composables/useManagerState.test.ts similarity index 100% rename from tests-ui/tests/composables/useManagerState.test.ts rename to src/workbench/extensions/manager/composables/useManagerState.test.ts diff --git a/tests-ui/tests/store/comfyManagerStore.test.ts b/src/workbench/extensions/manager/stores/comfyManagerStore.test.ts similarity index 99% rename from tests-ui/tests/store/comfyManagerStore.test.ts rename to src/workbench/extensions/manager/stores/comfyManagerStore.test.ts index d131a0fc9..d1754d59a 100644 --- a/tests-ui/tests/store/comfyManagerStore.test.ts +++ b/src/workbench/extensions/manager/stores/comfyManagerStore.test.ts @@ -17,12 +17,6 @@ vi.mock('@/workbench/extensions/manager/services/comfyManagerService', () => ({ useComfyManagerService: vi.fn() })) -vi.mock('@/services/dialogService', () => ({ - useDialogService: () => ({ - showManagerProgressDialog: vi.fn() - }) -})) - vi.mock('@/workbench/extensions/manager/composables/useManagerQueue', () => { const enqueueTaskMock = vi.fn() diff --git a/src/workbench/extensions/manager/stores/comfyManagerStore.ts b/src/workbench/extensions/manager/stores/comfyManagerStore.ts index 06097cbe7..df6df758a 100644 --- a/src/workbench/extensions/manager/stores/comfyManagerStore.ts +++ b/src/workbench/extensions/manager/stores/comfyManagerStore.ts @@ -8,7 +8,7 @@ import { useCachedRequest } from '@/composables/useCachedRequest' import { useServerLogs } from '@/composables/useServerLogs' import { api } from '@/scripts/api' import { app } from '@/scripts/app' -import { useDialogService } from '@/services/dialogService' + import { normalizePackKeys } from '@/utils/packUtils' import { useManagerQueue } from '@/workbench/extensions/manager/composables/useManagerQueue' import { useComfyManagerService } from '@/workbench/extensions/manager/services/comfyManagerService' @@ -32,7 +32,6 @@ type UpdateAllPacksParams = components['schemas']['UpdateAllPacksParams'] export const useComfyManagerStore = defineStore('comfyManager', () => { const { t } = useI18n() const managerService = useComfyManagerService() - const { showManagerProgressDialog } = useDialogService() const installedPacks = ref({}) const enabledPacksIds = ref>(new Set()) @@ -204,8 +203,6 @@ export const useComfyManagerStore = defineStore('comfyManager', () => { }) try { - // Show progress dialog immediately when task is queued - showManagerProgressDialog() managerQueue.isProcessing.value = true // Prepare logging hook @@ -392,44 +389,3 @@ export const useComfyManagerStore = defineStore('comfyManager', () => { enablePack } }) - -/** - * Store for state of the manager progress dialog content. - * The dialog itself is managed by the dialog store. This store is used to - * manage the visibility of the dialog's content, header, footer. - */ -export const useManagerProgressDialogStore = defineStore( - 'managerProgressDialog', - () => { - const isExpanded = ref(false) - const activeTabIndex = ref(0) - - const setActiveTabIndex = (index: number) => { - activeTabIndex.value = index - } - - const getActiveTabIndex = () => { - return activeTabIndex.value - } - - const toggle = () => { - isExpanded.value = !isExpanded.value - } - - const collapse = () => { - isExpanded.value = false - } - - const expand = () => { - isExpanded.value = true - } - return { - isExpanded, - toggle, - collapse, - expand, - setActiveTabIndex, - getActiveTabIndex - } - } -) diff --git a/tests-ui/tests/stores/conflictDetectionStore.test.ts b/src/workbench/extensions/manager/stores/conflictDetectionStore.test.ts similarity index 100% rename from tests-ui/tests/stores/conflictDetectionStore.test.ts rename to src/workbench/extensions/manager/stores/conflictDetectionStore.test.ts diff --git a/tests-ui/tests/utils/conflictUtils.test.ts b/src/workbench/extensions/manager/utils/conflictUtils.test.ts similarity index 100% rename from tests-ui/tests/utils/conflictUtils.test.ts rename to src/workbench/extensions/manager/utils/conflictUtils.test.ts diff --git a/tests-ui/tests/workbench/extensions/manager/utils/graphHasMissingNodes.test.ts b/src/workbench/extensions/manager/utils/graphHasMissingNodes.test.ts similarity index 100% rename from tests-ui/tests/workbench/extensions/manager/utils/graphHasMissingNodes.test.ts rename to src/workbench/extensions/manager/utils/graphHasMissingNodes.test.ts diff --git a/tests-ui/tests/utils/systemCompatibility.test.ts b/src/workbench/extensions/manager/utils/systemCompatibility.test.ts similarity index 100% rename from tests-ui/tests/utils/systemCompatibility.test.ts rename to src/workbench/extensions/manager/utils/systemCompatibility.test.ts diff --git a/tests-ui/tests/utils/versionUtil.test.ts b/src/workbench/extensions/manager/utils/versionUtil.test.ts similarity index 100% rename from tests-ui/tests/utils/versionUtil.test.ts rename to src/workbench/extensions/manager/utils/versionUtil.test.ts diff --git a/tests-ui/utils/modelMetadataUtil.test.ts b/src/workbench/utils/modelMetadataUtil.test.ts similarity index 100% rename from tests-ui/utils/modelMetadataUtil.test.ts rename to src/workbench/utils/modelMetadataUtil.test.ts diff --git a/tests-ui/tests/utils/nodeDefOrderingUtil.test.ts b/src/workbench/utils/nodeDefOrderingUtil.test.ts similarity index 100% rename from tests-ui/tests/utils/nodeDefOrderingUtil.test.ts rename to src/workbench/utils/nodeDefOrderingUtil.test.ts diff --git a/tests-ui/CLAUDE.md b/tests-ui/CLAUDE.md deleted file mode 100644 index 10e5d660b..000000000 --- a/tests-ui/CLAUDE.md +++ /dev/null @@ -1,23 +0,0 @@ -# Unit Testing Guidelines - -## Running Tests -- Single file: `pnpm test:unit -- ` -- All tests: `pnpm test:unit` -- Wrong Examples: - - Still runs all tests: `pnpm test:unit ` - -## Testing Approach - -- Write tests for new features -- Run single tests for performance -- Follow existing test patterns - -## Test Structure - -- Check @tests-ui/README.md for guidelines -- Use existing test utilities -- Mock external dependencies - -## Mocking -- Read: https://vitest.dev/api/mock.html -- Critical: Always prefer vitest mock functions over writing verbose manual mocks diff --git a/tests-ui/fixtures/historySortingFixtures.ts b/tests-ui/fixtures/historySortingFixtures.ts deleted file mode 100644 index a7b630667..000000000 --- a/tests-ui/fixtures/historySortingFixtures.ts +++ /dev/null @@ -1,258 +0,0 @@ -/** - * @fileoverview Test fixtures for history V2 timestamp-based sorting - */ -import type { HistoryResponseV2 } from '@/platform/remote/comfyui/history/types/historyV2Types' - -export const historyV2WithMissingTimestamp: HistoryResponseV2 = { - history: [ - { - prompt_id: 'item-timestamp-1000', - prompt: { - priority: 0, - prompt_id: 'item-timestamp-1000', - extra_data: { - client_id: 'test-client' - } - }, - outputs: { - '1': { - images: [{ filename: 'test1.png', type: 'output', subfolder: '' }] - } - }, - status: { - status_str: 'success', - completed: true, - messages: [ - [ - 'execution_success', - { prompt_id: 'item-timestamp-1000', timestamp: 1000 } - ] - ] - } - }, - { - prompt_id: 'item-timestamp-2000', - prompt: { - priority: 0, - prompt_id: 'item-timestamp-2000', - extra_data: { - client_id: 'test-client' - } - }, - outputs: { - '2': { - images: [{ filename: 'test2.png', type: 'output', subfolder: '' }] - } - }, - status: { - status_str: 'success', - completed: true, - messages: [ - [ - 'execution_success', - { prompt_id: 'item-timestamp-2000', timestamp: 2000 } - ] - ] - } - }, - { - prompt_id: 'item-no-timestamp', - prompt: { - priority: 0, - prompt_id: 'item-no-timestamp', - extra_data: { - client_id: 'test-client' - } - }, - outputs: { - '3': { - images: [{ filename: 'test3.png', type: 'output', subfolder: '' }] - } - }, - status: { - status_str: 'success', - completed: true, - messages: [] - } - } - ] -} - -export const historyV2FiveItemsSorting: HistoryResponseV2 = { - history: [ - { - prompt_id: 'item-timestamp-3000', - prompt: { - priority: 0, - prompt_id: 'item-timestamp-3000', - extra_data: { client_id: 'test-client' } - }, - outputs: { - '1': { - images: [{ filename: 'test1.png', type: 'output', subfolder: '' }] - } - }, - status: { - status_str: 'success', - completed: true, - messages: [ - [ - 'execution_success', - { prompt_id: 'item-timestamp-3000', timestamp: 3000 } - ] - ] - } - }, - { - prompt_id: 'item-timestamp-1000', - prompt: { - priority: 0, - prompt_id: 'item-timestamp-1000', - extra_data: { client_id: 'test-client' } - }, - outputs: { - '2': { - images: [{ filename: 'test2.png', type: 'output', subfolder: '' }] - } - }, - status: { - status_str: 'success', - completed: true, - messages: [ - [ - 'execution_success', - { prompt_id: 'item-timestamp-1000', timestamp: 1000 } - ] - ] - } - }, - { - prompt_id: 'item-timestamp-5000', - prompt: { - priority: 0, - prompt_id: 'item-timestamp-5000', - extra_data: { client_id: 'test-client' } - }, - outputs: { - '3': { - images: [{ filename: 'test3.png', type: 'output', subfolder: '' }] - } - }, - status: { - status_str: 'success', - completed: true, - messages: [ - [ - 'execution_success', - { prompt_id: 'item-timestamp-5000', timestamp: 5000 } - ] - ] - } - }, - { - prompt_id: 'item-timestamp-2000', - prompt: { - priority: 0, - prompt_id: 'item-timestamp-2000', - extra_data: { client_id: 'test-client' } - }, - outputs: { - '4': { - images: [{ filename: 'test4.png', type: 'output', subfolder: '' }] - } - }, - status: { - status_str: 'success', - completed: true, - messages: [ - [ - 'execution_success', - { prompt_id: 'item-timestamp-2000', timestamp: 2000 } - ] - ] - } - }, - { - prompt_id: 'item-timestamp-4000', - prompt: { - priority: 0, - prompt_id: 'item-timestamp-4000', - extra_data: { client_id: 'test-client' } - }, - outputs: { - '5': { - images: [{ filename: 'test5.png', type: 'output', subfolder: '' }] - } - }, - status: { - status_str: 'success', - completed: true, - messages: [ - [ - 'execution_success', - { prompt_id: 'item-timestamp-4000', timestamp: 4000 } - ] - ] - } - } - ] -} - -export const historyV2MultipleNoTimestamp: HistoryResponseV2 = { - history: [ - { - prompt_id: 'item-no-timestamp-1', - prompt: { - priority: 0, - prompt_id: 'item-no-timestamp-1', - extra_data: { client_id: 'test-client' } - }, - outputs: { - '1': { - images: [{ filename: 'test1.png', type: 'output', subfolder: '' }] - } - }, - status: { - status_str: 'success', - completed: true, - messages: [] - } - }, - { - prompt_id: 'item-no-timestamp-2', - prompt: { - priority: 0, - prompt_id: 'item-no-timestamp-2', - extra_data: { client_id: 'test-client' } - }, - outputs: { - '2': { - images: [{ filename: 'test2.png', type: 'output', subfolder: '' }] - } - }, - status: { - status_str: 'success', - completed: true, - messages: [] - } - }, - { - prompt_id: 'item-no-timestamp-3', - prompt: { - priority: 0, - prompt_id: 'item-no-timestamp-3', - extra_data: { client_id: 'test-client' } - }, - outputs: { - '3': { - images: [{ filename: 'test3.png', type: 'output', subfolder: '' }] - } - }, - status: { - status_str: 'success', - completed: true, - messages: [] - } - } - ] -} diff --git a/tests-ui/stores/templateRankingStore.test.ts b/tests-ui/stores/templateRankingStore.test.ts new file mode 100644 index 000000000..cb3a9539d --- /dev/null +++ b/tests-ui/stores/templateRankingStore.test.ts @@ -0,0 +1,135 @@ +import { createPinia, setActivePinia } from 'pinia' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { useTemplateRankingStore } from '@/stores/templateRankingStore' + +// Mock axios +vi.mock('axios', () => ({ + default: { + get: vi.fn() + } +})) + +describe('templateRankingStore', () => { + beforeEach(() => { + setActivePinia(createPinia()) + vi.clearAllMocks() + }) + + describe('computeFreshness', () => { + it('returns 1.0 for brand new template (today)', () => { + const store = useTemplateRankingStore() + const today = new Date().toISOString().split('T')[0] + const freshness = store.computeFreshness(today) + expect(freshness).toBeCloseTo(1.0, 1) + }) + + it('returns ~0.5 for 90-day old template', () => { + const store = useTemplateRankingStore() + const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000) + .toISOString() + .split('T')[0] + const freshness = store.computeFreshness(ninetyDaysAgo) + expect(freshness).toBeCloseTo(0.5, 1) + }) + + it('returns 0.1 minimum for very old template', () => { + const store = useTemplateRankingStore() + const freshness = store.computeFreshness('2020-01-01') + expect(freshness).toBe(0.1) + }) + + it('returns 0.5 for undefined date', () => { + const store = useTemplateRankingStore() + expect(store.computeFreshness(undefined)).toBe(0.5) + }) + + it('returns 0.5 for invalid date', () => { + const store = useTemplateRankingStore() + expect(store.computeFreshness('not-a-date')).toBe(0.5) + }) + }) + + describe('computeDefaultScore', () => { + it('uses default searchRank of 5 when not provided', () => { + const store = useTemplateRankingStore() + // Set largestUsageScore to avoid NaN when usage is 0 + store.largestUsageScore = 100 + const score = store.computeDefaultScore('2024-01-01', undefined, 0) + // With no usage score loaded, usage = 0 + // internal = 5/10 = 0.5, freshness ~0.1 (old date) + // score = 0 * 0.5 + 0.5 * 0.3 + 0.1 * 0.2 = 0.15 + 0.02 = 0.17 + expect(score).toBeCloseTo(0.17, 1) + }) + + it('high searchRank (10) boosts score', () => { + const store = useTemplateRankingStore() + store.largestUsageScore = 100 + const lowRank = store.computeDefaultScore('2024-01-01', 1, 0) + const highRank = store.computeDefaultScore('2024-01-01', 10, 0) + expect(highRank).toBeGreaterThan(lowRank) + }) + + it('low searchRank (1) demotes score', () => { + const store = useTemplateRankingStore() + store.largestUsageScore = 100 + const neutral = store.computeDefaultScore('2024-01-01', 5, 0) + const demoted = store.computeDefaultScore('2024-01-01', 1, 0) + expect(demoted).toBeLessThan(neutral) + }) + + it('searchRank difference is significant', () => { + const store = useTemplateRankingStore() + store.largestUsageScore = 100 + const rank1 = store.computeDefaultScore('2024-01-01', 1, 0) + const rank10 = store.computeDefaultScore('2024-01-01', 10, 0) + // Difference should be 0.9 * 0.3 = 0.27 (30% weight, 0.9 range) + expect(rank10 - rank1).toBeCloseTo(0.27, 2) + }) + }) + + describe('computePopularScore', () => { + it('does not use searchRank', () => { + const store = useTemplateRankingStore() + store.largestUsageScore = 100 + // Popular score ignores searchRank - just usage + freshness + const score1 = store.computePopularScore('2024-01-01', 0) + const score2 = store.computePopularScore('2024-01-01', 0) + expect(score1).toBe(score2) + }) + + it('newer templates score higher', () => { + const store = useTemplateRankingStore() + store.largestUsageScore = 100 + const today = new Date().toISOString().split('T')[0] + const oldScore = store.computePopularScore('2020-01-01', 0) + const newScore = store.computePopularScore(today, 0) + expect(newScore).toBeGreaterThan(oldScore) + }) + }) + + describe('searchRank edge cases', () => { + it('handles searchRank of 0 (should still work, treated as very low)', () => { + const store = useTemplateRankingStore() + store.largestUsageScore = 100 + const score = store.computeDefaultScore('2024-01-01', 0, 0) + expect(score).toBeGreaterThanOrEqual(0) + }) + + it('handles searchRank above 10 (clamping not enforced, but works)', () => { + const store = useTemplateRankingStore() + store.largestUsageScore = 100 + const rank10 = store.computeDefaultScore('2024-01-01', 10, 0) + const rank15 = store.computeDefaultScore('2024-01-01', 15, 0) + expect(rank15).toBeGreaterThan(rank10) + }) + + it('handles negative searchRank', () => { + const store = useTemplateRankingStore() + store.largestUsageScore = 100 + const score = store.computeDefaultScore('2024-01-01', -5, 0) + // Should still compute, just negative contribution from searchRank + expect(typeof score).toBe('number') + }) + }) +}) diff --git a/tests-ui/tests/components/dialog/footer/ManagerProgressFooter.test.ts b/tests-ui/tests/components/dialog/footer/ManagerProgressFooter.test.ts deleted file mode 100644 index 024e025b8..000000000 --- a/tests-ui/tests/components/dialog/footer/ManagerProgressFooter.test.ts +++ /dev/null @@ -1,486 +0,0 @@ -import { mount } from '@vue/test-utils' -import { createPinia, setActivePinia } from 'pinia' -import PrimeVue from 'primevue/config' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { nextTick } from 'vue' -import { createI18n } from 'vue-i18n' - -import { useSettingStore } from '@/platform/settings/settingStore' -import { useCommandStore } from '@/stores/commandStore' -import { useDialogStore } from '@/stores/dialogStore' -import ManagerProgressFooter from '@/workbench/extensions/manager/components/ManagerProgressFooter.vue' -import { useComfyManagerService } from '@/workbench/extensions/manager/services/comfyManagerService' -import { - useComfyManagerStore, - useManagerProgressDialogStore -} from '@/workbench/extensions/manager/stores/comfyManagerStore' -import type { TaskLog } from '@/workbench/extensions/manager/types/comfyManagerTypes' - -// Mock modules -vi.mock('@/workbench/extensions/manager/stores/comfyManagerStore') -vi.mock('@/stores/dialogStore') -vi.mock('@/platform/settings/settingStore') -vi.mock('@/stores/commandStore') -vi.mock('@/workbench/extensions/manager/services/comfyManagerService') -vi.mock( - '@/workbench/extensions/manager/composables/useConflictDetection', - () => ({ - useConflictDetection: vi.fn(() => ({ - conflictedPackages: { value: [] }, - runFullConflictAnalysis: vi.fn().mockResolvedValue(undefined) - })) - }) -) - -// Mock useEventListener to capture the event handler -let reconnectHandler: (() => void) | null = null -vi.mock('@vueuse/core', async () => { - const actual = await vi.importActual('@vueuse/core') - return { - ...actual, - useEventListener: vi.fn( - (_target: any, event: string, handler: any, _options: any) => { - if (event === 'reconnected') { - reconnectHandler = handler - } - } - ) - } -}) -vi.mock('@/platform/workflow/core/services/workflowService', () => ({ - useWorkflowService: vi.fn(() => ({ - reloadCurrentWorkflow: vi.fn().mockResolvedValue(undefined) - })) -})) -vi.mock('@/stores/workspace/colorPaletteStore', () => ({ - useColorPaletteStore: vi.fn(() => ({ - completedActivePalette: { - light_theme: false - } - })) -})) - -// Helper function to mount component with required setup -const mountComponent = (options: { captureError?: boolean } = {}) => { - const pinia = createPinia() - setActivePinia(pinia) - - const i18n = createI18n({ - legacy: false, - locale: 'en', - messages: { - en: { - g: { - close: 'Close', - progressCountOf: 'of' - }, - contextMenu: { - Collapse: 'Collapse', - Expand: 'Expand' - }, - manager: { - clickToFinishSetup: 'Click', - applyChanges: 'Apply Changes', - toFinishSetup: 'to finish setup', - restartingBackend: 'Restarting backend to apply changes...', - extensionsSuccessfullyInstalled: - 'Extension(s) successfully installed and are ready to use!', - restartToApplyChanges: 'To apply changes, please restart ComfyUI', - installingDependencies: 'Installing dependencies...' - } - } - } - }) - - const config: any = { - global: { - plugins: [pinia, PrimeVue, i18n] - } - } - - // Add error handler for tests that expect errors - if (options.captureError) { - config.global.config = { - errorHandler: () => { - // Suppress error in test - } - } - } - - return mount(ManagerProgressFooter, config) -} - -describe('ManagerProgressFooter', () => { - const mockTaskLogs: TaskLog[] = [] - - const mockComfyManagerStore = { - taskLogs: mockTaskLogs, - allTasksDone: true, - isProcessingTasks: false, - succeededTasksIds: [] as string[], - failedTasksIds: [] as string[], - taskHistory: {} as Record, - taskQueue: null, - resetTaskState: vi.fn(), - clearLogs: vi.fn(), - setStale: vi.fn(), - // Add other required properties - isLoading: { value: false }, - error: { value: null }, - statusMessage: { value: 'DONE' }, - installedPacks: {}, - installedPacksIds: new Set(), - isPackInstalled: vi.fn(), - isPackEnabled: vi.fn(), - getInstalledPackVersion: vi.fn(), - refreshInstalledList: vi.fn(), - installPack: vi.fn(), - uninstallPack: vi.fn(), - updatePack: vi.fn(), - updateAllPacks: vi.fn(), - disablePack: vi.fn(), - enablePack: vi.fn() - } - - const mockDialogStore = { - closeDialog: vi.fn(), - // Add other required properties - dialogStack: { value: [] }, - showDialog: vi.fn(), - $id: 'dialog', - $state: {} as any, - $patch: vi.fn(), - $reset: vi.fn(), - $subscribe: vi.fn(), - $dispose: vi.fn(), - $onAction: vi.fn() - } - - const mockSettingStore = { - get: vi.fn().mockReturnValue(false), - set: vi.fn(), - // Add other required properties - settingValues: { value: {} }, - settingsById: { value: {} }, - exists: vi.fn(), - getDefaultValue: vi.fn(), - loadSettingValues: vi.fn(), - updateValue: vi.fn(), - $id: 'setting', - $state: {} as any, - $patch: vi.fn(), - $reset: vi.fn(), - $subscribe: vi.fn(), - $dispose: vi.fn(), - $onAction: vi.fn() - } - - const mockProgressDialogStore = { - isExpanded: false, - toggle: vi.fn(), - collapse: vi.fn(), - expand: vi.fn() - } - - const mockCommandStore = { - execute: vi.fn().mockResolvedValue(undefined) - } - - const mockComfyManagerService = { - rebootComfyUI: vi.fn().mockResolvedValue(null) - } - - beforeEach(() => { - vi.clearAllMocks() - // Create new pinia instance for each test - const pinia = createPinia() - setActivePinia(pinia) - - // Reset task logs - mockTaskLogs.length = 0 - mockComfyManagerStore.taskLogs = mockTaskLogs - // Reset event handler - reconnectHandler = null - - vi.mocked(useComfyManagerStore).mockReturnValue( - mockComfyManagerStore as any - ) - vi.mocked(useDialogStore).mockReturnValue(mockDialogStore as any) - vi.mocked(useSettingStore).mockReturnValue(mockSettingStore as any) - vi.mocked(useManagerProgressDialogStore).mockReturnValue( - mockProgressDialogStore as any - ) - vi.mocked(useCommandStore).mockReturnValue(mockCommandStore as any) - vi.mocked(useComfyManagerService).mockReturnValue( - mockComfyManagerService as any - ) - }) - - describe('State 1: Queue Running', () => { - it('should display loading spinner and progress counter when queue is running', async () => { - // Setup queue running state - mockComfyManagerStore.isProcessingTasks = true - mockComfyManagerStore.succeededTasksIds = ['1', '2'] - mockComfyManagerStore.failedTasksIds = [] - mockComfyManagerStore.taskHistory = { - '1': { taskName: 'Installing pack1' }, - '2': { taskName: 'Installing pack2' }, - '3': { taskName: 'Installing pack3' } - } - mockTaskLogs.push( - { taskName: 'Installing pack1', taskId: '1', logs: [] }, - { taskName: 'Installing pack2', taskId: '2', logs: [] }, - { taskName: 'Installing pack3', taskId: '3', logs: [] } - ) - - const wrapper = mountComponent() - - // Check loading spinner exists (DotSpinner component) - expect(wrapper.find('.inline-flex').exists()).toBe(true) - - // Check current task name is displayed - expect(wrapper.text()).toContain('Installing pack3') - - // Check progress counter (completed: 2 of 3) - expect(wrapper.text()).toMatch(/2.*of.*3/) - - // Check expand/collapse button exists - const expandButton = wrapper.find('[aria-label="Expand"]') - expect(expandButton.exists()).toBe(true) - - // Check Apply Changes button is NOT shown - expect(wrapper.text()).not.toContain('Apply Changes') - }) - - it('should toggle expansion when expand button is clicked', async () => { - mockComfyManagerStore.isProcessingTasks = true - mockTaskLogs.push({ taskName: 'Installing', taskId: '1', logs: [] }) - - const wrapper = mountComponent() - - const expandButton = wrapper.find('[aria-label="Expand"]') - await expandButton.trigger('click') - - expect(mockProgressDialogStore.toggle).toHaveBeenCalled() - }) - }) - - describe('State 2: Tasks Completed (Waiting for Restart)', () => { - it('should display check mark and Apply Changes button when all tasks are done', async () => { - // Setup tasks completed state - mockComfyManagerStore.isProcessingTasks = false - mockTaskLogs.push( - { taskName: 'Installed pack1', taskId: '1', logs: [] }, - { taskName: 'Installed pack2', taskId: '2', logs: [] } - ) - mockComfyManagerStore.allTasksDone = true - - const wrapper = mountComponent() - - // Check check mark emoji - expect(wrapper.text()).toContain('✅') - - // Check restart message - expect(wrapper.text()).toContain( - 'To apply changes, please restart ComfyUI' - ) - expect(wrapper.text()).toContain('Apply Changes') - - // Check Apply Changes button exists - const applyButton = wrapper - .findAll('button') - .find((btn) => btn.text().includes('Apply Changes')) - expect(applyButton).toBeTruthy() - - // Check no progress counter - expect(wrapper.text()).not.toMatch(/\d+.*of.*\d+/) - }) - }) - - describe('State 3: Restarting', () => { - it('should display restarting message and spinner during restart', async () => { - // Setup completed state first - mockComfyManagerStore.isProcessingTasks = false - mockComfyManagerStore.allTasksDone = true - - const wrapper = mountComponent() - - // Click Apply Changes to trigger restart - const applyButton = wrapper - .findAll('button') - .find((btn) => btn.text().includes('Apply Changes')) - await applyButton?.trigger('click') - - // Wait for state update - await nextTick() - - // Check restarting message - expect(wrapper.text()).toContain('Restarting backend to apply changes...') - - // Check loading spinner during restart - expect(wrapper.find('.inline-flex').exists()).toBe(true) - - // Check Apply Changes button is hidden - expect(wrapper.text()).not.toContain('Apply Changes') - }) - }) - - describe('State 4: Restart Completed', () => { - it('should display success message and auto-close after 3 seconds', async () => { - vi.useFakeTimers() - - // Setup completed state - mockComfyManagerStore.isProcessingTasks = false - mockComfyManagerStore.allTasksDone = true - - const wrapper = mountComponent() - - // Trigger restart - const applyButton = wrapper - .findAll('button') - .find((btn) => btn.text().includes('Apply Changes')) - await applyButton?.trigger('click') - - // Wait for event listener to be set up - await nextTick() - - // Trigger the reconnect handler directly - if (reconnectHandler) { - await reconnectHandler() - } - - // Wait for restart completed state - await nextTick() - - // Check success message - expect(wrapper.text()).toContain('🎉') - expect(wrapper.text()).toContain( - 'Extension(s) successfully installed and are ready to use!' - ) - - // Check dialog closes after 3 seconds - vi.advanceTimersByTime(3000) - - await nextTick() - - expect(mockDialogStore.closeDialog).toHaveBeenCalledWith({ - key: 'global-manager-progress-dialog' - }) - expect(mockComfyManagerStore.resetTaskState).toHaveBeenCalled() - - vi.useRealTimers() - }) - }) - - describe('Common Features', () => { - it('should always display close button', async () => { - const wrapper = mountComponent() - - const closeButton = wrapper.find('[aria-label="Close"]') - expect(closeButton.exists()).toBe(true) - }) - - it('should close dialog when close button is clicked', async () => { - const wrapper = mountComponent() - - const closeButton = wrapper.find('[aria-label="Close"]') - await closeButton.trigger('click') - - expect(mockDialogStore.closeDialog).toHaveBeenCalledWith({ - key: 'global-manager-progress-dialog' - }) - }) - }) - - describe('Toast Management', () => { - it('should suppress reconnection toasts during restart', async () => { - mockComfyManagerStore.isProcessingTasks = false - mockComfyManagerStore.allTasksDone = true - mockSettingStore.get.mockReturnValue(false) // Original setting - - const wrapper = mountComponent() - - // Click Apply Changes - const applyButton = wrapper - .findAll('button') - .find((btn) => btn.text().includes('Apply Changes')) - await applyButton?.trigger('click') - - // Check toast setting was disabled - expect(mockSettingStore.set).toHaveBeenCalledWith( - 'Comfy.Toast.DisableReconnectingToast', - true - ) - }) - - it('should restore toast settings after restart completes', async () => { - mockComfyManagerStore.isProcessingTasks = false - mockComfyManagerStore.allTasksDone = true - mockSettingStore.get.mockReturnValue(false) // Original setting - - const wrapper = mountComponent() - - // Click Apply Changes - const applyButton = wrapper - .findAll('button') - .find((btn) => btn.text().includes('Apply Changes')) - await applyButton?.trigger('click') - - // Wait for event listener to be set up - await nextTick() - - // Trigger the reconnect handler directly - if (reconnectHandler) { - await reconnectHandler() - } - - // Wait for settings restoration - await nextTick() - - expect(mockSettingStore.set).toHaveBeenCalledWith( - 'Comfy.Toast.DisableReconnectingToast', - false // Restored to original - ) - }) - }) - - describe('Error Handling', () => { - it('should restore state and close dialog on restart error', async () => { - mockComfyManagerStore.isProcessingTasks = false - mockComfyManagerStore.allTasksDone = true - - // Mock restart to throw error - mockComfyManagerService.rebootComfyUI.mockRejectedValue( - new Error('Restart failed') - ) - - const wrapper = mountComponent({ captureError: true }) - - // Click Apply Changes - const applyButton = wrapper - .findAll('button') - .find((btn) => btn.text().includes('Apply Changes')) - - expect(applyButton).toBeTruthy() - - // The component throws the error but Vue Test Utils catches it - // We need to check if the error handling logic was executed - await applyButton!.trigger('click').catch(() => { - // Error is expected, ignore it - }) - - // Wait for error handling - await nextTick() - - // Check dialog was closed on error - expect(mockDialogStore.closeDialog).toHaveBeenCalled() - // Check toast settings were restored - expect(mockSettingStore.set).toHaveBeenCalledWith( - 'Comfy.Toast.DisableReconnectingToast', - false - ) - // Check that the error handler was called - expect(mockComfyManagerService.rebootComfyUI).toHaveBeenCalled() - }) - }) -}) diff --git a/tests-ui/tests/composables/graph/useGraphNodeManager.test.ts b/tests-ui/tests/composables/graph/useGraphNodeManager.test.ts new file mode 100644 index 000000000..ba22d98cd --- /dev/null +++ b/tests-ui/tests/composables/graph/useGraphNodeManager.test.ts @@ -0,0 +1,49 @@ +import { setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { describe, expect, it, vi } from 'vitest' +import { nextTick, watch } from 'vue' + +import { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager' +import { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph' +import { NodeSlotType } from '@/lib/litegraph/src/types/globalEnums' + +setActivePinia(createTestingPinia()) + +function createTestGraph() { + const graph = new LGraph() + const node = new LGraphNode('test') + node.addInput('input', 'INT') + node.addWidget('number', 'testnum', 2, () => undefined, {}) + graph.add(node) + + const { vueNodeData } = useGraphNodeManager(graph) + const onReactivityUpdate = vi.fn() + watch(vueNodeData, onReactivityUpdate) + + return [node, graph, onReactivityUpdate] as const +} + +describe('Node Reactivity', () => { + it('should trigger on callback', async () => { + const [node, , onReactivityUpdate] = createTestGraph() + + node.widgets![0].callback!(2) + await nextTick() + expect(onReactivityUpdate).toHaveBeenCalledTimes(1) + }) + + it('should remain reactive after a connection is made', async () => { + const [node, graph, onReactivityUpdate] = createTestGraph() + + graph.trigger('node:slot-links:changed', { + nodeId: '1', + slotType: NodeSlotType.INPUT + }) + await nextTick() + onReactivityUpdate.mockClear() + + node.widgets![0].callback!(2) + await nextTick() + expect(onReactivityUpdate).toHaveBeenCalledTimes(1) + }) +}) diff --git a/tests-ui/tests/litegraph.test.ts b/tests-ui/tests/litegraph.test.ts deleted file mode 100644 index 068da2fb5..000000000 --- a/tests-ui/tests/litegraph.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { describe, expect, it } from 'vitest' - -import { LGraph, LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph' - -function swapNodes(nodes: LGraphNode[]) { - const firstNode = nodes[0] - const lastNode = nodes[nodes.length - 1] - nodes[0] = lastNode - nodes[nodes.length - 1] = firstNode - return nodes -} - -function createGraph(...nodes: LGraphNode[]) { - const graph = new LGraph() - nodes.forEach((node) => graph.add(node)) - return graph -} - -class DummyNode extends LGraphNode { - constructor() { - super('dummy') - } -} - -describe('LGraph', () => { - it('should serialize deterministic node order', async () => { - LiteGraph.registerNodeType('dummy', DummyNode) - const node1 = new DummyNode() - const node2 = new DummyNode() - const graph = createGraph(node1, node2) - - const result1 = graph.serialize({ sortNodes: true }) - expect(result1.nodes).not.toHaveLength(0) - graph._nodes = swapNodes(graph.nodes) - const result2 = graph.serialize({ sortNodes: true }) - - expect(result1).toEqual(result2) - }) -}) diff --git a/tests-ui/tests/litegraph/README.md b/tests-ui/tests/litegraph/README.md deleted file mode 100644 index eacc17a4c..000000000 --- a/tests-ui/tests/litegraph/README.md +++ /dev/null @@ -1,59 +0,0 @@ -# LiteGraph Tests - -This directory contains the test suite for the LiteGraph library. - -## Structure - -``` -litegraph/ -├── core/ # Core functionality tests (LGraph, LGraphNode, etc.) -├── canvas/ # Canvas-related tests (rendering, interactions) -├── infrastructure/ # Infrastructure tests (Rectangle, utilities) -├── subgraph/ # Subgraph-specific tests -├── utils/ # Utility function tests -└── fixtures/ # Test helpers, fixtures, and assets -``` - -## Running Tests - -```bash -# Run all litegraph tests -pnpm test:unit -- tests-ui/tests/litegraph/ - -# Run specific subdirectory -pnpm test:unit -- tests-ui/tests/litegraph/core/ - -# Run single test file -pnpm test:unit -- tests-ui/tests/litegraph/core/LGraph.test.ts -``` - -## Migration Status - -These tests were migrated from `src/lib/litegraph/test/` to centralize test infrastructure. Currently, some tests are marked with `.skip` due to import/setup issues that need to be resolved. - -### TODO: Fix Skipped Tests - -The following test files have been temporarily disabled and need fixes: -- Most subgraph tests (circular dependency issues) -- Some core tests (missing test utilities) -- Canvas tests (mock setup issues) - -See individual test files marked with `// TODO: Fix these tests after migration` for specific issues. - -## Writing New Tests - -1. Always import from the barrel export to avoid circular dependencies: - ```typescript - import { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph' - ``` - -2. Use the test fixtures from `fixtures/` directory -3. Follow existing patterns for test organization - -## Test Fixtures - -Test fixtures and helpers are located in the `fixtures/` directory: -- `testExtensions.ts` - Custom vitest extensions -- `subgraphHelpers.ts` - Helpers for creating test subgraphs -- `subgraphFixtures.ts` - Common subgraph test scenarios -- `assets/` - Test data files \ No newline at end of file diff --git a/tests-ui/tests/litegraph/core/__snapshots__/ConfigureGraph.test.ts.snap b/tests-ui/tests/litegraph/core/__snapshots__/ConfigureGraph.test.ts.snap deleted file mode 100644 index 7e9cd555b..000000000 --- a/tests-ui/tests/litegraph/core/__snapshots__/ConfigureGraph.test.ts.snap +++ /dev/null @@ -1,331 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`LGraph configure() > LGraph matches previous snapshot (normal configure() usage) > configuredBasicGraph 1`] = ` -LGraph { - "_groups": [ - LGraphGroup { - "_bounding": Float32Array [ - 20, - 20, - 1, - 3, - ], - "_children": Set {}, - "_nodes": [], - "_pos": Float32Array [ - 20, - 20, - ], - "_size": Float32Array [ - 1, - 3, - ], - "color": "#6029aa", - "flags": {}, - "font": undefined, - "font_size": 14, - "graph": [Circular], - "id": 123, - "isPointInside": [Function], - "selected": undefined, - "setDirtyCanvas": [Function], - "title": "A group to test with", - }, - ], - "_input_nodes": undefined, - "_last_trigger_time": undefined, - "_links": Map {}, - "_nodes": [ - LGraphNode { - "_collapsed_width": undefined, - "_level": undefined, - "_pos": Float32Array [ - 10, - 10, - ], - "_posSize": Float32Array [ - 10, - 10, - 140, - 60, - ], - "_relative_id": undefined, - "_shape": undefined, - "_size": Float32Array [ - 140, - 60, - ], - "action_call": undefined, - "action_triggered": undefined, - "badgePosition": "top-left", - "badges": [], - "bgcolor": undefined, - "block_delete": undefined, - "boxcolor": undefined, - "clip_area": undefined, - "clonable": undefined, - "color": undefined, - "console": undefined, - "exec_version": undefined, - "execute_triggered": undefined, - "flags": {}, - "freeWidgetSpace": undefined, - "gotFocusAt": undefined, - "graph": [Circular], - "has_errors": undefined, - "id": 1, - "ignore_remove": undefined, - "inputs": [], - "last_serialization": undefined, - "locked": undefined, - "lostFocusAt": undefined, - "mode": 0, - "mouseOver": undefined, - "onMouseDown": [Function], - "order": 0, - "outputs": [], - "progress": undefined, - "properties": {}, - "properties_info": [], - "redraw_on_mouse": undefined, - "removable": undefined, - "resizable": undefined, - "selected": undefined, - "serialize_widgets": undefined, - "showAdvanced": undefined, - "strokeStyles": { - "error": [Function], - "selected": [Function], - }, - "title": "LGraphNode", - "title_buttons": [], - "type": "mustBeSet", - "widgets": undefined, - "widgets_start_y": undefined, - "widgets_up": undefined, - }, - ], - "_nodes_by_id": { - "1": LGraphNode { - "_collapsed_width": undefined, - "_level": undefined, - "_pos": Float32Array [ - 10, - 10, - ], - "_posSize": Float32Array [ - 10, - 10, - 140, - 60, - ], - "_relative_id": undefined, - "_shape": undefined, - "_size": Float32Array [ - 140, - 60, - ], - "action_call": undefined, - "action_triggered": undefined, - "badgePosition": "top-left", - "badges": [], - "bgcolor": undefined, - "block_delete": undefined, - "boxcolor": undefined, - "clip_area": undefined, - "clonable": undefined, - "color": undefined, - "console": undefined, - "exec_version": undefined, - "execute_triggered": undefined, - "flags": {}, - "freeWidgetSpace": undefined, - "gotFocusAt": undefined, - "graph": [Circular], - "has_errors": undefined, - "id": 1, - "ignore_remove": undefined, - "inputs": [], - "last_serialization": undefined, - "locked": undefined, - "lostFocusAt": undefined, - "mode": 0, - "mouseOver": undefined, - "onMouseDown": [Function], - "order": 0, - "outputs": [], - "progress": undefined, - "properties": {}, - "properties_info": [], - "redraw_on_mouse": undefined, - "removable": undefined, - "resizable": undefined, - "selected": undefined, - "serialize_widgets": undefined, - "showAdvanced": undefined, - "strokeStyles": { - "error": [Function], - "selected": [Function], - }, - "title": "LGraphNode", - "title_buttons": [], - "type": "mustBeSet", - "widgets": undefined, - "widgets_start_y": undefined, - "widgets_up": undefined, - }, - }, - "_nodes_executable": [], - "_nodes_in_order": [ - LGraphNode { - "_collapsed_width": undefined, - "_level": undefined, - "_pos": Float32Array [ - 10, - 10, - ], - "_posSize": Float32Array [ - 10, - 10, - 140, - 60, - ], - "_relative_id": undefined, - "_shape": undefined, - "_size": Float32Array [ - 140, - 60, - ], - "action_call": undefined, - "action_triggered": undefined, - "badgePosition": "top-left", - "badges": [], - "bgcolor": undefined, - "block_delete": undefined, - "boxcolor": undefined, - "clip_area": undefined, - "clonable": undefined, - "color": undefined, - "console": undefined, - "exec_version": undefined, - "execute_triggered": undefined, - "flags": {}, - "freeWidgetSpace": undefined, - "gotFocusAt": undefined, - "graph": [Circular], - "has_errors": undefined, - "id": 1, - "ignore_remove": undefined, - "inputs": [], - "last_serialization": undefined, - "locked": undefined, - "lostFocusAt": undefined, - "mode": 0, - "mouseOver": undefined, - "onMouseDown": [Function], - "order": 0, - "outputs": [], - "progress": undefined, - "properties": {}, - "properties_info": [], - "redraw_on_mouse": undefined, - "removable": undefined, - "resizable": undefined, - "selected": undefined, - "serialize_widgets": undefined, - "showAdvanced": undefined, - "strokeStyles": { - "error": [Function], - "selected": [Function], - }, - "title": "LGraphNode", - "title_buttons": [], - "type": "mustBeSet", - "widgets": undefined, - "widgets_start_y": undefined, - "widgets_up": undefined, - }, - ], - "_subgraphs": Map {}, - "_version": 3, - "catch_errors": true, - "config": {}, - "elapsed_time": 0.01, - "errors_in_execution": undefined, - "events": CustomEventTarget {}, - "execution_time": undefined, - "execution_timer_id": undefined, - "extra": {}, - "filter": undefined, - "fixedtime": 0, - "fixedtime_lapse": 0.01, - "globaltime": 0, - "id": "ca9da7d8-fddd-4707-ad32-67be9be13140", - "iteration": 0, - "last_update_time": 0, - "links": Map {}, - "list_of_graphcanvas": null, - "nodes_actioning": [], - "nodes_executedAction": [], - "nodes_executing": [], - "revision": 0, - "runningtime": 0, - "starttime": 0, - "state": { - "lastGroupId": 123, - "lastLinkId": 0, - "lastNodeId": 1, - "lastRerouteId": 0, - }, - "status": 1, - "vars": {}, - "version": 1, -} -`; - -exports[`LGraph configure() > LGraph matches previous snapshot (normal configure() usage) > configuredMinGraph 1`] = ` -LGraph { - "_groups": [], - "_input_nodes": undefined, - "_last_trigger_time": undefined, - "_links": Map {}, - "_nodes": [], - "_nodes_by_id": {}, - "_nodes_executable": [], - "_nodes_in_order": [], - "_subgraphs": Map {}, - "_version": 0, - "catch_errors": true, - "config": {}, - "elapsed_time": 0.01, - "errors_in_execution": undefined, - "events": CustomEventTarget {}, - "execution_time": undefined, - "execution_timer_id": undefined, - "extra": {}, - "filter": undefined, - "fixedtime": 0, - "fixedtime_lapse": 0.01, - "globaltime": 0, - "id": "d175890f-716a-4ece-ba33-1d17a513b7be", - "iteration": 0, - "last_update_time": 0, - "links": Map {}, - "list_of_graphcanvas": null, - "nodes_actioning": [], - "nodes_executedAction": [], - "nodes_executing": [], - "revision": 0, - "runningtime": 0, - "starttime": 0, - "state": { - "lastGroupId": 0, - "lastLinkId": 0, - "lastNodeId": 0, - "lastRerouteId": 0, - }, - "status": 1, - "vars": {}, - "version": 1, -} -`; diff --git a/tests-ui/tests/litegraph/core/__snapshots__/LGraph_constructor.test.ts.snap b/tests-ui/tests/litegraph/core/__snapshots__/LGraph_constructor.test.ts.snap deleted file mode 100644 index cd54aa094..000000000 --- a/tests-ui/tests/litegraph/core/__snapshots__/LGraph_constructor.test.ts.snap +++ /dev/null @@ -1,331 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`LGraph (constructor only) > Matches previous snapshot > basicLGraph 1`] = ` -LGraph { - "_groups": [ - LGraphGroup { - "_bounding": Float32Array [ - 20, - 20, - 1, - 3, - ], - "_children": Set {}, - "_nodes": [], - "_pos": Float32Array [ - 20, - 20, - ], - "_size": Float32Array [ - 1, - 3, - ], - "color": "#6029aa", - "flags": {}, - "font": undefined, - "font_size": 14, - "graph": [Circular], - "id": 123, - "isPointInside": [Function], - "selected": undefined, - "setDirtyCanvas": [Function], - "title": "A group to test with", - }, - ], - "_input_nodes": undefined, - "_last_trigger_time": undefined, - "_links": Map {}, - "_nodes": [ - LGraphNode { - "_collapsed_width": undefined, - "_level": undefined, - "_pos": Float32Array [ - 10, - 10, - ], - "_posSize": Float32Array [ - 10, - 10, - 140, - 60, - ], - "_relative_id": undefined, - "_shape": undefined, - "_size": Float32Array [ - 140, - 60, - ], - "action_call": undefined, - "action_triggered": undefined, - "badgePosition": "top-left", - "badges": [], - "bgcolor": undefined, - "block_delete": undefined, - "boxcolor": undefined, - "clip_area": undefined, - "clonable": undefined, - "color": undefined, - "console": undefined, - "exec_version": undefined, - "execute_triggered": undefined, - "flags": {}, - "freeWidgetSpace": undefined, - "gotFocusAt": undefined, - "graph": [Circular], - "has_errors": undefined, - "id": 1, - "ignore_remove": undefined, - "inputs": [], - "last_serialization": undefined, - "locked": undefined, - "lostFocusAt": undefined, - "mode": 0, - "mouseOver": undefined, - "onMouseDown": [Function], - "order": 0, - "outputs": [], - "progress": undefined, - "properties": {}, - "properties_info": [], - "redraw_on_mouse": undefined, - "removable": undefined, - "resizable": undefined, - "selected": undefined, - "serialize_widgets": undefined, - "showAdvanced": undefined, - "strokeStyles": { - "error": [Function], - "selected": [Function], - }, - "title": "LGraphNode", - "title_buttons": [], - "type": "mustBeSet", - "widgets": undefined, - "widgets_start_y": undefined, - "widgets_up": undefined, - }, - ], - "_nodes_by_id": { - "1": LGraphNode { - "_collapsed_width": undefined, - "_level": undefined, - "_pos": Float32Array [ - 10, - 10, - ], - "_posSize": Float32Array [ - 10, - 10, - 140, - 60, - ], - "_relative_id": undefined, - "_shape": undefined, - "_size": Float32Array [ - 140, - 60, - ], - "action_call": undefined, - "action_triggered": undefined, - "badgePosition": "top-left", - "badges": [], - "bgcolor": undefined, - "block_delete": undefined, - "boxcolor": undefined, - "clip_area": undefined, - "clonable": undefined, - "color": undefined, - "console": undefined, - "exec_version": undefined, - "execute_triggered": undefined, - "flags": {}, - "freeWidgetSpace": undefined, - "gotFocusAt": undefined, - "graph": [Circular], - "has_errors": undefined, - "id": 1, - "ignore_remove": undefined, - "inputs": [], - "last_serialization": undefined, - "locked": undefined, - "lostFocusAt": undefined, - "mode": 0, - "mouseOver": undefined, - "onMouseDown": [Function], - "order": 0, - "outputs": [], - "progress": undefined, - "properties": {}, - "properties_info": [], - "redraw_on_mouse": undefined, - "removable": undefined, - "resizable": undefined, - "selected": undefined, - "serialize_widgets": undefined, - "showAdvanced": undefined, - "strokeStyles": { - "error": [Function], - "selected": [Function], - }, - "title": "LGraphNode", - "title_buttons": [], - "type": "mustBeSet", - "widgets": undefined, - "widgets_start_y": undefined, - "widgets_up": undefined, - }, - }, - "_nodes_executable": [], - "_nodes_in_order": [ - LGraphNode { - "_collapsed_width": undefined, - "_level": undefined, - "_pos": Float32Array [ - 10, - 10, - ], - "_posSize": Float32Array [ - 10, - 10, - 140, - 60, - ], - "_relative_id": undefined, - "_shape": undefined, - "_size": Float32Array [ - 140, - 60, - ], - "action_call": undefined, - "action_triggered": undefined, - "badgePosition": "top-left", - "badges": [], - "bgcolor": undefined, - "block_delete": undefined, - "boxcolor": undefined, - "clip_area": undefined, - "clonable": undefined, - "color": undefined, - "console": undefined, - "exec_version": undefined, - "execute_triggered": undefined, - "flags": {}, - "freeWidgetSpace": undefined, - "gotFocusAt": undefined, - "graph": [Circular], - "has_errors": undefined, - "id": 1, - "ignore_remove": undefined, - "inputs": [], - "last_serialization": undefined, - "locked": undefined, - "lostFocusAt": undefined, - "mode": 0, - "mouseOver": undefined, - "onMouseDown": [Function], - "order": 0, - "outputs": [], - "progress": undefined, - "properties": {}, - "properties_info": [], - "redraw_on_mouse": undefined, - "removable": undefined, - "resizable": undefined, - "selected": undefined, - "serialize_widgets": undefined, - "showAdvanced": undefined, - "strokeStyles": { - "error": [Function], - "selected": [Function], - }, - "title": "LGraphNode", - "title_buttons": [], - "type": "mustBeSet", - "widgets": undefined, - "widgets_start_y": undefined, - "widgets_up": undefined, - }, - ], - "_subgraphs": Map {}, - "_version": 3, - "catch_errors": true, - "config": {}, - "elapsed_time": 0.01, - "errors_in_execution": undefined, - "events": CustomEventTarget {}, - "execution_time": undefined, - "execution_timer_id": undefined, - "extra": {}, - "filter": undefined, - "fixedtime": 0, - "fixedtime_lapse": 0.01, - "globaltime": 0, - "id": "ca9da7d8-fddd-4707-ad32-67be9be13140", - "iteration": 0, - "last_update_time": 0, - "links": Map {}, - "list_of_graphcanvas": null, - "nodes_actioning": [], - "nodes_executedAction": [], - "nodes_executing": [], - "revision": 0, - "runningtime": 0, - "starttime": 0, - "state": { - "lastGroupId": 123, - "lastLinkId": 0, - "lastNodeId": 1, - "lastRerouteId": 0, - }, - "status": 1, - "vars": {}, - "version": 1, -} -`; - -exports[`LGraph (constructor only) > Matches previous snapshot > minLGraph 1`] = ` -LGraph { - "_groups": [], - "_input_nodes": undefined, - "_last_trigger_time": undefined, - "_links": Map {}, - "_nodes": [], - "_nodes_by_id": {}, - "_nodes_executable": [], - "_nodes_in_order": [], - "_subgraphs": Map {}, - "_version": 0, - "catch_errors": true, - "config": {}, - "elapsed_time": 0.01, - "errors_in_execution": undefined, - "events": CustomEventTarget {}, - "execution_time": undefined, - "execution_timer_id": undefined, - "extra": {}, - "filter": undefined, - "fixedtime": 0, - "fixedtime_lapse": 0.01, - "globaltime": 0, - "id": "d175890f-716a-4ece-ba33-1d17a513b7be", - "iteration": 0, - "last_update_time": 0, - "links": Map {}, - "list_of_graphcanvas": null, - "nodes_actioning": [], - "nodes_executedAction": [], - "nodes_executing": [], - "revision": 0, - "runningtime": 0, - "starttime": 0, - "state": { - "lastGroupId": 0, - "lastLinkId": 0, - "lastNodeId": 0, - "lastRerouteId": 0, - }, - "status": 1, - "vars": {}, - "version": 1, -} -`; diff --git a/tests-ui/tsconfig.json b/tests-ui/tsconfig.json deleted file mode 100644 index f600c4a7f..000000000 --- a/tests-ui/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - /* Test files should not be compiled */ - "noEmit": true, - // "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "resolveJsonModule": true - }, - "include": [ - "**/*.ts", - ] -} diff --git a/vite.config.mts b/vite.config.mts index 7ad4dea5a..e5f070726 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -482,12 +482,6 @@ export default defineConfig({ : [] }, - test: { - globals: true, - environment: 'happy-dom', - setupFiles: ['./vitest.setup.ts'] - }, - define: { __COMFYUI_FRONTEND_VERSION__: JSON.stringify( process.env.npm_package_version diff --git a/vitest.config.ts b/vitest.config.ts index 1dab6ffd1..b74f7496c 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -19,8 +19,8 @@ export default defineConfig({ setupFiles: ['./vitest.setup.ts'], retry: process.env.CI ? 2 : 0, include: [ - 'tests-ui/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}', - 'src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}' + 'src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}', + 'packages/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}' ], coverage: { reporter: ['text', 'json', 'html'] @@ -39,7 +39,6 @@ export default defineConfig({ '@/utils/formatUtil': '/packages/shared-frontend-utils/src/formatUtil.ts', '@/utils/networkUtil': '/packages/shared-frontend-utils/src/networkUtil.ts', - '@tests-ui': '/tests-ui', '@': '/src' } },