diff --git a/browser_tests/fixtures/utils/litegraphUtils.ts b/browser_tests/fixtures/utils/litegraphUtils.ts index 832db6324..3666ebb7d 100644 --- a/browser_tests/fixtures/utils/litegraphUtils.ts +++ b/browser_tests/fixtures/utils/litegraphUtils.ts @@ -79,48 +79,15 @@ export class SubgraphSlotReference { const node = type === 'input' ? currentGraph.inputNode : currentGraph.outputNode - const slots = - type === 'input' ? currentGraph.inputs : currentGraph.outputs if (!node) { throw new Error(`No ${type} node found in subgraph`) } - // Calculate position for next available slot - // const nextSlotIndex = slots?.length || 0 - // const slotHeight = 20 - // const slotY = node.pos[1] + 30 + nextSlotIndex * slotHeight - - // Find last slot position - const lastSlot = slots.at(-1) - let slotX: number - let slotY: number - - if (lastSlot) { - // If there are existing slots, position the new one below the last one - const gapHeight = 20 - slotX = lastSlot.pos[0] - slotY = lastSlot.pos[1] + gapHeight - } else { - // No existing slots - use slotAnchorX if available, otherwise calculate from node position - if (currentGraph.slotAnchorX !== undefined) { - // The actual slot X position seems to be slotAnchorX - 10 - slotX = currentGraph.slotAnchorX - 10 - } else { - // Fallback: calculate from node edge - slotX = - type === 'input' - ? node.pos[0] + node.size[0] - 10 // Right edge for input node - : node.pos[0] + 10 // Left edge for output node - } - // For Y position when no slots exist, use middle of node - slotY = node.pos[1] + node.size[1] / 2 - } - // Convert from offset to canvas coordinates const canvasPos = window['app'].canvas.ds.convertOffsetToCanvas([ - slotX, - slotY + node.emptySlot.pos[0], + node.emptySlot.pos[1] ]) return canvasPos }, @@ -152,7 +119,7 @@ class NodeSlotReference { window['app'].canvas.ds.convertOffsetToCanvas(rawPos) // Debug logging - convert Float64Arrays to regular arrays for visibility - // eslint-disable-next-line no-console + console.log( `NodeSlotReference debug for ${type} slot ${index} on node ${id}:`, { 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 c21094f81..eaa359731 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 1c9232428..ff608be64 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/templates.spec.ts b/browser_tests/tests/templates.spec.ts index 977f3f6df..ae7e3747d 100644 --- a/browser_tests/tests/templates.spec.ts +++ b/browser_tests/tests/templates.spec.ts @@ -189,9 +189,7 @@ test.describe('Templates', () => { const templateGrid = comfyPage.page.locator( '[data-testid="template-workflows-content"]' ) - const nav = comfyPage.page - .locator('header') - .filter({ hasText: 'Templates' }) + const nav = comfyPage.page.locator('header', { hasText: 'Templates' }) await comfyPage.templates.waitForMinimumCardCount(1) await expect(templateGrid).toBeVisible() @@ -201,7 +199,8 @@ test.describe('Templates', () => { await comfyPage.page.setViewportSize(mobileSize) await comfyPage.templates.waitForMinimumCardCount(1) await expect(templateGrid).toBeVisible() - await expect(nav).not.toBeVisible() // Nav should collapse at mobile size + // Nav header is clipped by overflow-hidden parent at mobile size + await expect(nav).not.toBeInViewport() const tabletSize = { width: 1024, height: 800 } await comfyPage.page.setViewportSize(tabletSize) 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 694f33c40..1afdb11a2 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 67bc93009..298a09d66 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 b216647c9..7b20ddbeb 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 b509651cf..f1d41cf13 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 04ad81b3d..6f40af9c0 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 75be80225..0485e0302 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 73b8a3f99..e5b95630f 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 85b288e29..d47e5ece3 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 9aafc1306..e89433357 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 0e7abaa24..d108f0803 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 83f258a3d..0f003c13e 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 9cf3b642f..67eaac039 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 fc77c7fe1..0f55f3a83 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 eb52b6764..4b5733a29 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 fc9e06620..390def0a2 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 d6ff66592..6d4f41790 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 cab9d380d..226ebb711 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 b128c4d7d..ad7a79ae0 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 5fa318662..5b94967b8 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 74a4e62b0..b551a56ba 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 0af0e7447..3d0ae2ed0 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/build/plugins/comfyAPIPlugin.ts b/build/plugins/comfyAPIPlugin.ts index 6441dd781..394914dbe 100644 --- a/build/plugins/comfyAPIPlugin.ts +++ b/build/plugins/comfyAPIPlugin.ts @@ -76,6 +76,7 @@ function getModuleName(id: string): string { export function comfyAPIPlugin(isDev: boolean): Plugin { return { name: 'comfy-api-plugin', + apply: 'build', transform(code: string, id: string) { if (isDev) return null diff --git a/build/tsconfig.json b/build/tsconfig.json index 1c24810a8..3232dcbd6 100644 --- a/build/tsconfig.json +++ b/build/tsconfig.json @@ -5,7 +5,7 @@ "noEmit": true, "strict": true, "esModuleInterop": true, - "moduleResolution": "node", + "moduleResolution": "bundler", "allowSyntheticDefaultImports": true, "noUnusedLocals": true, "noUnusedParameters": true, diff --git a/docs/testing/vitest-patterns.md b/docs/testing/vitest-patterns.md index 2eb7c8e09..9c7fef7c2 100644 --- a/docs/testing/vitest-patterns.md +++ b/docs/testing/vitest-patterns.md @@ -30,6 +30,10 @@ describe('MyStore', () => { **Why `stubActions: false`?** By default, testing pinia stubs all actions. Set to `false` when testing actual store behavior. +## i18n in Component Tests + +Use real `createI18n` with empty messages instead of mocking `vue-i18n`. See `SearchBox.test.ts` for example. + ## Mock Patterns ### Reset all mocks at once diff --git a/package.json b/package.json index 418cbcc40..810d73c9e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@comfyorg/comfyui-frontend", "private": true, - "version": "1.38.7", + "version": "1.38.9", "type": "module", "repository": "https://github.com/Comfy-Org/ComfyUI_frontend", "homepage": "https://comfy.org", @@ -169,6 +169,7 @@ "firebase": "catalog:", "fuse.js": "^7.0.0", "glob": "^11.0.3", + "jsonata": "catalog:", "jsondiffpatch": "^0.6.0", "loglevel": "^1.9.2", "marked": "^15.0.11", diff --git a/packages/design-system/src/icons/nodeSlot2.svg b/packages/design-system/src/icons/nodeSlot2.svg deleted file mode 100644 index cc1280570..000000000 --- a/packages/design-system/src/icons/nodeSlot2.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - diff --git a/packages/design-system/src/icons/nodeSlot3.svg b/packages/design-system/src/icons/nodeSlot3.svg deleted file mode 100644 index fc94a178b..000000000 --- a/packages/design-system/src/icons/nodeSlot3.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 15ccfa824..eced908c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -186,6 +186,9 @@ catalogs: jsdom: specifier: ^27.4.0 version: 27.4.0 + jsonata: + specifier: ^2.1.0 + version: 2.1.0 knip: specifier: ^5.75.1 version: 5.75.1 @@ -449,6 +452,9 @@ importers: glob: specifier: ^11.0.3 version: 11.0.3 + jsonata: + specifier: 'catalog:' + version: 2.1.0 jsondiffpatch: specifier: ^0.6.0 version: 0.6.0 @@ -6045,6 +6051,10 @@ packages: engines: {node: '>=6'} hasBin: true + jsonata@2.1.0: + resolution: {integrity: sha512-OCzaRMK8HobtX8fp37uIVmL8CY1IGc/a6gLsDqz3quExFR09/U78HUzWYr7T31UEB6+Eu0/8dkVD5fFDOl9a8w==} + engines: {node: '>= 8'} + jsonc-eslint-parser@2.4.0: resolution: {integrity: sha512-WYDyuc/uFcGp6YtM2H0uKmUwieOuzeE/5YocFJLnLfclZ4inf3mRn8ZVy1s7Hxji7Jxm6Ss8gqpexD/GlKoGgg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -14403,6 +14413,8 @@ snapshots: json5@2.2.3: {} + jsonata@2.1.0: {} + jsonc-eslint-parser@2.4.0: dependencies: acorn: 8.15.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 5856550a0..0c91ec424 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -62,6 +62,7 @@ catalog: happy-dom: ^20.0.11 husky: ^9.1.7 jiti: 2.6.1 + jsonata: ^2.1.0 jsdom: ^27.4.0 knip: ^5.75.1 lint-staged: ^16.2.7 diff --git a/src/components/TopMenuSection.test.ts b/src/components/TopMenuSection.test.ts index b4d11efea..6640f9b88 100644 --- a/src/components/TopMenuSection.test.ts +++ b/src/components/TopMenuSection.test.ts @@ -1,12 +1,17 @@ import { createTestingPinia } from '@pinia/testing' import { mount } from '@vue/test-utils' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { computed } from 'vue' +import { computed, nextTick } from 'vue' import { createI18n } from 'vue-i18n' import TopMenuSection from '@/components/TopMenuSection.vue' import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue' import LoginButton from '@/components/topbar/LoginButton.vue' +import type { + JobListItem, + JobStatus +} from '@/platform/remote/comfyui/jobs/jobTypes' +import { TaskItemImpl, useQueueStore } from '@/stores/queueStore' import { isElectron } from '@/utils/envUtil' const mockData = vi.hoisted(() => ({ isLoggedIn: false })) @@ -36,7 +41,8 @@ function createWrapper() { sideToolbar: { queueProgressOverlay: { viewJobHistory: 'View job history', - expandCollapsedQueue: 'Expand collapsed queue' + expandCollapsedQueue: 'Expand collapsed queue', + activeJobsShort: '{count} active | {count} active' } } } @@ -59,6 +65,19 @@ function createWrapper() { }) } +function createJob(id: string, status: JobStatus): JobListItem { + return { + id, + status, + create_time: 0, + priority: 0 + } +} + +function createTask(id: string, status: JobStatus): TaskItemImpl { + return new TaskItemImpl(createJob(id, status)) +} + describe('TopMenuSection', () => { beforeEach(() => { vi.resetAllMocks() @@ -100,4 +119,19 @@ describe('TopMenuSection', () => { }) }) }) + + it('shows the active jobs label with the current count', async () => { + const wrapper = createWrapper() + const queueStore = useQueueStore() + queueStore.pendingTasks = [createTask('pending-1', 'pending')] + queueStore.runningTasks = [ + createTask('running-1', 'in_progress'), + createTask('running-2', 'in_progress') + ] + + await nextTick() + + const queueButton = wrapper.find('[data-testid="queue-overlay-toggle"]') + expect(queueButton.text()).toContain('3 active') + }) }) diff --git a/src/components/TopMenuSection.vue b/src/components/TopMenuSection.vue index 28f2411ea..a20537038 100644 --- a/src/components/TopMenuSection.vue +++ b/src/components/TopMenuSection.vue @@ -44,19 +44,17 @@ queueStore.pendingTasks.length) +const activeJobsLabel = computed(() => { + const count = activeJobsCount.value + return t( + 'sideToolbar.queueProgressOverlay.activeJobsShort', + { count: n(count) }, + count + ) +}) const isIntegratedTabBar = computed( () => settingStore.get('Comfy.UI.TabBarLayout') === 'Integrated' ) diff --git a/src/components/common/EditableText.test.ts b/src/components/common/EditableText.test.ts index 2d31123b9..df1ea5152 100644 --- a/src/components/common/EditableText.test.ts +++ b/src/components/common/EditableText.test.ts @@ -51,7 +51,7 @@ describe('EditableText', () => { isEditing: true }) await wrapper.findComponent(InputText).setValue('New Text') - await wrapper.findComponent(InputText).trigger('keyup.enter') + await wrapper.findComponent(InputText).trigger('keydown.enter') // Blur event should have been triggered expect(wrapper.findComponent(InputText).element).not.toBe( document.activeElement @@ -79,7 +79,7 @@ describe('EditableText', () => { await wrapper.findComponent(InputText).setValue('Modified Text') // Press escape - await wrapper.findComponent(InputText).trigger('keyup.escape') + await wrapper.findComponent(InputText).trigger('keydown.escape') // Should emit cancel event expect(wrapper.emitted('cancel')).toBeTruthy() @@ -103,7 +103,7 @@ describe('EditableText', () => { await wrapper.findComponent(InputText).setValue('Modified Text') // Press escape (which triggers blur internally) - await wrapper.findComponent(InputText).trigger('keyup.escape') + await wrapper.findComponent(InputText).trigger('keydown.escape') // Manually trigger blur to simulate the blur that happens after escape await wrapper.findComponent(InputText).trigger('blur') @@ -120,7 +120,7 @@ describe('EditableText', () => { isEditing: true }) await enterWrapper.findComponent(InputText).setValue('Saved Text') - await enterWrapper.findComponent(InputText).trigger('keyup.enter') + await enterWrapper.findComponent(InputText).trigger('keydown.enter') // Trigger blur that happens after enter await enterWrapper.findComponent(InputText).trigger('blur') expect(enterWrapper.emitted('edit')).toBeTruthy() @@ -133,7 +133,7 @@ describe('EditableText', () => { isEditing: true }) await escapeWrapper.findComponent(InputText).setValue('Cancelled Text') - await escapeWrapper.findComponent(InputText).trigger('keyup.escape') + await escapeWrapper.findComponent(InputText).trigger('keydown.escape') expect(escapeWrapper.emitted('cancel')).toBeTruthy() expect(escapeWrapper.emitted('edit')).toBeFalsy() }) diff --git a/src/components/common/EditableText.vue b/src/components/common/EditableText.vue index 322d332a4..440935fb9 100644 --- a/src/components/common/EditableText.vue +++ b/src/components/common/EditableText.vue @@ -3,7 +3,7 @@ {{ modelValue }} - + + +export default meta +type Story = StoryObj + +export const Default: Story = {} + +export const Failed: Story = { + args: { + label: 'Failed', + severity: 'danger' + } +} + +export const Finished: Story = { + args: { + label: 'Finished', + severity: 'contrast' + } +} + +export const Dot: Story = { + args: { + label: undefined, + variant: 'dot', + severity: 'danger' + } +} + +export const Circle: Story = { + args: { + label: '3', + variant: 'circle' + } +} + +export const AllSeverities: Story = { + render: () => ({ + components: { StatusBadge }, + template: ` +
+ + + + + +
+ ` + }) +} + +export const AllVariants: Story = { + render: () => ({ + components: { StatusBadge }, + template: ` +
+
+ + label +
+
+ + dot +
+
+ + circle +
+
+ ` + }) +} diff --git a/src/components/common/StatusBadge.vue b/src/components/common/StatusBadge.vue index 46ef7ac79..a29cd966d 100644 --- a/src/components/common/StatusBadge.vue +++ b/src/components/common/StatusBadge.vue @@ -1,30 +1,27 @@ diff --git a/src/components/common/VirtualGrid.vue b/src/components/common/VirtualGrid.vue index 134778778..fb6b4374c 100644 --- a/src/components/common/VirtualGrid.vue +++ b/src/components/common/VirtualGrid.vue @@ -1,16 +1,20 @@ @@ -28,19 +32,22 @@ type GridState = { const { items, + gridStyle, bufferRows = 1, scrollThrottle = 64, resizeDebounce = 64, defaultItemHeight = 200, - defaultItemWidth = 200 + defaultItemWidth = 200, + maxColumns = Infinity } = defineProps<{ items: (T & { key: string })[] - gridStyle: Partial + gridStyle: CSSProperties bufferRows?: number scrollThrottle?: number resizeDebounce?: number defaultItemHeight?: number defaultItemWidth?: number + maxColumns?: number }>() const emit = defineEmits<{ @@ -59,7 +66,18 @@ const { y: scrollY } = useScroll(container, { eventListenerOptions: { passive: true } }) -const cols = computed(() => Math.floor(width.value / itemWidth.value) || 1) +const cols = computed(() => + Math.min(Math.floor(width.value / itemWidth.value) || 1, maxColumns) +) + +const mergedGridStyle = computed(() => { + if (maxColumns === Infinity) return gridStyle + return { + ...gridStyle, + gridTemplateColumns: `repeat(${maxColumns}, minmax(0, 1fr))` + } +}) + const viewRows = computed(() => Math.ceil(height.value / itemHeight.value)) const offsetRows = computed(() => Math.floor(scrollY.value / itemHeight.value)) const isValidGrid = computed(() => height.value && width.value && items?.length) @@ -83,6 +101,16 @@ const renderedItems = computed(() => isValidGrid.value ? items.slice(state.value.start, state.value.end) : [] ) +function rowsToHeight(rows: number): string { + return `${(rows / cols.value) * itemHeight.value}px` +} +const topSpacerStyle = computed(() => ({ + height: rowsToHeight(state.value.start) +})) +const bottomSpacerStyle = computed(() => ({ + height: rowsToHeight(items.length - state.value.end) +})) + whenever( () => state.value.isNearEnd, () => { @@ -109,15 +137,6 @@ const onResize = debounce(updateItemSize, resizeDebounce) watch([width, height], onResize, { flush: 'post' }) whenever(() => items, updateItemSize, { flush: 'post' }) onBeforeUnmount(() => { - onResize.cancel() // Clear pending debounced calls + onResize.cancel() }) - - diff --git a/src/components/common/WorkspaceProfilePic.vue b/src/components/common/WorkspaceProfilePic.vue new file mode 100644 index 000000000..642317267 --- /dev/null +++ b/src/components/common/WorkspaceProfilePic.vue @@ -0,0 +1,43 @@ + + + diff --git a/src/components/common/statusBadge.variants.ts b/src/components/common/statusBadge.variants.ts new file mode 100644 index 000000000..479a0dda8 --- /dev/null +++ b/src/components/common/statusBadge.variants.ts @@ -0,0 +1,26 @@ +import type { VariantProps } from 'cva' +import { cva } from 'cva' + +export const statusBadgeVariants = cva({ + base: 'inline-flex items-center justify-center rounded-full', + variants: { + severity: { + default: 'bg-primary-background text-base-foreground', + secondary: 'bg-secondary-background text-base-foreground', + warn: 'bg-warning-background text-base-background', + danger: 'bg-destructive-background text-white', + contrast: 'bg-base-foreground text-base-background' + }, + variant: { + label: 'h-3.5 px-1 text-xxxs font-semibold uppercase', + dot: 'size-2', + circle: 'size-3.5 text-xxxs font-semibold' + } + }, + defaultVariants: { + severity: 'default', + variant: 'label' + } +}) + +export type StatusBadgeVariants = VariantProps diff --git a/src/components/dialog/GlobalDialog.vue b/src/components/dialog/GlobalDialog.vue index afc056d61..2a1f0ef3d 100644 --- a/src/components/dialog/GlobalDialog.vue +++ b/src/components/dialog/GlobalDialog.vue @@ -4,7 +4,12 @@ v-for="item in dialogStore.dialogStack" :key="item.key" v-model:visible="item.visible" - class="global-dialog" + :class="[ + 'global-dialog', + item.key === 'global-settings' && teamWorkspacesEnabled + ? 'settings-dialog-workspace' + : '' + ]" v-bind="item.dialogComponentProps" :pt="item.dialogComponentProps.pt" :aria-labelledby="item.key" @@ -38,7 +43,15 @@ @@ -55,4 +68,27 @@ const dialogStore = useDialogStore() @apply p-2 2xl:p-[var(--p-dialog-content-padding)]; @apply pt-0; } + +/* Workspace mode: wider settings dialog */ +.settings-dialog-workspace { + width: 100%; + max-width: 1440px; +} + +.settings-dialog-workspace .p-dialog-content { + width: 100%; +} + +.manager-dialog { + height: 80vh; + max-width: 1724px; + max-height: 1026px; +} + +@media (min-width: 3000px) { + .manager-dialog { + max-width: 2200px; + max-height: 1320px; + } +} diff --git a/src/components/dialog/content/setting/WorkspacePanel.vue b/src/components/dialog/content/setting/WorkspacePanel.vue new file mode 100644 index 000000000..aff8f3733 --- /dev/null +++ b/src/components/dialog/content/setting/WorkspacePanel.vue @@ -0,0 +1,11 @@ + + + diff --git a/src/components/dialog/content/setting/WorkspacePanelContent.vue b/src/components/dialog/content/setting/WorkspacePanelContent.vue new file mode 100644 index 000000000..9366a573f --- /dev/null +++ b/src/components/dialog/content/setting/WorkspacePanelContent.vue @@ -0,0 +1,163 @@ + + + diff --git a/src/components/dialog/content/setting/WorkspaceSidebarItem.vue b/src/components/dialog/content/setting/WorkspaceSidebarItem.vue new file mode 100644 index 000000000..cab92c7a8 --- /dev/null +++ b/src/components/dialog/content/setting/WorkspaceSidebarItem.vue @@ -0,0 +1,19 @@ + + + diff --git a/src/components/dialog/content/workspace/CreateWorkspaceDialogContent.vue b/src/components/dialog/content/workspace/CreateWorkspaceDialogContent.vue new file mode 100644 index 000000000..b9444ce58 --- /dev/null +++ b/src/components/dialog/content/workspace/CreateWorkspaceDialogContent.vue @@ -0,0 +1,113 @@ + + + diff --git a/src/components/dialog/content/workspace/DeleteWorkspaceDialogContent.vue b/src/components/dialog/content/workspace/DeleteWorkspaceDialogContent.vue new file mode 100644 index 000000000..dea2da18d --- /dev/null +++ b/src/components/dialog/content/workspace/DeleteWorkspaceDialogContent.vue @@ -0,0 +1,89 @@ + + + diff --git a/src/components/dialog/content/workspace/EditWorkspaceDialogContent.vue b/src/components/dialog/content/workspace/EditWorkspaceDialogContent.vue new file mode 100644 index 000000000..62b650a4e --- /dev/null +++ b/src/components/dialog/content/workspace/EditWorkspaceDialogContent.vue @@ -0,0 +1,104 @@ + + + diff --git a/src/components/dialog/content/workspace/LeaveWorkspaceDialogContent.vue b/src/components/dialog/content/workspace/LeaveWorkspaceDialogContent.vue new file mode 100644 index 000000000..6a3d16c36 --- /dev/null +++ b/src/components/dialog/content/workspace/LeaveWorkspaceDialogContent.vue @@ -0,0 +1,78 @@ + + + diff --git a/src/components/queue/QueueProgressOverlay.vue b/src/components/queue/QueueProgressOverlay.vue index d6b3edcd8..626709593 100644 --- a/src/components/queue/QueueProgressOverlay.vue +++ b/src/components/queue/QueueProgressOverlay.vue @@ -200,7 +200,13 @@ const onCancelItem = wrapWithErrorHandlingAsync(async (item: JobListItem) => { if (item.state === 'running' || item.state === 'initialization') { // Running/initializing jobs: interrupt execution - await api.interrupt(promptId) + // Cloud backend uses deleteItem, local uses interrupt + if (isCloud) { + await api.deleteItem('queue', promptId) + } else { + await api.interrupt(promptId) + } + executionStore.clearInitializationByPromptId(promptId) await queueStore.update() } else if (item.state === 'pending') { // Pending jobs: remove from queue @@ -268,7 +274,15 @@ const inspectJobAsset = wrapWithErrorHandlingAsync( ) const cancelQueuedWorkflows = wrapWithErrorHandlingAsync(async () => { + // Capture pending promptIds before clearing + const pendingPromptIds = queueStore.pendingTasks + .map((task) => task.promptId) + .filter((id): id is string => typeof id === 'string' && id.length > 0) + await commandStore.execute('Comfy.ClearPendingTasks') + + // Clear initialization state for removed prompts + executionStore.clearInitializationByPromptIds(pendingPromptIds) }) const interruptAll = wrapWithErrorHandlingAsync(async () => { @@ -284,10 +298,14 @@ const interruptAll = wrapWithErrorHandlingAsync(async () => { // on cloud to ensure we cancel the workflow the user clicked. if (isCloud) { await Promise.all(promptIds.map((id) => api.deleteItem('queue', id))) + executionStore.clearInitializationByPromptIds(promptIds) + await queueStore.update() return } await Promise.all(promptIds.map((id) => api.interrupt(id))) + executionStore.clearInitializationByPromptIds(promptIds) + await queueStore.update() }) const showClearHistoryDialog = () => { diff --git a/src/components/rightSidePanel/layout/PropertiesAccordionItem.vue b/src/components/rightSidePanel/layout/PropertiesAccordionItem.vue index d3ee425dd..0b1b88820 100644 --- a/src/components/rightSidePanel/layout/PropertiesAccordionItem.vue +++ b/src/components/rightSidePanel/layout/PropertiesAccordionItem.vue @@ -5,25 +5,32 @@ import { cn } from '@/utils/tailwindUtil' import TransitionCollapse from './TransitionCollapse.vue' -const props = defineProps<{ +const { + disabled, + label, + enableEmptyState, + tooltip, + class: className +} = defineProps<{ disabled?: boolean label?: string enableEmptyState?: boolean tooltip?: string + class?: string }>() const isCollapse = defineModel('collapse', { default: false }) -const isExpanded = computed(() => !isCollapse.value && !props.disabled) +const isExpanded = computed(() => !isCollapse.value && !disabled) const tooltipConfig = computed(() => { - if (!props.tooltip) return undefined - return { value: props.tooltip, showDelay: 1000 } + if (!tooltip) return undefined + return { value: tooltip, showDelay: 1000 } }) diff --git a/src/components/sidebar/tabs/AssetsSidebarListView.vue b/src/components/sidebar/tabs/AssetsSidebarListView.vue index dd7982fc9..b39d982a5 100644 --- a/src/components/sidebar/tabs/AssetsSidebarListView.vue +++ b/src/components/sidebar/tabs/AssetsSidebarListView.vue @@ -2,7 +2,7 @@
- {{ t('sideToolbar.generatedAssetsHeader') }} + {{ + t( + assetType === 'input' + ? 'sideToolbar.importedAssetsHeader' + : 'sideToolbar.generatedAssetsHeader' + ) + }}
@@ -108,7 +114,7 @@ import AssetsListItem from '@/platform/assets/components/AssetsListItem.vue' import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema' import type { AssetItem } from '@/platform/assets/schemas/assetSchema' import { iconForMediaType } from '@/platform/assets/utils/mediaIconUtil' -import type { JobState } from '@/types/queue' +import { isActiveJobState } from '@/utils/queueUtil' import { formatDuration, formatSize, @@ -118,9 +124,14 @@ import { import { iconForJobState } from '@/utils/queueDisplay' import { cn } from '@/utils/tailwindUtil' -const { assets, isSelected } = defineProps<{ +const { + assets, + isSelected, + assetType = 'output' +} = defineProps<{ assets: AssetItem[] isSelected: (assetId: string) => boolean + assetType?: 'input' | 'output' }>() const emit = defineEmits<{ @@ -161,12 +172,6 @@ const listGridStyle = { gap: '0.5rem' } -function isActiveJobState(state: JobState): boolean { - return ( - state === 'pending' || state === 'initialization' || state === 'running' - ) -} - function getAssetPrimaryText(asset: AssetItem): string { return truncateFilename(asset.name) } diff --git a/src/components/sidebar/tabs/AssetsSidebarTab.vue b/src/components/sidebar/tabs/AssetsSidebarTab.vue index 341d41019..5fd466cda 100644 --- a/src/components/sidebar/tabs/AssetsSidebarTab.vue +++ b/src/components/sidebar/tabs/AssetsSidebarTab.vue @@ -100,34 +100,24 @@ v-if="isListView" :assets="displayAssets" :is-selected="isSelected" + :asset-type="activeTab" @select-asset="handleAssetSelect" @context-menu="handleAssetContextMenu" @approach-end="handleApproachEnd" /> - - - + @zoom="handleZoomClick" + @output-count-click="enterFolderView" + />
diff --git a/src/components/widget/layout/BaseModalLayout.vue b/src/components/widget/layout/BaseModalLayout.vue index 0da7755af..148ea3ccd 100644 --- a/src/components/widget/layout/BaseModalLayout.vue +++ b/src/components/widget/layout/BaseModalLayout.vue @@ -1,100 +1,128 @@ @@ -102,27 +130,29 @@ diff --git a/src/components/widget/nav/NavItem.vue b/src/components/widget/nav/NavItem.vue index 8c20ad929..eb20b9852 100644 --- a/src/components/widget/nav/NavItem.vue +++ b/src/components/widget/nav/NavItem.vue @@ -5,7 +5,7 @@ disabled: !isOverflowing, pt: { text: { class: 'whitespace-nowrap' } } }" - class="flex cursor-pointer items-start gap-2 rounded-md px-4 py-3 text-sm transition-colors text-base-foreground" + class="flex cursor-pointer items-center-safe gap-2 rounded-md px-4 py-3 text-sm transition-colors text-base-foreground" :class=" active ? 'bg-interface-menu-component-surface-selected' @@ -15,25 +15,32 @@ @mouseenter="checkOverflow" @click="onClick" > -
- -
+ - + + diff --git a/src/platform/assets/components/AssetCard.vue b/src/platform/assets/components/AssetCard.vue index 11e404d41..ccaf546c1 100644 --- a/src/platform/assets/components/AssetCard.vue +++ b/src/platform/assets/components/AssetCard.vue @@ -9,30 +9,28 @@ cn( 'rounded-2xl overflow-hidden transition-all duration-200 bg-modal-card-background p-2 gap-2 flex flex-col h-full', interactive && - 'group appearance-none bg-transparent m-0 outline-none text-left hover:bg-secondary-background focus:bg-secondary-background border-none focus:outline-solid outline-base-foreground outline-4' + 'group appearance-none bg-transparent m-0 outline-none text-left hover:bg-secondary-background focus:bg-secondary-background border-none focus:outline-solid outline-base-foreground outline-4', + focused && 'bg-secondary-background outline-solid' ) " + @click.stop="interactive && $emit('focus', asset)" + @focus="interactive && $emit('focus', asset)" @keydown.enter.self="interactive && $emit('select', asset)" >
- + + diff --git a/src/platform/assets/components/modelInfo/ModelInfoPanel.test.ts b/src/platform/assets/components/modelInfo/ModelInfoPanel.test.ts new file mode 100644 index 000000000..89300617f --- /dev/null +++ b/src/platform/assets/components/modelInfo/ModelInfoPanel.test.ts @@ -0,0 +1,165 @@ +import { mount } from '@vue/test-utils' +import { createTestingPinia } from '@pinia/testing' +import { describe, expect, it } from 'vitest' +import { createI18n } from 'vue-i18n' + +import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser' + +import ModelInfoPanel from './ModelInfoPanel.vue' + +const i18n = createI18n({ + legacy: false, + locale: 'en', + messages: { en: {} }, + missingWarn: false, + fallbackWarn: false +}) + +describe('ModelInfoPanel', () => { + const createMockAsset = ( + overrides: Partial = {} + ): AssetDisplayItem => ({ + id: 'test-id', + name: 'test-model.safetensors', + asset_hash: 'hash123', + size: 1024, + mime_type: 'application/octet-stream', + tags: ['models', 'checkpoints'], + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-01T00:00:00Z', + last_access_time: '2024-01-01T00:00:00Z', + description: 'A test model description', + badges: [], + stats: {}, + ...overrides + }) + + const mountPanel = (asset: AssetDisplayItem) => { + return mount(ModelInfoPanel, { + props: { asset }, + global: { + plugins: [createTestingPinia({ stubActions: false }), i18n] + } + }) + } + + describe('Basic Info Section', () => { + it('renders basic info section', () => { + const wrapper = mountPanel(createMockAsset()) + expect(wrapper.text()).toContain('assetBrowser.modelInfo.basicInfo') + }) + + it('displays asset filename', () => { + const asset = createMockAsset({ name: 'my-model.safetensors' }) + const wrapper = mountPanel(asset) + expect(wrapper.text()).toContain('my-model.safetensors') + }) + + it('displays name from user_metadata when present', () => { + const asset = createMockAsset({ + user_metadata: { name: 'My Custom Model' } + }) + const wrapper = mountPanel(asset) + expect(wrapper.text()).toContain('My Custom Model') + }) + + it('falls back to asset name when user_metadata.name not present', () => { + const asset = createMockAsset({ name: 'fallback-model.safetensors' }) + const wrapper = mountPanel(asset) + expect(wrapper.text()).toContain('fallback-model.safetensors') + }) + + it('renders source link when source_arn is present', () => { + const asset = createMockAsset({ + user_metadata: { source_arn: 'civitai:model:123:version:456' } + }) + const wrapper = mountPanel(asset) + const link = wrapper.find( + 'a[href="https://civitai.com/models/123?modelVersionId=456"]' + ) + expect(link.exists()).toBe(true) + expect(link.attributes('target')).toBe('_blank') + }) + + it('displays Civitai icon for Civitai source', () => { + const asset = createMockAsset({ + user_metadata: { source_arn: 'civitai:model:123:version:456' } + }) + const wrapper = mountPanel(asset) + expect( + wrapper.find('img[src="/assets/images/civitai.svg"]').exists() + ).toBe(true) + }) + + it('does not render source field when source_arn is absent', () => { + const asset = createMockAsset() + const wrapper = mountPanel(asset) + const links = wrapper.findAll('a') + expect(links).toHaveLength(0) + }) + }) + + describe('Model Tagging Section', () => { + it('renders model tagging section', () => { + const wrapper = mountPanel(createMockAsset()) + expect(wrapper.text()).toContain('assetBrowser.modelInfo.modelTagging') + }) + + it('renders model type field', () => { + const wrapper = mountPanel(createMockAsset()) + expect(wrapper.text()).toContain('assetBrowser.modelInfo.modelType') + }) + + it('renders base models field', () => { + const asset = createMockAsset({ + user_metadata: { base_model: ['SDXL'] } + }) + const wrapper = mountPanel(asset) + expect(wrapper.text()).toContain( + 'assetBrowser.modelInfo.compatibleBaseModels' + ) + }) + + it('renders additional tags field', () => { + const wrapper = mountPanel(createMockAsset()) + expect(wrapper.text()).toContain('assetBrowser.modelInfo.additionalTags') + }) + }) + + describe('Model Description Section', () => { + it('renders trigger phrases when present', () => { + const asset = createMockAsset({ + user_metadata: { trained_words: ['trigger1', 'trigger2'] } + }) + const wrapper = mountPanel(asset) + expect(wrapper.text()).toContain('trigger1') + expect(wrapper.text()).toContain('trigger2') + }) + + it('renders description section', () => { + const wrapper = mountPanel(createMockAsset()) + expect(wrapper.text()).toContain( + 'assetBrowser.modelInfo.modelDescription' + ) + }) + + it('does not render trigger phrases field when empty', () => { + const asset = createMockAsset() + const wrapper = mountPanel(asset) + expect(wrapper.text()).not.toContain( + 'assetBrowser.modelInfo.triggerPhrases' + ) + }) + }) + + describe('Accordion Structure', () => { + it('renders all three section labels', () => { + const wrapper = mountPanel(createMockAsset()) + expect(wrapper.text()).toContain('assetBrowser.modelInfo.basicInfo') + expect(wrapper.text()).toContain('assetBrowser.modelInfo.modelTagging') + expect(wrapper.text()).toContain( + 'assetBrowser.modelInfo.modelDescription' + ) + }) + }) +}) diff --git a/src/platform/assets/components/modelInfo/ModelInfoPanel.vue b/src/platform/assets/components/modelInfo/ModelInfoPanel.vue new file mode 100644 index 000000000..c69cb6429 --- /dev/null +++ b/src/platform/assets/components/modelInfo/ModelInfoPanel.vue @@ -0,0 +1,296 @@ +