mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-17 21:21:06 +00:00
Compare commits
3 Commits
sno-qa-106
...
test/asset
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf45762ed2 | ||
|
|
6f62f87a0b | ||
|
|
515f234143 |
@@ -227,6 +227,14 @@ export class AssetsSidebarTab extends SidebarTab {
|
||||
return this.page.getByText('Oldest first')
|
||||
}
|
||||
|
||||
get sortLongestFirst() {
|
||||
return this.page.getByText('Generation time (longest first)')
|
||||
}
|
||||
|
||||
get sortFastestFirst() {
|
||||
return this.page.getByText('Generation time (fastest first)')
|
||||
}
|
||||
|
||||
// --- Asset cards ---
|
||||
|
||||
get assetCards() {
|
||||
|
||||
@@ -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
|
||||
}) => {
|
||||
|
||||
@@ -667,3 +667,114 @@ test.describe('Assets sidebar - settings menu', () => {
|
||||
await expect(tab.gridViewOption).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================================================
|
||||
// 11. Sort functionality (cloud-only — sort options require DISTRIBUTION=cloud)
|
||||
// ==========================================================================
|
||||
|
||||
// Sort options are guarded by isCloud (compile-time). The cloud CI project
|
||||
// cannot use comfyPageFixture (auth required). Enable once cloud E2E infra
|
||||
// supports authenticated comfyPage setup.
|
||||
test.describe('Assets sidebar - sort', () => {
|
||||
test.fixme(true, 'Requires DISTRIBUTION=cloud build with auth bypass')
|
||||
const SORT_JOBS: RawJobListItem[] = [
|
||||
createMockJob({
|
||||
id: 'sort-oldest',
|
||||
create_time: 1000,
|
||||
execution_start_time: 1000,
|
||||
execution_end_time: 1010,
|
||||
preview_output: {
|
||||
filename: 'oldest_asset.png',
|
||||
subfolder: '',
|
||||
type: 'output',
|
||||
nodeId: '1',
|
||||
mediaType: 'images'
|
||||
}
|
||||
}),
|
||||
createMockJob({
|
||||
id: 'sort-middle',
|
||||
create_time: 2000,
|
||||
execution_start_time: 2000,
|
||||
execution_end_time: 2003,
|
||||
preview_output: {
|
||||
filename: 'middle_asset.png',
|
||||
subfolder: '',
|
||||
type: 'output',
|
||||
nodeId: '2',
|
||||
mediaType: 'images'
|
||||
}
|
||||
}),
|
||||
createMockJob({
|
||||
id: 'sort-newest',
|
||||
create_time: 3000,
|
||||
execution_start_time: 3000,
|
||||
execution_end_time: 3020,
|
||||
preview_output: {
|
||||
filename: 'newest_asset.png',
|
||||
subfolder: '',
|
||||
type: 'output',
|
||||
nodeId: '3',
|
||||
mediaType: 'images'
|
||||
}
|
||||
})
|
||||
]
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.assets.mockOutputHistory(SORT_JOBS)
|
||||
await comfyPage.assets.mockInputFiles([])
|
||||
await comfyPage.setup()
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
await comfyPage.assets.clearMocks()
|
||||
})
|
||||
|
||||
test('Assets display in newest-first order by default', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const tab = comfyPage.menu.assetsTab
|
||||
await tab.open()
|
||||
await tab.waitForAssets(3)
|
||||
|
||||
const firstCard = tab.assetCards.first()
|
||||
await expect(firstCard).toContainText('newest_asset')
|
||||
})
|
||||
|
||||
test('Sort by oldest first reverses order', async ({ comfyPage }) => {
|
||||
const tab = comfyPage.menu.assetsTab
|
||||
await tab.open()
|
||||
await tab.waitForAssets(3)
|
||||
|
||||
await tab.openSettingsMenu()
|
||||
await tab.sortOldestFirst.click()
|
||||
|
||||
await expect(tab.assetCards.first()).toContainText('oldest_asset')
|
||||
await expect(tab.assetCards.last()).toContainText('newest_asset')
|
||||
})
|
||||
|
||||
test('Sort by longest execution duration', async ({ comfyPage }) => {
|
||||
const tab = comfyPage.menu.assetsTab
|
||||
await tab.open()
|
||||
await tab.waitForAssets(3)
|
||||
|
||||
await tab.openSettingsMenu()
|
||||
await tab.sortLongestFirst.click()
|
||||
|
||||
// Durations: newest=20s, oldest=10s, middle=3s
|
||||
await expect(tab.assetCards.first()).toContainText('newest_asset')
|
||||
await expect(tab.assetCards.last()).toContainText('middle_asset')
|
||||
})
|
||||
|
||||
test('Sort by fastest execution duration', async ({ comfyPage }) => {
|
||||
const tab = comfyPage.menu.assetsTab
|
||||
await tab.open()
|
||||
await tab.waitForAssets(3)
|
||||
|
||||
await tab.openSettingsMenu()
|
||||
await tab.sortFastestFirst.click()
|
||||
|
||||
// Durations: middle=3s, oldest=10s, newest=20s
|
||||
await expect(tab.assetCards.first()).toContainText('middle_asset')
|
||||
await expect(tab.assetCards.last()).toContainText('newest_asset')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user