mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-12 00:42:03 +00:00
## Summary Use ShadCN-style Reka popover primitives for the live queue job list after the unused legacy queue row implementation is removed in #11621. This is the first step in migrating popovers toward the ShadCN UI pattern: local design-system wrappers over Reka UI, rather than ad hoc direct Reka or PrimeVue popovers at each call site. ## Changes - **What**: Added the minimal ShadCN-style popover primitives needed by this fix: `Popover`, `PopoverAnchor`, and `PopoverContent`. - **What**: Migrated `JobAssetsList` job details from manual fixed positioning to these popover primitives with viewport collision handling. - **What**: Removed the obsolete manual hover-position helper after `JobAssetsList` stopped using it. - **Dependencies**: No new dependencies; the primitives wrap the existing `reka-ui` package. - Added browser coverage for bottom-row job details clipping in the queue overlay. ## Review Focus - This PR is stacked on #11621. - The live queue surfaces are `JobAssetsList` consumers: expanded queue progress overlay and job history sidebar. - The new `src/components/ui/popover` files intentionally seed the ShadCN-style migration path, but only include the pieces used here to keep the first PR small. - Follow-up PRs can add `PopoverTrigger` and migrate existing PrimeVue/direct-Reka popovers once there is an actual caller.
179 lines
5.4 KiB
TypeScript
179 lines
5.4 KiB
TypeScript
import { expect } from '@playwright/test'
|
|
|
|
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
|
|
import { createMockJob } from '@e2e/fixtures/helpers/AssetsHelper'
|
|
import { TestIds } from '@e2e/fixtures/selectors'
|
|
import type { RawJobListItem } from '@/platform/remote/comfyui/jobs/jobTypes'
|
|
|
|
const now = Date.now()
|
|
|
|
const MOCK_JOBS: RawJobListItem[] = [
|
|
createMockJob({
|
|
id: 'job-completed-1',
|
|
status: 'completed',
|
|
create_time: now - 60_000,
|
|
execution_start_time: now - 60_000,
|
|
execution_end_time: now - 50_000,
|
|
outputs_count: 2
|
|
}),
|
|
createMockJob({
|
|
id: 'job-completed-2',
|
|
status: 'completed',
|
|
create_time: now - 120_000,
|
|
execution_start_time: now - 120_000,
|
|
execution_end_time: now - 115_000,
|
|
outputs_count: 1
|
|
}),
|
|
createMockJob({
|
|
id: 'job-failed-1',
|
|
status: 'failed',
|
|
create_time: now - 30_000,
|
|
execution_start_time: now - 30_000,
|
|
execution_end_time: now - 28_000,
|
|
outputs_count: 0
|
|
}),
|
|
createMockJob({
|
|
id: 'job-failed-bottom',
|
|
status: 'failed',
|
|
create_time: now - 180_000,
|
|
execution_start_time: now - 180_000,
|
|
execution_end_time: now - 178_000,
|
|
outputs_count: 0
|
|
})
|
|
]
|
|
|
|
test.describe('Queue overlay', () => {
|
|
test.beforeEach(async ({ comfyPage }) => {
|
|
await comfyPage.assets.mockOutputHistory(MOCK_JOBS)
|
|
await comfyPage.settings.setSetting('Comfy.Minimap.Visible', false)
|
|
await comfyPage.settings.setSetting('Comfy.Queue.QPOV2', false)
|
|
await comfyPage.setup()
|
|
})
|
|
|
|
test.afterEach(async ({ comfyPage }) => {
|
|
await comfyPage.assets.clearMocks()
|
|
})
|
|
|
|
test('Toggle button opens expanded queue overlay', async ({ comfyPage }) => {
|
|
const toggle = comfyPage.page.getByTestId(TestIds.queue.overlayToggle)
|
|
await toggle.click()
|
|
|
|
// Expanded overlay should show job items
|
|
await expect(comfyPage.page.locator('[data-job-id]').first()).toBeVisible()
|
|
})
|
|
|
|
test('Overlay shows filter tabs (All, Completed)', async ({ comfyPage }) => {
|
|
const toggle = comfyPage.page.getByTestId(TestIds.queue.overlayToggle)
|
|
await toggle.click()
|
|
|
|
await expect(
|
|
comfyPage.page.getByRole('button', { name: 'All', exact: true })
|
|
).toBeVisible()
|
|
await expect(
|
|
comfyPage.page.getByRole('button', { name: 'Completed', exact: true })
|
|
).toBeVisible()
|
|
})
|
|
|
|
test('Overlay shows Failed tab when failed jobs exist', async ({
|
|
comfyPage
|
|
}) => {
|
|
const toggle = comfyPage.page.getByTestId(TestIds.queue.overlayToggle)
|
|
await toggle.click()
|
|
|
|
await expect(comfyPage.page.locator('[data-job-id]').first()).toBeVisible()
|
|
|
|
await expect(
|
|
comfyPage.page.getByRole('button', { name: 'Failed', exact: true })
|
|
).toBeVisible()
|
|
})
|
|
|
|
test('Completed filter shows only completed jobs', async ({ comfyPage }) => {
|
|
const toggle = comfyPage.page.getByTestId(TestIds.queue.overlayToggle)
|
|
await toggle.click()
|
|
|
|
await expect(comfyPage.page.locator('[data-job-id]').first()).toBeVisible()
|
|
|
|
await comfyPage.page
|
|
.getByRole('button', { name: 'Completed', exact: true })
|
|
.click()
|
|
|
|
await expect(
|
|
comfyPage.page.locator('[data-job-id="job-completed-1"]')
|
|
).toBeVisible()
|
|
await expect(
|
|
comfyPage.page.locator('[data-job-id="job-failed-1"]')
|
|
).toBeHidden()
|
|
})
|
|
|
|
test('Toggling overlay again closes it', async ({ comfyPage }) => {
|
|
const toggle = comfyPage.page.getByTestId(TestIds.queue.overlayToggle)
|
|
await toggle.click()
|
|
|
|
await expect(comfyPage.page.locator('[data-job-id]').first()).toBeVisible()
|
|
|
|
await toggle.click()
|
|
|
|
await expect(comfyPage.page.locator('[data-job-id]').first()).toBeHidden()
|
|
})
|
|
|
|
test('Job details popover stays inside the viewport for bottom rows', async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.page.setViewportSize({ width: 1280, height: 420 })
|
|
|
|
const toggle = comfyPage.page.getByTestId(TestIds.queue.overlayToggle)
|
|
await toggle.click()
|
|
|
|
const bottomJob = comfyPage.page.locator(
|
|
'[data-job-id="job-failed-bottom"]'
|
|
)
|
|
await expect(bottomJob).toBeVisible()
|
|
await bottomJob.scrollIntoViewIfNeeded()
|
|
await expect(bottomJob).toBeVisible()
|
|
|
|
const viewportSize = comfyPage.page.viewportSize()
|
|
if (!viewportSize) throw new Error('Viewport must be available')
|
|
|
|
const rowBox = await bottomJob.boundingBox()
|
|
if (!rowBox) throw new Error('Bottom job row should be measurable')
|
|
expect(
|
|
rowBox.y + rowBox.height,
|
|
'Test row should be low enough to exercise bottom-edge collision handling'
|
|
).toBeGreaterThan(viewportSize.height * 0.55)
|
|
await expect
|
|
.poll(async () =>
|
|
bottomJob.evaluate((element) => {
|
|
const rect = element.getBoundingClientRect()
|
|
const hitTarget = document.elementFromPoint(
|
|
rect.x + rect.width / 2,
|
|
rect.y + rect.height / 2
|
|
)
|
|
return hitTarget ? element.contains(hitTarget) : false
|
|
})
|
|
)
|
|
.toBe(true)
|
|
|
|
await comfyPage.page.mouse.move(0, 0)
|
|
await comfyPage.page.mouse.move(
|
|
rowBox.x + rowBox.width / 2,
|
|
rowBox.y + rowBox.height / 2,
|
|
{ steps: 5 }
|
|
)
|
|
|
|
const popover = comfyPage.page.getByTestId(TestIds.queue.jobDetailsPopover)
|
|
await expect(popover).toBeVisible()
|
|
|
|
await expect
|
|
.poll(async () => {
|
|
const popoverBox = await popover.boundingBox()
|
|
if (!popoverBox) return false
|
|
|
|
return (
|
|
popoverBox.y >= 0 &&
|
|
popoverBox.y + popoverBox.height <= viewportSize.height
|
|
)
|
|
})
|
|
.toBe(true)
|
|
})
|
|
})
|