Compare commits

..

3 Commits

Author SHA1 Message Date
pythongosssss
770c0d274e rabbit 2026-03-30 10:05:24 -07:00
pythongosssss
b2c2c34688 add hidden sizer elements 2026-03-30 09:54:01 -07:00
pythongosssss
21ee55b578 Ensure all save/save as buttons are the same width 2026-03-30 03:46:40 -07:00
8 changed files with 137 additions and 136 deletions

View File

@@ -30,6 +30,10 @@ export class BuilderFooterHelper {
return this.page.getByTestId(TestIds.builder.saveButton)
}
get saveGroup(): Locator {
return this.page.getByTestId(TestIds.builder.saveGroup)
}
get saveAsButton(): Locator {
return this.page.getByTestId(TestIds.builder.saveAsButton)
}

View File

@@ -82,6 +82,7 @@ export const TestIds = {
footerNav: 'builder-footer-nav',
saveButton: 'builder-save-button',
saveAsButton: 'builder-save-as-button',
saveGroup: 'builder-save-group',
saveAsChevron: 'builder-save-as-chevron',
ioItem: 'builder-io-item',
ioItemTitle: 'builder-io-item-title',

View File

@@ -189,6 +189,41 @@ test.describe('Builder save flow', { tag: ['@ui'] }, () => {
await expect(saveAs.nameInput).toBeVisible()
})
test('Save button width is consistent across all states', async ({
comfyPage
}) => {
const { appMode } = comfyPage
await comfyPage.workflow.loadWorkflow('default')
await fitToViewInstant(comfyPage)
await appMode.enterBuilder()
// State 1: Disabled "Save as" (no outputs selected)
const disabledBox = await appMode.footer.saveAsButton.boundingBox()
expect(disabledBox).toBeTruthy()
// Select I/O to enable the button
await appMode.steps.goToInputs()
const ksampler = await comfyPage.nodeOps.getNodeRefById('3')
await appMode.select.selectInputWidget(ksampler)
await appMode.steps.goToOutputs()
await appMode.select.selectOutputNode()
// State 2: Enabled "Save as" (unsaved, has outputs)
const enabledBox = await appMode.footer.saveAsButton.boundingBox()
expect(enabledBox).toBeTruthy()
expect(enabledBox!.width).toBe(disabledBox!.width)
// Save the workflow to transition to the Save + chevron state
await builderSaveAs(appMode, `${Date.now()} width-test`, 'App')
await appMode.saveAs.closeButton.click()
await comfyPage.nextFrame()
// State 3: Save + chevron button group (saved workflow)
const saveButtonGroupBox = await appMode.footer.saveGroup.boundingBox()
expect(saveButtonGroupBox).toBeTruthy()
expect(saveButtonGroupBox!.width).toBe(disabledBox!.width)
})
test('Connect output popover appears when no outputs selected', async ({
comfyPage
}) => {

View File

@@ -25,7 +25,7 @@ ComfyUI's extension system follows these key principles:
## Core Extensions List
The following table lists the main core extensions in the system. Additional extensions may exist for specialized features like cloud integration, badges, and advanced UI features. See `src/extensions/core/` for the complete list.
The following table lists ALL core extensions in the system as of 2025-01-30:
### Main Extensions

View File

@@ -2,70 +2,22 @@
This guide covers patterns and examples for testing Vue components in the ComfyUI Frontend codebase.
## Testing Libraries
This project supports two component testing approaches:
- **@testing-library/vue** (Recommended) - User-centric, behavior-focused testing with semantic queries
- **@vue/test-utils** - Lower-level component API testing
Both are acceptable, but Testing Library is preferred for new tests as it encourages better testing practices.
## Table of Contents
1. [Basic Component Testing with Testing Library](#basic-component-testing-with-testing-library)
2. [Basic Component Testing with Vue Test Utils](#basic-component-testing-with-vue-test-utils)
3. [PrimeVue Components Testing](#primevue-components-testing)
4. [Tooltip Directives](#tooltip-directives)
5. [Component Events Testing](#component-events-testing)
6. [User Interaction Testing](#user-interaction-testing)
7. [Asynchronous Component Testing](#asynchronous-component-testing)
8. [Working with Vue Reactivity](#working-with-vue-reactivity)
1. [Basic Component Testing](#basic-component-testing)
2. [PrimeVue Components Testing](#primevue-components-testing)
3. [Tooltip Directives](#tooltip-directives)
4. [Component Events Testing](#component-events-testing)
5. [User Interaction Testing](#user-interaction-testing)
6. [Asynchronous Component Testing](#asynchronous-component-testing)
7. [Working with Vue Reactivity](#working-with-vue-reactivity)
## Basic Component Testing with Testing Library
## Basic Component Testing
User-centric approach using @testing-library/vue (recommended):
Basic approach to testing a component's rendering and structure:
```typescript
// Example from: src/components/sidebar/SidebarIcon.test.ts
import { render, screen } from '@testing-library/vue'
import userEvent from '@testing-library/user-event'
import SidebarIcon from './SidebarIcon.vue'
describe('SidebarIcon', () => {
const exampleProps = {
icon: 'pi pi-cog',
selected: false,
tooltip: 'Settings'
}
it('renders as a button with accessible label', () => {
render(SidebarIcon, { props: exampleProps })
const button = screen.getByRole('button', { name: 'Settings' })
expect(button).toBeInTheDocument()
})
it('handles click interactions', async () => {
const user = userEvent.setup()
const onClick = vi.fn()
render(SidebarIcon, {
props: { ...exampleProps, onClick }
})
await user.click(screen.getByRole('button'))
expect(onClick).toHaveBeenCalled()
})
})
```
## Basic Component Testing with Vue Test Utils
Lower-level API testing approach:
```typescript
// Alternative approach using @vue/test-utils
// Example from: src/components/sidebar/SidebarIcon.spec.ts
import { mount } from '@vue/test-utils'
import SidebarIcon from './SidebarIcon.vue'

View File

@@ -147,9 +147,9 @@ it('should subscribe to logs API', () => {
})
```
## Mocking Utility Functions
## Mocking Lodash Functions
Mocking utility functions like debounce from es-toolkit:
Mocking utility functions like debounce:
```typescript
// Mock debounce to execute immediately

View File

@@ -33,76 +33,91 @@
{{ t('g.next') }}
<i class="icon-[lucide--chevron-right]" aria-hidden="true" />
</Button>
<ConnectOutputPopover
v-if="!hasOutputs"
:is-select-active="isSelectStep"
@switch="navigateToStep('builder:outputs')"
>
<div class="relative min-w-24">
<!--
Invisible sizers: both labels rendered with matching button padding
so the container's intrinsic width equals the wider label.
height:0 + overflow:hidden keeps them invisible without affecting height.
-->
<div class="max-h-0 overflow-y-hidden" aria-hidden="true">
<div class="px-4 py-2 text-sm">{{ t('g.save') }}</div>
<div class="px-4 py-2 text-sm">{{ t('builderToolbar.saveAs') }}</div>
</div>
<ConnectOutputPopover
v-if="!hasOutputs"
class="w-full"
:is-select-active="isSelectStep"
@switch="navigateToStep('builder:outputs')"
>
<Button
size="lg"
class="w-full"
:class="disabledSaveClasses"
data-testid="builder-save-as-button"
>
{{ isSaved ? t('g.save') : t('builderToolbar.saveAs') }}
</Button>
</ConnectOutputPopover>
<ButtonGroup
v-else-if="isSaved"
data-testid="builder-save-group"
class="w-full rounded-lg bg-secondary-background has-[[data-save-chevron]:hover]:bg-secondary-background-hover"
>
<Button
size="lg"
:disabled="!isModified"
class="flex-1"
:class="isModified ? activeSaveClasses : disabledSaveClasses"
data-testid="builder-save-button"
@click="save()"
>
{{ t('g.save') }}
</Button>
<DropdownMenuRoot>
<DropdownMenuTrigger as-child>
<Button
size="lg"
:aria-label="t('builderToolbar.saveAs')"
data-save-chevron
data-testid="builder-save-as-chevron"
class="w-6 rounded-l-none border-l border-border-default px-0"
>
<i
class="icon-[lucide--chevron-down] size-4"
aria-hidden="true"
/>
</Button>
</DropdownMenuTrigger>
<DropdownMenuPortal>
<DropdownMenuContent
align="end"
:side-offset="4"
class="z-1001 min-w-36 rounded-lg border border-border-subtle bg-base-background p-1 shadow-interface"
>
<DropdownMenuItem as-child @select="saveAs()">
<Button
variant="secondary"
size="lg"
class="w-full justify-start font-normal"
>
{{ t('builderToolbar.saveAs') }}
</Button>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenuPortal>
</DropdownMenuRoot>
</ButtonGroup>
<Button
v-else
size="lg"
:class="cn('w-24', disabledSaveClasses)"
class="w-full"
:class="activeSaveClasses"
data-testid="builder-save-as-button"
@click="saveAs()"
>
{{ isSaved ? t('g.save') : t('builderToolbar.saveAs') }}
{{ t('builderToolbar.saveAs') }}
</Button>
</ConnectOutputPopover>
<ButtonGroup
v-else-if="isSaved"
class="w-24 rounded-lg bg-secondary-background has-[[data-save-chevron]:hover]:bg-secondary-background-hover"
>
<Button
size="lg"
:disabled="!isModified"
class="flex-1"
:class="isModified ? activeSaveClasses : disabledSaveClasses"
data-testid="builder-save-button"
@click="save()"
>
{{ t('g.save') }}
</Button>
<DropdownMenuRoot>
<DropdownMenuTrigger as-child>
<Button
size="lg"
:aria-label="t('builderToolbar.saveAs')"
data-save-chevron
data-testid="builder-save-as-chevron"
class="w-6 rounded-l-none border-l border-border-default px-0"
>
<i
class="icon-[lucide--chevron-down] size-4"
aria-hidden="true"
/>
</Button>
</DropdownMenuTrigger>
<DropdownMenuPortal>
<DropdownMenuContent
align="end"
:side-offset="4"
class="z-1001 min-w-36 rounded-lg border border-border-subtle bg-base-background p-1 shadow-interface"
>
<DropdownMenuItem as-child @select="saveAs()">
<Button
variant="secondary"
size="lg"
class="w-full justify-start font-normal"
>
{{ t('builderToolbar.saveAs') }}
</Button>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenuPortal>
</DropdownMenuRoot>
</ButtonGroup>
<Button
v-else
size="lg"
:class="activeSaveClasses"
data-testid="builder-save-as-button"
@click="saveAs()"
>
{{ t('builderToolbar.saveAs') }}
</Button>
</div>
</nav>
</div>
</template>
@@ -126,8 +141,6 @@ import { useAppMode } from '@/composables/useAppMode'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useAppModeStore } from '@/stores/appModeStore'
import { useDialogStore } from '@/stores/dialogStore'
import { cn } from '@/utils/tailwindUtil'
import BuilderOpensAsPopover from './BuilderOpensAsPopover.vue'
import { setWorkflowDefaultView } from './builderViewOptions'
import ConnectOutputPopover from './ConnectOutputPopover.vue'

View File

@@ -4,18 +4,14 @@ Our project supports multiple languages using `vue-i18n`. This allows users arou
## Supported Languages
- ar (العربية)
- en (English)
- es (Español)
- fa (فارسی)
- fr (Français)
- zh (中文)
- ru (Русский)
- ja (日本語)
- ko (한국어)
- pt-BR (Português Brasileiro)
- ru (Русский)
- fr (Français)
- es (Español)
- tr (Türkçe)
- zh (中文)
- zh-TW (繁體中文)
## How to Add a New Language