mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-31 01:35:51 +00:00
Compare commits
3 Commits
docs/weekl
...
pysssss/ap
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
770c0d274e | ||
|
|
b2c2c34688 | ||
|
|
21ee55b578 |
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
}) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user