Compare commits

...

10 Commits

Author SHA1 Message Date
Dante
a09613220a Merge branch 'main' into test/job-history-e2e 2026-04-26 09:26:02 +09:00
dante01yoon
f826f74339 Merge remote-tracking branch 'origin/main' into test/job-history-e2e
# Conflicts:
#	browser_tests/fixtures/components/SidebarTab.ts
2026-04-15 20:12:12 +09:00
dante01yoon
49b8576806 fix: address PR review feedback for job history E2E tests
- Fix timestamp unit mismatch: Date.now() instead of Date.now() / 1000
- Use @e2e/ and @/ path aliases instead of deep relative imports
- Fix flake: wait for all jobs to render before capturing initialCount
- Use retrying assertion toHaveCount(0) instead of snapshot count
- Add missing timeout on toBeVisible after All tab click
- Remove dead groupLabels getter with brittle Tailwind selector
- Remove redundant comments
2026-04-15 10:42:16 +09:00
Alexander Brown
5b6c3a5455 Merge branch 'main' into test/job-history-e2e 2026-04-10 00:11:39 -07:00
dante01yoon
91205994eb refactor: rename panel to root in JobHistorySidebarTab
Align with the emerging sidebar tab naming convention where all
tab page objects use a `root` getter for container scoping.
2026-04-09 16:16:26 +09:00
dante01yoon
754cea7b72 Merge origin/main into test/job-history-e2e 2026-04-07 11:05:57 +09:00
dante01yoon
725e4eabb3 fix(test): enable QPOV2 setting so job history sidebar tab is registered
The job history sidebar tab is only rendered when Comfy.Queue.QPOV2 is
true (default: false). All 11 tests timed out waiting for the tab button.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 15:47:04 +09:00
dante01yoon
7e3fb71284 fix(test): use seconds-based timestamps to match main convention
Align createMockJob defaults and jobHistory test data with main's
seconds-based timestamp convention (Date.now() / 1000).
2026-04-01 15:00:42 +09:00
dante01yoon
58dcfbc983 fix(test): scope job history locators and use millisecond timestamps
- Scope all JobHistorySidebarTab locators to .sidebar-content-container
  to avoid collision with QueueOverlayExpanded's identical controls
- Use milliseconds (Date.now()) instead of seconds for mock timestamps,
  matching the Python backend's int(time.time() * 1000) format
2026-04-01 14:59:27 +09:00
dante01yoon
1a01192140 test(jobHistory): add E2E tests for job history sidebar tab
Add JobHistorySidebarTab fixture with filter tab, search, and job
item locators. Add createMockJob factory to AssetsHelper. Add 11
test scenarios covering tab display, filter tabs (All/Completed/
Failed), search, empty state, and Failed tab visibility.
2026-04-01 14:59:27 +09:00
3 changed files with 319 additions and 12 deletions

View File

@@ -24,6 +24,7 @@ import { SettingDialog } from '@e2e/fixtures/components/SettingDialog'
import { TemplatesDialog } from '@e2e/fixtures/components/TemplatesDialog'
import {
AssetsSidebarTab,
JobHistorySidebarTab,
ModelLibrarySidebarTab,
NodeLibrarySidebarTab,
NodeLibrarySidebarTabV2,
@@ -64,6 +65,7 @@ class ComfyPropertiesPanel {
class ComfyMenu {
private _assetsTab: AssetsSidebarTab | null = null
private _jobHistoryTab: JobHistorySidebarTab | null = null
private _modelLibraryTab: ModelLibrarySidebarTab | null = null
private _nodeLibraryTab: NodeLibrarySidebarTab | null = null
private _nodeLibraryTabV2: NodeLibrarySidebarTabV2 | null = null
@@ -82,6 +84,11 @@ class ComfyMenu {
this.buttons = this.sideToolbar.locator('.side-bar-button')
}
get jobHistoryTab() {
this._jobHistoryTab ??= new JobHistorySidebarTab(this.page)
return this._jobHistoryTab
}
get modelLibraryTab() {
this._modelLibraryTab ??= new ModelLibrarySidebarTab(this.page)
return this._modelLibraryTab

View File

@@ -30,6 +30,17 @@ class SidebarTab {
}
await this.tabButton.click()
}
async dismissToasts() {
const closeButtons = this.page.locator('.p-toast-close-button')
for (const button of await closeButtons.all()) {
await button.click().catch(() => {})
}
await expect(this.page.locator('.p-toast-message'))
.toHaveCount(0, { timeout: 5000 })
.catch(() => {})
}
}
export class NodeLibrarySidebarTab extends SidebarTab {
@@ -206,6 +217,64 @@ export class WorkflowsSidebarTab extends SidebarTab {
}
}
export class JobHistorySidebarTab extends SidebarTab {
constructor(public override readonly page: Page) {
super(page, 'job-history')
}
/** Scope all locators to the sidebar root to avoid collision
* with QueueOverlayExpanded which renders the same controls. */
get root() {
return this.page.locator('.sidebar-content-container')
}
get allTab() {
return this.root.getByRole('button', { name: 'All', exact: true })
}
get completedTab() {
return this.root.getByRole('button', { name: 'Completed', exact: true })
}
get failedTab() {
return this.root.getByRole('button', { name: 'Failed', exact: true })
}
get searchInput() {
return this.root.getByPlaceholder('Search...')
}
get filterButton() {
return this.root.getByRole('button', { name: /Filter/i })
}
get sortButton() {
return this.root.getByRole('button', { name: /Sort/i })
}
get jobItems() {
return this.root.locator('[data-job-id]')
}
get noActiveJobsText() {
return this.root.getByText('No active jobs')
}
async waitForJobsLoad() {
await expect(this.jobItems.first()).toBeVisible({ timeout: 5000 })
}
getJobById(id: string) {
return this.root.locator(`[data-job-id="${id}"]`)
}
override async open() {
await this.dismissToasts()
await super.open()
await this.allTab.waitFor({ state: 'visible', timeout: 5000 })
}
}
export class ModelLibrarySidebarTab extends SidebarTab {
public readonly searchInput: Locator
public readonly modelTree: Locator
@@ -349,18 +418,6 @@ export class AssetsSidebarTab extends SidebarTab {
await this.generatedTab.waitFor({ state: 'visible' })
}
/** Dismiss all visible toast notifications by clicking their close buttons. */
async dismissToasts() {
const closeButtons = this.page.locator('.p-toast-close-button')
for (const btn of await closeButtons.all()) {
await btn.click().catch(() => {})
}
// Wait for all toast elements to fully animate out and detach from DOM
await expect(this.page.locator('.p-toast-message'))
.toHaveCount(0)
.catch(() => {})
}
async switchToImported() {
await this.dismissToasts()
await this.importedTab.click()

View File

@@ -0,0 +1,243 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
import { createMockJob } from '@e2e/fixtures/helpers/AssetsHelper'
import type { RawJobListItem } from '@/platform/remote/comfyui/jobs/jobTypes'
const now = Date.now()
const COMPLETED_JOBS: RawJobListItem[] = [
createMockJob({
id: 'job-completed-1',
status: 'completed',
create_time: now - 60,
execution_start_time: now - 60,
execution_end_time: now - 50,
outputs_count: 2
}),
createMockJob({
id: 'job-completed-2',
status: 'completed',
create_time: now - 120,
execution_start_time: now - 120,
execution_end_time: now - 115,
outputs_count: 1
})
]
const FAILED_JOBS: RawJobListItem[] = [
createMockJob({
id: 'job-failed-1',
status: 'failed',
create_time: now - 30,
execution_start_time: now - 30,
execution_end_time: now - 28,
outputs_count: 0
})
]
const ALL_JOBS = [...COMPLETED_JOBS, ...FAILED_JOBS]
// ==========================================================================
// 1. Tab open and job display
// ==========================================================================
test.describe('Job history sidebar - display', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.assets.mockOutputHistory(ALL_JOBS)
await comfyPage.settings.setSetting('Comfy.Queue.QPOV2', true)
await comfyPage.setup()
})
test.afterEach(async ({ comfyPage }) => {
await comfyPage.assets.clearMocks()
})
test('Opens job history tab and shows job items', async ({ comfyPage }) => {
const tab = comfyPage.menu.jobHistoryTab
await tab.open()
await tab.waitForJobsLoad()
await expect
.poll(() => tab.jobItems.count(), { timeout: 5000 })
.toBeGreaterThanOrEqual(1)
})
test('Shows All, Completed filter tabs', async ({ comfyPage }) => {
const tab = comfyPage.menu.jobHistoryTab
await tab.open()
await expect(tab.allTab).toBeVisible()
await expect(tab.completedTab).toBeVisible()
})
test('Shows Failed tab when failed jobs exist', async ({ comfyPage }) => {
const tab = comfyPage.menu.jobHistoryTab
await tab.open()
await tab.waitForJobsLoad()
await expect(tab.failedTab).toBeVisible()
})
test('Shows search input and filter/sort buttons', async ({ comfyPage }) => {
const tab = comfyPage.menu.jobHistoryTab
await tab.open()
await expect(tab.searchInput).toBeVisible()
await expect(tab.filterButton).toBeVisible()
await expect(tab.sortButton).toBeVisible()
})
})
// ==========================================================================
// 2. Filter tabs
// ==========================================================================
test.describe('Job history sidebar - filter tabs', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.assets.mockOutputHistory(ALL_JOBS)
await comfyPage.settings.setSetting('Comfy.Queue.QPOV2', true)
await comfyPage.setup()
})
test.afterEach(async ({ comfyPage }) => {
await comfyPage.assets.clearMocks()
})
test('Completed tab filters to completed jobs only', async ({
comfyPage
}) => {
const tab = comfyPage.menu.jobHistoryTab
await tab.open()
await tab.waitForJobsLoad()
await tab.completedTab.click()
await expect(tab.getJobById('job-completed-1')).toBeVisible({
timeout: 5000
})
await expect(tab.getJobById('job-failed-1')).toBeHidden()
})
test('Failed tab filters to failed jobs only', async ({ comfyPage }) => {
const tab = comfyPage.menu.jobHistoryTab
await tab.open()
await tab.waitForJobsLoad()
await tab.failedTab.click()
await expect(tab.getJobById('job-failed-1')).toBeVisible({ timeout: 5000 })
await expect(tab.getJobById('job-completed-1')).toBeHidden()
})
test('All tab shows all jobs again', async ({ comfyPage }) => {
const tab = comfyPage.menu.jobHistoryTab
await tab.open()
await tab.waitForJobsLoad()
// Switch to Completed then back to All
await tab.completedTab.click()
await expect(tab.getJobById('job-failed-1')).toBeHidden()
await tab.allTab.click()
await expect(tab.getJobById('job-completed-1')).toBeVisible({
timeout: 5000
})
await expect(tab.getJobById('job-failed-1')).toBeVisible({ timeout: 5000 })
})
})
// ==========================================================================
// 3. Search
// ==========================================================================
test.describe('Job history sidebar - search', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.assets.mockOutputHistory(ALL_JOBS)
await comfyPage.settings.setSetting('Comfy.Queue.QPOV2', true)
await comfyPage.setup()
})
test.afterEach(async ({ comfyPage }) => {
await comfyPage.assets.clearMocks()
})
test('Search filters jobs by text', async ({ comfyPage }) => {
const tab = comfyPage.menu.jobHistoryTab
await tab.open()
await tab.waitForJobsLoad()
await expect(tab.jobItems).toHaveCount(ALL_JOBS.length, { timeout: 5000 })
const initialCount = await tab.jobItems.count()
// Search for a specific job ID substring
await tab.searchInput.fill('failed')
// Wait for filter to reduce count (150ms debounce)
await expect
.poll(() => tab.jobItems.count(), { timeout: 5000 })
.toBeLessThan(initialCount)
})
})
// ==========================================================================
// 4. Empty state
// ==========================================================================
test.describe('Job history sidebar - empty state', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.assets.mockOutputHistory([])
await comfyPage.settings.setSetting('Comfy.Queue.QPOV2', true)
await comfyPage.setup()
})
test.afterEach(async ({ comfyPage }) => {
await comfyPage.assets.clearMocks()
})
test('Shows no active jobs when history is empty', async ({ comfyPage }) => {
const tab = comfyPage.menu.jobHistoryTab
await tab.open()
await expect(tab.noActiveJobsText).toBeVisible()
await expect(tab.jobItems).toHaveCount(0)
})
test('Failed tab is hidden when no failed jobs exist', async ({
comfyPage
}) => {
const tab = comfyPage.menu.jobHistoryTab
await tab.open()
await expect(tab.failedTab).toBeHidden()
})
})
// ==========================================================================
// 5. Only completed jobs (no failed tab)
// ==========================================================================
test.describe('Job history sidebar - completed only', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.assets.mockOutputHistory(COMPLETED_JOBS)
await comfyPage.settings.setSetting('Comfy.Queue.QPOV2', true)
await comfyPage.setup()
})
test.afterEach(async ({ comfyPage }) => {
await comfyPage.assets.clearMocks()
})
test('Failed tab hidden when only completed jobs exist', async ({
comfyPage
}) => {
const tab = comfyPage.menu.jobHistoryTab
await tab.open()
await tab.waitForJobsLoad()
await expect(tab.failedTab).toBeHidden()
await expect(tab.allTab).toBeVisible()
await expect(tab.completedTab).toBeVisible()
})
})