diff --git a/.github/workflows/ci-tests-e2e-forks.yaml b/.github/workflows/ci-tests-e2e-forks.yaml index c1828b7fb7..8f039f1c4c 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 12ccfae935..90bf4112cf 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: @@ -37,7 +37,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 container: - image: ghcr.io/comfy-org/comfyui-ci-container:0.0.8 + image: ghcr.io/comfy-org/comfyui-ci-container:0.0.10 credentials: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} @@ -85,7 +85,7 @@ jobs: needs: setup runs-on: ubuntu-latest container: - image: ghcr.io/comfy-org/comfyui-ci-container:0.0.8 + image: ghcr.io/comfy-org/comfyui-ci-container:0.0.10 credentials: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} @@ -222,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 e1c51c546c..0ca6967217 100644 --- a/.github/workflows/pr-update-playwright-expectations.yaml +++ b/.github/workflows/pr-update-playwright-expectations.yaml @@ -77,7 +77,7 @@ jobs: needs: setup runs-on: ubuntu-latest container: - image: ghcr.io/comfy-org/comfyui-ci-container:0.0.8 + image: ghcr.io/comfy-org/comfyui-ci-container:0.0.10 credentials: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} diff --git a/.i18nrc.cjs b/.i18nrc.cjs index 14c9585919..86ce06eaa3 100644 --- a/.i18nrc.cjs +++ b/.i18nrc.cjs @@ -6,10 +6,11 @@ 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'], + 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 897094aded..5b7c126e9d 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 0d80ec8a0b..743572be3c 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) @@ -268,6 +276,8 @@ When referencing Comfy-Org repos: - 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 diff --git a/browser_tests/tests/menu.spec.ts b/browser_tests/tests/menu.spec.ts index 2c3ffb3d46..1ba1e55240 100644 --- a/browser_tests/tests/menu.spec.ts +++ b/browser_tests/tests/menu.spec.ts @@ -133,8 +133,11 @@ test.describe('Menu', () => { // Checkmark should be invisible again (panel is hidden) await expect(checkmark).toHaveClass(/invisible/) - // Click outside to close menu - await comfyPage.page.locator('body').click({ position: { x: 10, y: 10 } }) + // Click in top-right corner to close menu (avoid hamburger menu at top-left) + const viewport = comfyPage.page.viewportSize()! + await comfyPage.page + .locator('body') + .click({ position: { x: viewport.width - 10, y: 10 } }) // Verify menu is now closed await expect(menu).not.toBeVisible() diff --git a/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-default-workflow-mobile-chrome-linux.png b/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-default-workflow-mobile-chrome-linux.png index b24b687297..a7fef0346e 100644 Binary files a/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-default-workflow-mobile-chrome-linux.png and b/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-default-workflow-mobile-chrome-linux.png differ diff --git a/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-empty-canvas-mobile-chrome-linux.png b/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-empty-canvas-mobile-chrome-linux.png index db09fd07ac..6b617420d8 100644 Binary files a/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-empty-canvas-mobile-chrome-linux.png and b/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-empty-canvas-mobile-chrome-linux.png differ diff --git a/browser_tests/tests/viewport.spec.ts-snapshots/viewport-fits-when-saved-offscreen-chromium-linux.png b/browser_tests/tests/viewport.spec.ts-snapshots/viewport-fits-when-saved-offscreen-chromium-linux.png index cab519db76..c20ae5f89f 100644 Binary files a/browser_tests/tests/viewport.spec.ts-snapshots/viewport-fits-when-saved-offscreen-chromium-linux.png and b/browser_tests/tests/viewport.spec.ts-snapshots/viewport-fits-when-saved-offscreen-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png index 8b66d2c4a7..98db3de787 100644 Binary files a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png and b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png index 076371135c..b83a4ed596 100644 Binary files a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png and b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts-snapshots/vue-nodes-paned-with-touch-mobile-chrome-linux.png b/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts-snapshots/vue-nodes-paned-with-touch-mobile-chrome-linux.png index 151ee4a489..607efc16cd 100644 Binary files a/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts-snapshots/vue-nodes-paned-with-touch-mobile-chrome-linux.png and b/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts-snapshots/vue-nodes-paned-with-touch-mobile-chrome-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png index 2141828f04..f30ba7467c 100644 Binary files a/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png index 5d1cd36842..8c0051b30c 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png index 6485cbec7d..3f07068c90 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png index 8aad10ee6a..2085c39b3d 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png index 10bfe26698..a2332e2d45 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png index 09a9eb864a..bf2c01fc81 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png index 6ee2d87071..a43c024ddd 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png index d71dccc7bd..a9221a4046 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png index 53980f0b99..63a8c81d75 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png index 9c442e5f58..2cc234ffb0 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-touch-mobile-chrome-linux.png b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-touch-mobile-chrome-linux.png index 8562ca7e74..ac178e4a20 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-touch-mobile-chrome-linux.png and b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-touch-mobile-chrome-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts-snapshots/vue-node-bypassed-state-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts-snapshots/vue-node-bypassed-state-chromium-linux.png index bbaee40630..658ab53405 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts-snapshots/vue-node-bypassed-state-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts-snapshots/vue-node-bypassed-state-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png index 7a1bb89ceb..cfb61ef10a 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-dark-all-colors-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-dark-all-colors-chromium-linux.png index e6ea9ad307..1e3fd5359d 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-dark-all-colors-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-dark-all-colors-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-light-all-colors-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-light-all-colors-chromium-linux.png index f822ef7a84..880c63ecaa 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-light-all-colors-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-light-all-colors-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png index 8bd81cbd91..b18d31a895 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts-snapshots/vue-nodes-upload-widgets-chromium-linux.png b/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts-snapshots/vue-nodes-upload-widgets-chromium-linux.png index 8d8b4dea8a..97085e9a3e 100644 Binary files a/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts-snapshots/vue-nodes-upload-widgets-chromium-linux.png and b/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts-snapshots/vue-nodes-upload-widgets-chromium-linux.png differ diff --git a/browser_tests/tests/widget.spec.ts-snapshots/image-preview-changed-by-combo-value-chromium-linux.png b/browser_tests/tests/widget.spec.ts-snapshots/image-preview-changed-by-combo-value-chromium-linux.png index 9c5bffa2d1..c639941a76 100644 Binary files a/browser_tests/tests/widget.spec.ts-snapshots/image-preview-changed-by-combo-value-chromium-linux.png and b/browser_tests/tests/widget.spec.ts-snapshots/image-preview-changed-by-combo-value-chromium-linux.png differ diff --git a/docs/testing/vitest-patterns.md b/docs/testing/vitest-patterns.md new file mode 100644 index 0000000000..2eb7c8e09d --- /dev/null +++ b/docs/testing/vitest-patterns.md @@ -0,0 +1,138 @@ +--- +globs: + - '**/*.test.ts' + - '**/*.spec.ts' +--- + +# Vitest Patterns + +## Setup + +Use `createTestingPinia` from `@pinia/testing`, not `createPinia`: + +```typescript +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +describe('MyStore', () => { + beforeEach(() => { + setActivePinia(createTestingPinia({ stubActions: false })) + vi.useFakeTimers() + vi.resetAllMocks() + }) + + afterEach(() => { + vi.useRealTimers() + }) +}) +``` + +**Why `stubActions: false`?** By default, testing pinia stubs all actions. Set to `false` when testing actual store behavior. + +## Mock Patterns + +### Reset all mocks at once + +```typescript +beforeEach(() => { + vi.resetAllMocks() // Not individual mock.mockReset() calls +}) +``` + +### Module mocks with vi.mock() + +```typescript +vi.mock('@/scripts/api', () => ({ + api: { + addEventListener: vi.fn(), + fetchData: vi.fn() + } +})) + +vi.mock('@/services/myService', () => ({ + myService: { + doThing: vi.fn() + } +})) +``` + +### Configure mocks in tests + +```typescript +import { api } from '@/scripts/api' +import { myService } from '@/services/myService' + +it('handles success', () => { + vi.mocked(myService.doThing).mockResolvedValue({ data: 'test' }) + // ... test code +}) +``` + +## Testing Event Listeners + +When a store registers event listeners at module load time: + +```typescript +function getEventHandler() { + const call = vi.mocked(api.addEventListener).mock.calls.find( + ([event]) => event === 'my_event' + ) + return call?.[1] as (e: CustomEvent) => void +} + +function dispatch(data: MyEventType) { + const handler = getEventHandler() + handler(new CustomEvent('my_event', { detail: data })) +} + +it('handles events', () => { + const store = useMyStore() + dispatch({ field: 'value' }) + expect(store.items).toHaveLength(1) +}) +``` + +## Testing with Fake Timers + +For stores with intervals, timeouts, or polling: + +```typescript +beforeEach(() => { + vi.useFakeTimers() +}) + +afterEach(() => { + vi.useRealTimers() +}) + +it('polls after delay', async () => { + const store = useMyStore() + store.startPolling() + + await vi.advanceTimersByTimeAsync(30000) + + expect(mockService.fetch).toHaveBeenCalled() +}) +``` + +## Assertion Style + +Prefer `.toHaveLength()` over `.length.toBe()`: + +```typescript +// Good +expect(store.items).toHaveLength(1) + +// Avoid +expect(store.items.length).toBe(1) +``` + +Use `.toMatchObject()` for partial matching: + +```typescript +expect(store.completedItems[0]).toMatchObject({ + id: 'task-123', + status: 'done' +}) +``` diff --git a/knip.config.ts b/knip.config.ts index f2907a266f..d6a4a75179 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -8,7 +8,8 @@ const config: KnipConfig = { 'src/assets/css/style.css', 'src/main.ts', 'src/scripts/ui/menu/index.ts', - 'src/types/index.ts' + 'src/types/index.ts', + 'src/storybook/mocks/**/*.ts' ], project: ['**/*.{js,ts,vue}', '*.{js,ts,mts}'] }, diff --git a/package.json b/package.json index 6dcf03f5b4..8672b0cd2e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@comfyorg/comfyui-frontend", "private": true, - "version": "1.37.3", + "version": "1.38.0", "type": "module", "repository": "https://github.com/Comfy-Org/ComfyUI_frontend", "homepage": "https://comfy.org", @@ -66,6 +66,7 @@ "@prettier/plugin-oxc": "catalog:", "@sentry/vite-plugin": "catalog:", "@storybook/addon-docs": "catalog:", + "@storybook/addon-mcp": "catalog:", "@storybook/vue3": "catalog:", "@storybook/vue3-vite": "catalog:", "@tailwindcss/vite": "catalog:", diff --git a/packages/design-system/src/css/style.css b/packages/design-system/src/css/style.css index 7fc951496b..551e26c62e 100644 --- a/packages/design-system/src/css/style.css +++ b/packages/design-system/src/css/style.css @@ -9,6 +9,8 @@ @config '../../tailwind.config.ts'; +@custom-variant touch (@media (hover: none)); + @theme { --text-xxs: 0.625rem; --text-xxs--line-height: calc(1 / 0.625); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6eea185643..b0fd3de72a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -84,6 +84,9 @@ catalogs: '@storybook/addon-docs': specifier: ^10.1.9 version: 10.1.9 + '@storybook/addon-mcp': + specifier: 0.1.6 + version: 0.1.6 '@storybook/vue3': specifier: ^10.1.9 version: 10.1.9 @@ -549,6 +552,9 @@ importers: '@storybook/addon-docs': specifier: 'catalog:' version: 10.1.9(@types/react@19.1.9)(esbuild@0.27.1)(rollup@4.53.5)(storybook@10.1.9(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)) + '@storybook/addon-mcp': + specifier: 'catalog:' + version: 0.1.6(storybook@10.1.9(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3) '@storybook/vue3': specifier: 'catalog:' version: 10.1.9(storybook@10.1.9(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vue@3.5.13(typescript@5.9.3)) @@ -3148,6 +3154,11 @@ packages: peerDependencies: storybook: ^10.1.9 + '@storybook/addon-mcp@0.1.6': + resolution: {integrity: sha512-+EagCHqwIb9tg3DKskEsXpsqQVnMljxgR5Tt3Bu0ZpWweB1HdMy+ok128gzNfTZ3r+5ljksr0q66YCEkrQwdDA==} + peerDependencies: + storybook: ^9.1.16 || ^10.0.0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0 + '@storybook/builder-vite@10.1.9': resolution: {integrity: sha512-rUILpjGV7gKfXrUeZzpNAer9PspB3LJI1d+gJHISx2Gs24bdneA3y/gu0fWw46ccOSIcwb91xoK5QxliJcWsWg==} peerDependencies: @@ -3181,6 +3192,9 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@storybook/mcp@0.1.1': + resolution: {integrity: sha512-+AivFDms1XkY2VUvZBBYy0co5qvRh20eYXYwhaDPQXX2Q4y96arSkWn22e/l3DQwA9Ywzv481vj4gl4zPrCQkg==} + '@storybook/react-dom-shim@10.1.9': resolution: {integrity: sha512-gJsR6fI1gG4DSin6sQx8RmGDQF8Lije0cZbxHyVedNleBsveGXIPFUKFVi+pRNdwBPni1Z2g/gYyHzkOEqPD2w==} peerDependencies: @@ -3453,6 +3467,26 @@ packages: '@tiptap/starter-kit@2.10.4': resolution: {integrity: sha512-tu/WCs9Mkr5Nt8c3/uC4VvAbQlVX0OY7ygcqdzHGUeG9zP3twdW7o5xM3kyDKR2++sbVzqu5Ll5qNU+1JZvPGQ==} + '@tmcp/adapter-valibot@0.1.5': + resolution: {integrity: sha512-9P2wrVYPngemNK0UvPb/opC722/jfd09QxXmme1TRp/wPsl98vpSk/MXt24BCMqBRv4Dvs0xxJH4KHDcjXW52Q==} + peerDependencies: + tmcp: ^1.17.0 + valibot: ^1.1.0 + + '@tmcp/session-manager@0.2.1': + resolution: {integrity: sha512-DOGy9LfufXCy1wfpGHZ6qPSDQtRnTVwOb71+41ffovTqzLMZlK3iLK/LIsekHxIiku+iIAUiqEKN+DHbqEm8IA==} + peerDependencies: + tmcp: ^1.16.3 + + '@tmcp/transport-http@0.8.3': + resolution: {integrity: sha512-gnoBjDBd8/ppl4WRrNKPKHlioCxE8D0zTyNUOzqUjsg0s6GRsyB5iMirh9lC4QjQt0NEOrI+sIJdz+9ymf0MDA==} + peerDependencies: + '@tmcp/auth': ^0.3.3 || ^0.4.0 + tmcp: ^1.18.0 + peerDependenciesMeta: + '@tmcp/auth': + optional: true + '@trivago/prettier-plugin-sort-imports@5.2.2': resolution: {integrity: sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA==} engines: {node: '>18.12'} @@ -3786,6 +3820,11 @@ packages: cpu: [x64] os: [win32] + '@valibot/to-json-schema@1.5.0': + resolution: {integrity: sha512-GE7DmSr1C2UCWPiV0upRH6mv0cCPsqYGs819fb6srCS1tWhyXrkGGe+zxUiwzn/L1BOfADH4sNjY/YHCuP8phQ==} + peerDependencies: + valibot: ^1.2.0 + '@vitejs/plugin-vue@6.0.3': resolution: {integrity: sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5170,6 +5209,9 @@ packages: jiti: optional: true + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + esm-resolve@1.0.11: resolution: {integrity: sha512-LxF0wfUQm3ldUDHkkV2MIbvvY0TgzIpJ420jHSV1Dm+IlplBEWiJTKWM61GtxUfvjV6iD4OtTYFGAGM2uuIUWg==} @@ -5978,6 +6020,9 @@ packages: json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-rpc-2.0@1.7.1: + resolution: {integrity: sha512-JqZjhjAanbpkXIzFE7u8mE/iFblawwlXtONaCvRqI+pyABVz7B4M1EUNpyVW+dZjqgQ2L5HFmZCmOCgUKm00hg==} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -7386,6 +7431,9 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + sqids@0.3.0: + resolution: {integrity: sha512-lOQK1ucVg+W6n3FhRwwSeUijxe93b51Bfz5PMRMihVf1iVkl82ePQG7V5vwrhzB11v0NtsR25PSZRGiSomJaJw==} + stable-hash-x@0.2.0: resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} engines: {node: '>=12.0.0'} @@ -7613,6 +7661,9 @@ packages: resolution: {integrity: sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==} hasBin: true + tmcp@1.19.0: + resolution: {integrity: sha512-wOY449EdaWDo7wLZEOVjeH9fn/AqfFF4f+3pDerCI8xHpy2Z8msUjAF0Vkg01aEFIdFMmiNDiY4hu6E7jVX79w==} + tmp@0.2.5: resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} engines: {node: '>=14.14'} @@ -7858,6 +7909,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + uri-template-matcher@1.1.2: + resolution: {integrity: sha512-uZc1h12jdO3m/R77SfTEOuo6VbMhgWznaawKpBjRGSJb7i91x5PgI37NQJtG+Cerxkk0yr1pylBY2qG1kQ+aEQ==} + use-sync-external-store@1.6.0: resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: @@ -7873,6 +7927,14 @@ packages: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true + valibot@1.2.0: + resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + vfile-message@4.0.3: resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} @@ -8019,6 +8081,9 @@ packages: vue-component-type-helpers@3.2.1: resolution: {integrity: sha512-gKV7XOkQl4urSuLHNY1tnVQf7wVgtb/mKbRyxSLWGZUY9RK7aDPhBenTjm+i8ZFe0zC2PZeHMPtOZXZfyaFOzQ==} + vue-component-type-helpers@3.2.2: + resolution: {integrity: sha512-x8C2nx5XlUNM0WirgfTkHjJGO/ABBxlANZDtHw2HclHtQnn+RFPTnbjMJn8jHZW4TlUam0asHcA14lf1C6Jb+A==} + vue-demi@0.14.10: resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} engines: {node: '>=12'} @@ -10949,6 +11014,18 @@ snapshots: - vite - webpack + '@storybook/addon-mcp@0.1.6(storybook@10.1.9(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)': + dependencies: + '@storybook/mcp': 0.1.1(typescript@5.9.3) + '@tmcp/adapter-valibot': 0.1.5(tmcp@1.19.0(typescript@5.9.3))(valibot@1.2.0(typescript@5.9.3)) + '@tmcp/transport-http': 0.8.3(tmcp@1.19.0(typescript@5.9.3)) + storybook: 10.1.9(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + tmcp: 1.19.0(typescript@5.9.3) + valibot: 1.2.0(typescript@5.9.3) + transitivePeerDependencies: + - '@tmcp/auth' + - typescript + '@storybook/builder-vite@10.1.9(esbuild@0.27.1)(rollup@4.53.5)(storybook@10.1.9(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))': dependencies: '@storybook/csf-plugin': 10.1.9(esbuild@0.27.1)(rollup@4.53.5)(storybook@10.1.9(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)) @@ -10978,6 +11055,16 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) + '@storybook/mcp@0.1.1(typescript@5.9.3)': + dependencies: + '@tmcp/adapter-valibot': 0.1.5(tmcp@1.19.0(typescript@5.9.3))(valibot@1.2.0(typescript@5.9.3)) + '@tmcp/transport-http': 0.8.3(tmcp@1.19.0(typescript@5.9.3)) + tmcp: 1.19.0(typescript@5.9.3) + valibot: 1.2.0(typescript@5.9.3) + transitivePeerDependencies: + - '@tmcp/auth' + - typescript + '@storybook/react-dom-shim@10.1.9(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.9(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))': dependencies: react: 19.2.3 @@ -11007,7 +11094,7 @@ snapshots: storybook: 10.1.9(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) type-fest: 2.19.0 vue: 3.5.13(typescript@5.9.3) - vue-component-type-helpers: 3.2.1 + vue-component-type-helpers: 3.2.2 '@swc/helpers@0.5.17': dependencies: @@ -11275,6 +11362,23 @@ snapshots: '@tiptap/extension-text-style': 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4)) '@tiptap/pm': 2.10.4 + '@tmcp/adapter-valibot@0.1.5(tmcp@1.19.0(typescript@5.9.3))(valibot@1.2.0(typescript@5.9.3))': + dependencies: + '@standard-schema/spec': 1.1.0 + '@valibot/to-json-schema': 1.5.0(valibot@1.2.0(typescript@5.9.3)) + tmcp: 1.19.0(typescript@5.9.3) + valibot: 1.2.0(typescript@5.9.3) + + '@tmcp/session-manager@0.2.1(tmcp@1.19.0(typescript@5.9.3))': + dependencies: + tmcp: 1.19.0(typescript@5.9.3) + + '@tmcp/transport-http@0.8.3(tmcp@1.19.0(typescript@5.9.3))': + dependencies: + '@tmcp/session-manager': 0.2.1(tmcp@1.19.0(typescript@5.9.3)) + esm-env: 1.2.2 + tmcp: 1.19.0(typescript@5.9.3) + '@trivago/prettier-plugin-sort-imports@5.2.2(@vue/compiler-sfc@3.5.25)(prettier@3.7.4)': dependencies: '@babel/generator': 7.28.5 @@ -11623,6 +11727,10 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true + '@valibot/to-json-schema@1.5.0(valibot@1.2.0(typescript@5.9.3))': + dependencies: + valibot: 1.2.0(typescript@5.9.3) + '@vitejs/plugin-vue@6.0.3(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.53 @@ -13303,6 +13411,8 @@ snapshots: transitivePeerDependencies: - supports-color + esm-env@1.2.2: {} + esm-resolve@1.0.11: {} espree@10.4.0: @@ -14189,6 +14299,8 @@ snapshots: json-parse-even-better-errors@2.3.1: {} + json-rpc-2.0@1.7.1: {} + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -16055,6 +16167,8 @@ snapshots: sprintf-js@1.0.3: {} + sqids@0.3.0: {} + stable-hash-x@0.2.0: {} stack-utils@2.0.6: @@ -16347,6 +16461,16 @@ snapshots: dependencies: tldts-core: 7.0.19 + tmcp@1.19.0(typescript@5.9.3): + dependencies: + '@standard-schema/spec': 1.1.0 + json-rpc-2.0: 1.7.1 + sqids: 0.3.0 + uri-template-matcher: 1.1.2 + valibot: 1.2.0(typescript@5.9.3) + transitivePeerDependencies: + - typescript + tmp@0.2.5: {} to-regex-range@5.0.1: @@ -16644,6 +16768,8 @@ snapshots: dependencies: punycode: 2.3.1 + uri-template-matcher@1.1.2: {} + use-sync-external-store@1.6.0(react@19.2.3): dependencies: react: 19.2.3 @@ -16654,6 +16780,10 @@ snapshots: uuid@11.1.0: {} + valibot@1.2.0(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 @@ -16914,6 +17044,8 @@ snapshots: vue-component-type-helpers@3.2.1: {} + vue-component-type-helpers@3.2.2: {} + vue-demi@0.14.10(vue@3.5.13(typescript@5.9.3)): dependencies: vue: 3.5.13(typescript@5.9.3) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index bd4638b0d2..56f3f394d8 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -29,6 +29,7 @@ catalog: '@sentry/vue': ^10.32.1 '@sparkjsdev/spark': ^0.1.10 '@storybook/addon-docs': ^10.1.9 + '@storybook/addon-mcp': 0.1.6 '@storybook/vue3': ^10.1.9 '@storybook/vue3-vite': ^10.1.9 '@tailwindcss/vite': ^4.1.12 diff --git a/scripts/cicd/extract-playwright-counts.ts b/scripts/cicd/extract-playwright-counts.ts index ff6f44db30..1a79228166 100755 --- a/scripts/cicd/extract-playwright-counts.ts +++ b/scripts/cicd/extract-playwright-counts.ts @@ -10,37 +10,158 @@ interface TestStats { finished?: number } +interface TestResult { + status: string + duration?: number + error?: { + message?: string + stack?: string + } + attachments?: Array<{ + name: string + path?: string + contentType: string + }> +} + +interface TestCase { + title: string + ok: boolean + outcome: string + results: TestResult[] +} + +interface Suite { + title: string + file: string + suites?: Suite[] + tests?: TestCase[] +} + +interface FullReportData { + stats?: TestStats + suites?: Suite[] +} + interface ReportData { stats?: TestStats } +interface FailedTest { + name: string + file: string + traceUrl?: string + error?: string +} + interface TestCounts { passed: number failed: number flaky: number skipped: number total: number + failures?: FailedTest[] +} + +/** + * Extract failed test details from Playwright report + */ +function extractFailedTests( + reportData: FullReportData, + baseUrl?: string +): FailedTest[] { + const failures: FailedTest[] = [] + + function processTest(test: TestCase, file: string, suitePath: string[]) { + // Check if test failed or is flaky + const hasFailed = test.results.some( + (r) => r.status === 'failed' || r.status === 'timedOut' + ) + const isFlaky = test.outcome === 'flaky' + + if (hasFailed || isFlaky) { + const fullTestName = [...suitePath, test.title] + .filter(Boolean) + .join(' › ') + const failedResult = test.results.find( + (r) => r.status === 'failed' || r.status === 'timedOut' + ) + + // Find trace attachment + let traceUrl: string | undefined + if (failedResult?.attachments) { + const traceAttachment = failedResult.attachments.find( + (a) => a.name === 'trace' && a.contentType === 'application/zip' + ) + if (traceAttachment?.path) { + // Convert local path to URL path + const tracePath = traceAttachment.path.replace(/\\/g, '/') + const traceFile = path.basename(tracePath) + if (baseUrl) { + // Construct trace viewer URL + const traceDataUrl = `${baseUrl}/data/${traceFile}` + traceUrl = `${baseUrl}/trace/?trace=${encodeURIComponent(traceDataUrl)}` + } + } + } + + failures.push({ + name: fullTestName, + file: file, + traceUrl, + error: failedResult?.error?.message + }) + } + } + + function processSuite(suite: Suite, parentPath: string[] = []) { + const suitePath = suite.title ? [...parentPath, suite.title] : parentPath + + // Process tests in this suite + if (suite.tests) { + for (const test of suite.tests) { + processTest(test, suite.file, suitePath) + } + } + + // Recursively process nested suites + if (suite.suites) { + for (const childSuite of suite.suites) { + processSuite(childSuite, suitePath) + } + } + } + + if (reportData.suites) { + for (const suite of reportData.suites) { + processSuite(suite) + } + } + + return failures } /** * Extract test counts from Playwright HTML report * @param reportDir - Path to the playwright-report directory - * @returns Test counts { passed, failed, flaky, skipped, total } + * @param baseUrl - Base URL of the deployed report (for trace links) + * @returns Test counts { passed, failed, flaky, skipped, total, failures } */ -function extractTestCounts(reportDir: string): TestCounts { +function extractTestCounts(reportDir: string, baseUrl?: string): TestCounts { const counts: TestCounts = { passed: 0, failed: 0, flaky: 0, skipped: 0, - total: 0 + total: 0, + failures: [] } try { // First, try to find report.json which Playwright generates with JSON reporter const jsonReportFile = path.join(reportDir, 'report.json') if (fs.existsSync(jsonReportFile)) { - const reportJson: ReportData = JSON.parse( + const reportJson: FullReportData = JSON.parse( fs.readFileSync(jsonReportFile, 'utf-8') ) if (reportJson.stats) { @@ -54,6 +175,12 @@ function extractTestCounts(reportDir: string): TestCounts { counts.failed = stats.unexpected || 0 counts.flaky = stats.flaky || 0 counts.skipped = stats.skipped || 0 + + // Extract detailed failure information + if (counts.failed > 0 || counts.flaky > 0) { + counts.failures = extractFailedTests(reportJson, baseUrl) + } + return counts } } @@ -169,15 +296,18 @@ function extractTestCounts(reportDir: string): TestCounts { // Main execution const reportDir = process.argv[2] +const baseUrl = process.argv[3] // Optional: base URL for trace links if (!reportDir) { - console.error('Usage: extract-playwright-counts.ts ') + console.error( + 'Usage: extract-playwright-counts.ts [base-url]' + ) process.exit(1) } -const counts = extractTestCounts(reportDir) +const counts = extractTestCounts(reportDir, baseUrl) // Output as JSON for easy parsing in shell script -console.log(JSON.stringify(counts)) +process.stdout.write(JSON.stringify(counts) + '\n') -export { extractTestCounts } +export { extractTestCounts, extractFailedTests } diff --git a/scripts/cicd/pr-playwright-deploy-and-comment.sh b/scripts/cicd/pr-playwright-deploy-and-comment.sh index 840203f44a..9332e60b7b 100755 --- a/scripts/cicd/pr-playwright-deploy-and-comment.sh +++ b/scripts/cicd/pr-playwright-deploy-and-comment.sh @@ -134,23 +134,22 @@ post_comment() { # Main execution if [ "$STATUS" = "starting" ]; then - # Post starting comment + # Post concise starting comment comment=$(cat < **Tests are starting...** +Tests started at $START_TIME UTC -⏰ Started at: $START_TIME UTC +
+📊 Browser Tests -### 🚀 Running Tests -- 🧪 **chromium**: Running tests... -- 🧪 **chromium-0.5x**: Running tests... -- 🧪 **chromium-2x**: Running tests... -- 🧪 **mobile-chrome**: Running tests... +- **chromium**: Running... +- **chromium-0.5x**: Running... +- **chromium-2x**: Running... +- **mobile-chrome**: Running... ---- -⏱️ Please wait while tests are running... +
EOF ) post_comment "$comment" @@ -189,7 +188,8 @@ else if command -v tsx > /dev/null 2>&1 && [ -f "$EXTRACT_SCRIPT" ]; then echo "Extracting counts from $REPORT_DIR using $EXTRACT_SCRIPT" >&2 - counts=$(tsx "$EXTRACT_SCRIPT" "$REPORT_DIR" 2>&1 || echo '{}') + # Pass the base URL so we can generate trace links + counts=$(tsx "$EXTRACT_SCRIPT" "$REPORT_DIR" "$url" 2>&1 || echo '{}') echo "Extracted counts for $browser: $counts" >&2 echo "$counts" > "$temp_dir/$i.counts" else @@ -286,43 +286,74 @@ else # Determine overall status if [ $total_failed -gt 0 ]; then status_icon="❌" - status_text="Some tests failed" + status_text="Failed" elif [ $total_flaky -gt 0 ]; then status_icon="⚠️" - status_text="Tests passed with flaky tests" + status_text="Passed with flaky tests" elif [ $total_tests -gt 0 ]; then status_icon="✅" - status_text="All tests passed!" + status_text="Passed" else status_icon="🕵🏻" - status_text="No test results found" + status_text="No test results" fi - # Generate completion comment + # Generate concise completion comment comment="$COMMENT_MARKER -## 🎭 Playwright Test Results - -$status_icon **$status_text** - -⏰ Completed at: $(date -u '+%m/%d/%Y, %I:%M:%S %p') UTC" +## 🎭 Playwright Tests: $status_icon **$status_text**" # Add summary counts if we have test data if [ $total_tests -gt 0 ]; then comment="$comment -### 📈 Summary -- **Total Tests:** $total_tests -- **Passed:** $total_passed ✅ -- **Failed:** $total_failed $([ $total_failed -gt 0 ] && echo '❌' || echo '') -- **Flaky:** $total_flaky $([ $total_flaky -gt 0 ] && echo '⚠️' || echo '') -- **Skipped:** $total_skipped $([ $total_skipped -gt 0 ] && echo '⏭️' || echo '')" +**Results:** $total_passed passed, $total_failed failed, $total_flaky flaky, $total_skipped skipped (Total: $total_tests)" + fi + + # Extract and display failed tests from all browsers + if [ $total_failed -gt 0 ] || [ $total_flaky -gt 0 ]; then + comment="$comment + +### ❌ Failed Tests" + + # Process each browser's failures + for counts_json in "${counts_array[@]}"; do + [ -z "$counts_json" ] || [ "$counts_json" = "{}" ] && continue + + if command -v jq > /dev/null 2>&1; then + # Extract failures array from JSON + failures=$(echo "$counts_json" | jq -r '.failures // [] | .[]? | "\(.name)|\(.file)|\(.traceUrl // "")"') + + if [ -n "$failures" ]; then + while IFS='|' read -r test_name test_file trace_url; do + [ -z "$test_name" ] && continue + + # Convert file path to GitHub URL (relative to repo root) + github_file_url="https://github.com/$GITHUB_REPOSITORY/blob/$GITHUB_SHA/$test_file" + + # Build the failed test line + test_line="- [$test_name]($github_file_url)" + + if [ -n "$trace_url" ] && [ "$trace_url" != "null" ]; then + test_line="$test_line: [View trace]($trace_url)" + fi + + comment="$comment +$test_line" + done <<< "$failures" + fi + fi + done fi + # Add browser reports in collapsible section comment="$comment -### 📊 Test Reports by Browser" +
+📊 Browser Reports + +" - # Add browser results with individual counts + # Add browser results i=0 IFS=' ' read -r -a browser_array <<< "$BROWSERS" IFS=' ' read -r -a url_array <<< "$urls" @@ -349,7 +380,7 @@ $status_icon **$status_text** fi if [ -n "$b_total" ] && [ "$b_total" != "0" ]; then - counts_str=" • ✅ $b_passed / ❌ $b_failed / ⚠️ $b_flaky / ⏭️ $b_skipped" + counts_str=" (✅ $b_passed / ❌ $b_failed / ⚠️ $b_flaky / ⏭️ $b_skipped)" else counts_str="" fi @@ -358,10 +389,10 @@ $status_icon **$status_text** fi comment="$comment -- ✅ **${browser}**: [View Report](${url})${counts_str}" +- **${browser}**: [View Report](${url})${counts_str}" else comment="$comment -- ❌ **${browser}**: Deployment failed" +- **${browser}**: ❌ Deployment failed" fi i=$((i + 1)) done @@ -369,8 +400,7 @@ $status_icon **$status_text** comment="$comment ---- -🎉 Click on the links above to view detailed test results for each browser configuration." +
" post_comment "$comment" fi diff --git a/src/components/TopMenuSection.vue b/src/components/TopMenuSection.vue index 448a081f9e..28f2411ea7 100644 --- a/src/components/TopMenuSection.vue +++ b/src/components/TopMenuSection.vue @@ -20,9 +20,14 @@ variant="secondary" size="icon" :aria-label="t('menu.customNodesManager')" + class="relative" @click="openCustomNodeManager" > + @@ -49,13 +54,16 @@ {{ queuedCount }} - - + +