Compare commits

..

1 Commits

Author SHA1 Message Date
bymyself
c427749c9a test: add unit tests for useImageCrop composable 2026-04-10 15:14:33 -07:00
150 changed files with 2806 additions and 4770 deletions

View File

@@ -84,7 +84,6 @@
"typescript/no-unsafe-declaration-merging": "off",
"typescript/no-unused-vars": "off",
"unicorn/no-empty-file": "off",
"vitest/require-mock-type-parameters": "off",
"unicorn/no-new-array": "off",
"unicorn/no-single-promise-in-promise-methods": "off",
"unicorn/no-useless-fallback-in-spread": "off",
@@ -117,60 +116,13 @@
},
{
"files": ["browser_tests/**/*.ts"],
"jsPlugins": ["eslint-plugin-playwright"],
"rules": {
"typescript/no-explicit-any": "error",
"no-async-promise-executor": "error",
"no-control-regex": "error",
"no-useless-rename": "error",
"no-unused-private-class-members": "error",
"unicorn/no-empty-file": "error",
"playwright/consistent-spacing-between-blocks": "error",
"playwright/expect-expect": [
"error",
{
"assertFunctionNames": [
"recordMeasurement",
"logMeasurement",
"builderSaveAs"
],
"assertFunctionPatterns": [
"^expect",
"^assert",
"^verify",
"^searchAndExpect",
"waitForOpen",
"waitForClosed",
"waitForRequest"
]
}
],
"playwright/max-nested-describe": "error",
"playwright/no-duplicate-hooks": "error",
"playwright/no-element-handle": "error",
"playwright/no-eval": "error",
"playwright/no-focused-test": "error",
"playwright/no-force-option": "off",
"playwright/no-networkidle": "error",
"playwright/no-page-pause": "error",
"playwright/no-skipped-test": "error",
"playwright/no-unsafe-references": "error",
"playwright/no-unused-locators": "error",
"playwright/no-useless-await": "error",
"playwright/no-useless-not": "error",
"playwright/no-wait-for-navigation": "error",
"playwright/no-wait-for-selector": "error",
"playwright/no-wait-for-timeout": "error",
"playwright/prefer-hooks-on-top": "error",
"playwright/prefer-locator": "error",
"playwright/prefer-to-have-count": "error",
"playwright/prefer-to-have-length": "error",
"playwright/prefer-web-first-assertions": "error",
"playwright/prefer-native-locators": "error",
"playwright/require-to-pass-timeout": "error",
"playwright/valid-expect": "error",
"playwright/valid-expect-in-promise": "error",
"playwright/valid-title": "error"
"unicorn/no-empty-file": "error"
}
}
]

View File

@@ -318,9 +318,6 @@ When referencing Comfy-Org repos:
- Find existing `!important` classes that are interfering with the styling and propose corrections of those instead.
- NEVER use arbitrary percentage values like `w-[80%]` when a Tailwind fraction utility exists
- Use `w-4/5` instead of `w-[80%]`, `w-1/2` instead of `w-[50%]`, etc.
- NEVER use font-size classes (`text-xs`, `text-sm`, etc.) to size `icon-[...]` (iconify) icons
- Iconify icons size via `width`/`height: 1.2em`, so font-size produces unpredictable results
- Use `size-*` classes for explicit sizing, or set font-size on the **parent** container and let `1.2em` scale naturally
## Agent-only rules

View File

@@ -17,7 +17,7 @@ const features = computed(() => [
<div class="mx-auto max-w-3xl px-6 text-center">
<!-- Badge -->
<span
class="inline-block rounded-full bg-brand-yellow/10 px-4 py-1.5 text-xs tracking-widest text-brand-yellow uppercase"
class="inline-block rounded-full bg-brand-yellow/10 px-4 py-1.5 text-xs uppercase tracking-widest text-brand-yellow"
>
{{ t('academy.badge', locale) }}
</span>

View File

@@ -40,7 +40,7 @@ const steps = computed(() => [
<!-- Connecting line between steps (desktop only) -->
<div
v-if="index < steps.length - 1"
class="absolute top-8 right-0 hidden w-full translate-x-1/2 border-t border-brand-yellow/20 md:block"
class="absolute right-0 top-8 hidden w-full translate-x-1/2 border-t border-brand-yellow/20 md:block"
/>
<div class="relative">

View File

@@ -31,11 +31,11 @@ const ctaButtons = computed(() => [
<div class="flex w-full items-center justify-center md:w-[55%]">
<div class="relative -ml-12 -rotate-15 md:-ml-24" aria-hidden="true">
<div
class="size-64 rounded-full border-40 border-brand-yellow md:h-112 md:w-md md:border-64 lg:h-144 lg:w-xl lg:border-80"
class="h-64 w-64 rounded-full border-[40px] border-brand-yellow md:h-[28rem] md:w-[28rem] md:border-[64px] lg:h-[36rem] lg:w-[36rem] lg:border-[80px]"
>
<!-- Gap on the right side to form "C" shape -->
<div
class="absolute top-1/2 right-0 h-32 w-24 translate-x-1/2 -translate-y-1/2 bg-black md:h-48 md:w-36 lg:h-64 lg:w-48"
class="absolute right-0 top-1/2 h-32 w-24 -translate-y-1/2 translate-x-1/2 bg-black md:h-48 md:w-36 lg:h-64 lg:w-48"
/>
</div>
</div>
@@ -44,7 +44,7 @@ const ctaButtons = computed(() => [
<!-- Right: Text content -->
<div class="flex w-full flex-col items-start md:w-[45%]">
<h1
class="text-5xl/tight font-bold tracking-tight text-white md:text-6xl lg:text-7xl"
class="text-5xl font-bold leading-tight tracking-tight text-white md:text-6xl lg:text-7xl"
>
{{ t('hero.headline', locale) }}
</h1>

View File

@@ -17,7 +17,7 @@ const { locale = 'en' } = defineProps<{ locale?: Locale }>()
{{ t('manifesto.heading', locale) }}
</h2>
<p class="mx-auto mt-6 max-w-2xl text-lg/relaxed text-smoke-700">
<p class="mx-auto mt-6 max-w-2xl text-lg leading-relaxed text-smoke-700">
{{ t('manifesto.body', locale) }}
</p>

View File

@@ -33,11 +33,11 @@ const features = computed(() => [
<div class="flex flex-col items-center gap-4">
<!-- Play button triangle -->
<div
class="flex size-16 items-center justify-center rounded-full border-2 border-white/20"
class="flex h-16 w-16 items-center justify-center rounded-full border-2 border-white/20"
aria-hidden="true"
>
<div
class="ml-1 size-0 border-y-8 border-l-14 border-y-transparent border-l-white"
class="ml-1 h-0 w-0 border-y-8 border-l-[14px] border-y-transparent border-l-white"
/>
</div>
<p class="text-sm text-smoke-700">
@@ -54,7 +54,7 @@ const features = computed(() => [
class="flex items-center gap-2"
>
<span
class="size-2 rounded-full bg-brand-yellow"
class="h-2 w-2 rounded-full bg-brand-yellow"
aria-hidden="true"
/>
<span class="text-sm text-smoke-700">{{ feature }}</span>

View File

@@ -32,7 +32,7 @@ const metrics = computed(() => [
<div class="mx-auto max-w-7xl px-6">
<!-- Heading -->
<p
class="text-center text-xs font-medium tracking-widest text-smoke-700 uppercase"
class="text-center text-xs font-medium uppercase tracking-widest text-smoke-700"
>
{{ t('social.heading', locale) }}
</p>

View File

@@ -90,7 +90,7 @@ const filteredTestimonials = computed(() => {
:key="testimonial.name"
class="rounded-xl border border-white/10 bg-charcoal-600 p-6"
>
<blockquote class="text-base text-white italic">
<blockquote class="text-base italic text-white">
&ldquo;{{ testimonial.quote }}&rdquo;
</blockquote>

View File

@@ -24,12 +24,12 @@ const activeCategory = ref(0)
<!-- Left placeholder image (desktop only) -->
<div class="hidden flex-1 lg:block">
<div
class="aspect-2/3 rounded-full border border-white/10 bg-charcoal-600"
class="aspect-[2/3] rounded-full border border-white/10 bg-charcoal-600"
/>
</div>
<!-- Center content -->
<div class="flex flex-col items-center text-center lg:flex-2">
<div class="flex flex-col items-center text-center lg:flex-[2]">
<h2 class="text-3xl font-bold text-white">
{{ t('useCase.heading', locale) }}
</h2>
@@ -70,7 +70,7 @@ const activeCategory = ref(0)
<!-- Right placeholder image (desktop only) -->
<div class="hidden flex-1 lg:block">
<div
class="aspect-2/3 rounded-3xl border border-white/10 bg-charcoal-600"
class="aspect-[2/3] rounded-3xl border border-white/10 bg-charcoal-600"
/>
</div>
</div>

View File

@@ -53,7 +53,7 @@ const pillars = computed(() => [
class="rounded-xl border border-white/10 bg-charcoal-600 p-6 transition-colors hover:border-brand-yellow"
>
<div
class="flex size-12 items-center justify-center rounded-full bg-brand-yellow text-xl"
class="flex h-12 w-12 items-center justify-center rounded-full bg-brand-yellow text-xl"
>
{{ pillar.icon }}
</div>

View File

@@ -321,7 +321,7 @@ export class ComfyPage {
// window.app.extensionManager => GraphView ready
window.app && window.app.extensionManager
)
await this.page.locator('.p-blockui-mask').waitFor({ state: 'hidden' })
await this.page.waitForSelector('.p-blockui-mask', { state: 'hidden' })
await this.nextFrame()
}
@@ -371,7 +371,7 @@ export class ComfyPage {
}
async closeMenu() {
await this.page.locator('button.comfy-close-menu-btn').click()
await this.page.click('button.comfy-close-menu-btn')
await this.nextFrame()
}

View File

@@ -37,7 +37,7 @@ export class VueNodeHelpers {
*/
getNodeByTitle(title: string): Locator {
return this.page.locator('[data-node-id]').filter({
has: this.page.getByTestId('node-title').filter({ hasText: title })
has: this.page.locator('[data-testid="node-title"]', { hasText: title })
})
}
@@ -146,7 +146,7 @@ export class VueNodeHelpers {
expectedCount
)
} else {
await this.page.locator('[data-node-id]').first().waitFor()
await this.page.waitForSelector('[data-node-id]')
}
}

View File

@@ -1,97 +0,0 @@
import type { Locator, Page } from '@playwright/test'
import { TestIds } from '../selectors'
const ids = TestIds.outputHistory
export class OutputHistoryComponent {
constructor(private readonly page: Page) {}
get outputs(): Locator {
return this.page.getByTestId(ids.outputs)
}
get welcome(): Locator {
return this.page.getByTestId(ids.welcome)
}
get outputInfo(): Locator {
return this.page.getByTestId(ids.outputInfo)
}
get activeQueue(): Locator {
return this.page.getByTestId(ids.activeQueue)
}
get queueBadge(): Locator {
return this.page.getByTestId(ids.queueBadge)
}
get inProgressItems(): Locator {
return this.page.getByTestId(ids.inProgressItem)
}
get historyItems(): Locator {
return this.page.getByTestId(ids.historyItem)
}
get skeletons(): Locator {
return this.page.getByTestId(ids.skeleton)
}
get latentPreviews(): Locator {
return this.page.getByTestId(ids.latentPreview)
}
get imageOutputs(): Locator {
return this.page.getByTestId(ids.imageOutput)
}
get videoOutputs(): Locator {
return this.page.getByTestId(ids.videoOutput)
}
/** The currently selected (checked) in-progress item. */
get selectedInProgressItem(): Locator {
return this.page.locator(
`[data-testid="${ids.inProgressItem}"][data-state="checked"]`
)
}
/** The currently selected (checked) history item. */
get selectedHistoryItem(): Locator {
return this.page.locator(
`[data-testid="${ids.historyItem}"][data-state="checked"]`
)
}
/** The header-level progress bar. */
get headerProgressBar(): Locator {
return this.page.getByTestId(ids.headerProgressBar)
}
/** The in-progress item's progress bar (inside the thumbnail). */
get itemProgressBar(): Locator {
return this.inProgressItems.first().getByTestId(ids.itemProgressBar)
}
/** Overall progress in the header bar. */
get headerOverallProgress(): Locator {
return this.headerProgressBar.getByTestId(ids.progressOverall)
}
/** Node progress in the header bar. */
get headerNodeProgress(): Locator {
return this.headerProgressBar.getByTestId(ids.progressNode)
}
/** Overall progress in the in-progress item bar. */
get itemOverallProgress(): Locator {
return this.itemProgressBar.getByTestId(ids.progressOverall)
}
/** Node progress in the in-progress item bar. */
get itemNodeProgress(): Locator {
return this.itemProgressBar.getByTestId(ids.progressNode)
}
}

View File

@@ -52,6 +52,6 @@ export class SettingDialog extends BaseDialog {
name: 'About'
})
await aboutButton.click()
await this.page.locator('.about-container').waitFor()
await this.page.waitForSelector('.about-container')
}
}

View File

@@ -301,9 +301,7 @@ export class AssetsSidebarTab extends SidebarTab {
this.gridViewOption = page.getByText('Grid view')
this.sortNewestFirst = page.getByText('Newest first')
this.sortOldestFirst = page.getByText('Oldest first')
this.assetCards = page
.getByRole('button')
.and(page.locator('[data-selected]'))
this.assetCards = page.locator('[role="button"][data-selected]')
this.selectedCards = page.locator('[data-selected="true"]')
this.listViewItems = page.locator(
'.sidebar-content-container [role="button"][tabindex="0"]'

View File

@@ -3,7 +3,6 @@ import type { Locator, Page } from '@playwright/test'
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
import { TestIds } from '@e2e/fixtures/selectors'
import { OutputHistoryComponent } from '@e2e/fixtures/components/OutputHistory'
import { AppModeWidgetHelper } from '@e2e/fixtures/helpers/AppModeWidgetHelper'
import { BuilderFooterHelper } from '@e2e/fixtures/helpers/BuilderFooterHelper'
import { BuilderSaveAsHelper } from '@e2e/fixtures/helpers/BuilderSaveAsHelper'
@@ -15,7 +14,6 @@ export class AppModeHelper {
readonly footer: BuilderFooterHelper
readonly saveAs: BuilderSaveAsHelper
readonly select: BuilderSelectHelper
readonly outputHistory: OutputHistoryComponent
readonly widgets: AppModeWidgetHelper
/** The "Connect an output" popover shown when saving without outputs. */
public readonly connectOutputPopover: Locator
@@ -37,15 +35,12 @@ export class AppModeHelper {
public readonly backToWorkflowButton: Locator
/** The "Load template" button shown when no nodes exist. */
public readonly loadTemplateButton: Locator
/** The cancel button for an in-progress run in the output history. */
public readonly cancelRunButton: Locator
constructor(private readonly comfyPage: ComfyPage) {
this.steps = new BuilderStepsHelper(comfyPage)
this.footer = new BuilderFooterHelper(comfyPage)
this.saveAs = new BuilderSaveAsHelper(comfyPage)
this.select = new BuilderSelectHelper(comfyPage)
this.outputHistory = new OutputHistoryComponent(comfyPage.page)
this.widgets = new AppModeWidgetHelper(comfyPage)
this.connectOutputPopover = this.page.getByTestId(
TestIds.builder.connectOutputPopover
@@ -53,7 +48,7 @@ export class AppModeHelper {
this.outputPlaceholder = this.page.getByTestId(
TestIds.builder.outputPlaceholder
)
this.linearWidgets = this.page.getByTestId('linear-widgets')
this.linearWidgets = this.page.locator('[data-testid="linear-widgets"]')
this.imagePickerPopover = this.page
.getByRole('dialog')
.filter({ has: this.page.getByRole('button', { name: 'All' }) })
@@ -72,9 +67,6 @@ export class AppModeHelper {
this.loadTemplateButton = this.page.getByTestId(
TestIds.appMode.loadTemplate
)
this.cancelRunButton = this.page.getByTestId(
TestIds.outputHistory.cancelRun
)
}
private get page(): Page {

View File

@@ -1,211 +0,0 @@
import type { WebSocketRoute } from '@playwright/test'
import type { RawJobListItem } from '@/platform/remote/comfyui/jobs/jobTypes'
import type { ComfyPage } from '../ComfyPage'
import { createMockJob } from './AssetsHelper'
/**
* Helper for simulating prompt execution in e2e tests.
*/
export class ExecutionHelper {
private jobCounter = 0
private readonly completedJobs: RawJobListItem[] = []
private readonly page: ComfyPage['page']
private readonly command: ComfyPage['command']
private readonly assets: ComfyPage['assets']
constructor(
comfyPage: ComfyPage,
private readonly ws: WebSocketRoute
) {
this.page = comfyPage.page
this.command = comfyPage.command
this.assets = comfyPage.assets
}
/**
* Intercept POST /api/prompt, execute Comfy.QueuePrompt, and return
* the synthetic job ID.
*
* The app receives a valid PromptResponse so storeJob() fires
* and registers the job against the active workflow path.
*/
async run(): Promise<string> {
const jobId = `test-job-${++this.jobCounter}`
let fulfilled!: () => void
const prompted = new Promise<void>((r) => {
fulfilled = r
})
await this.page.route(
'**/api/prompt',
async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
prompt_id: jobId,
node_errors: {}
})
})
fulfilled()
},
{ times: 1 }
)
await this.command.executeCommand('Comfy.QueuePrompt')
await prompted
return jobId
}
/**
* Send a binary `b_preview_with_metadata` WS message (type 4).
* Encodes the metadata and a tiny 1x1 PNG so the app creates a blob URL.
*/
latentPreview(jobId: string, nodeId: string): void {
const metadata = JSON.stringify({
node_id: nodeId,
display_node_id: nodeId,
parent_node_id: nodeId,
real_node_id: nodeId,
prompt_id: jobId,
image_type: 'image/png'
})
const metadataBytes = new TextEncoder().encode(metadata)
// 1x1 red PNG
const png = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwADhQGAWjR9awAAAABJRU5ErkJggg==',
'base64'
)
// Binary format: [type:uint32][metadataLength:uint32][metadata][imageData]
const buf = new ArrayBuffer(8 + metadataBytes.length + png.length)
const view = new DataView(buf)
view.setUint32(0, 4) // type 4 = PREVIEW_IMAGE_WITH_METADATA
view.setUint32(4, metadataBytes.length)
new Uint8Array(buf, 8, metadataBytes.length).set(metadataBytes)
new Uint8Array(buf, 8 + metadataBytes.length).set(png)
this.ws.send(Buffer.from(buf))
}
/** Send `execution_start` WS event. */
executionStart(jobId: string): void {
this.ws.send(
JSON.stringify({
type: 'execution_start',
data: { prompt_id: jobId, timestamp: Date.now() }
})
)
}
/** Send `executing` WS event to signal which node is currently running. */
executing(jobId: string, nodeId: string | null): void {
this.ws.send(
JSON.stringify({
type: 'executing',
data: { prompt_id: jobId, node: nodeId }
})
)
}
/** Send `executed` WS event with node output. */
executed(
jobId: string,
nodeId: string,
output: Record<string, unknown>
): void {
this.ws.send(
JSON.stringify({
type: 'executed',
data: {
prompt_id: jobId,
node: nodeId,
display_node: nodeId,
output
}
})
)
}
/** Send `execution_success` WS event. */
executionSuccess(jobId: string): void {
this.ws.send(
JSON.stringify({
type: 'execution_success',
data: { prompt_id: jobId, timestamp: Date.now() }
})
)
}
/** Send `execution_error` WS event. */
executionError(jobId: string, nodeId: string, message: string): void {
this.ws.send(
JSON.stringify({
type: 'execution_error',
data: {
prompt_id: jobId,
timestamp: Date.now(),
node_id: nodeId,
node_type: 'Unknown',
exception_message: message,
exception_type: 'RuntimeError',
traceback: []
}
})
)
}
/** Send `progress` WS event. */
progress(jobId: string, nodeId: string, value: number, max: number): void {
this.ws.send(
JSON.stringify({
type: 'progress',
data: { prompt_id: jobId, node: nodeId, value, max }
})
)
}
/**
* Complete a job by adding it to mock history, sending execution_success,
* and triggering a history refresh via a status event.
*
* Requires an {@link AssetsHelper} to be passed in the constructor.
*/
async completeWithHistory(
jobId: string,
nodeId: string,
filename: string
): Promise<void> {
this.completedJobs.push(
createMockJob({
id: jobId,
preview_output: {
filename,
subfolder: '',
type: 'output',
nodeId,
mediaType: 'images'
}
})
)
await this.assets.mockOutputHistory(this.completedJobs)
this.executionSuccess(jobId)
// Trigger queue/history refresh
this.status(0)
}
/** Send `status` WS event to update queue count. */
status(queueRemaining: number): void {
this.ws.send(
JSON.stringify({
type: 'status',
data: { status: { exec_info: { queue_remaining: queueRemaining } } }
})
)
}
}

View File

@@ -157,7 +157,7 @@ export class SubgraphHelper {
// Wait for the appropriate UI element to appear
if (action === 'rightClick') {
await this.page.locator('.litemenu-entry').first().waitFor({
await this.page.waitForSelector('.litemenu-entry', {
state: 'visible',
timeout: 5000
})

View File

@@ -130,24 +130,6 @@ export const TestIds = {
outputPlaceholder: 'builder-output-placeholder',
connectOutputPopover: 'builder-connect-output-popover'
},
outputHistory: {
outputs: 'linear-outputs',
welcome: 'linear-welcome',
outputInfo: 'linear-output-info',
activeQueue: 'linear-job',
queueBadge: 'linear-job-badge',
inProgressItem: 'linear-in-progress-item',
historyItem: 'linear-history-item',
skeleton: 'linear-skeleton',
latentPreview: 'linear-latent-preview',
imageOutput: 'linear-image-output',
videoOutput: 'linear-video-output',
cancelRun: 'linear-cancel-run',
headerProgressBar: 'linear-header-progress-bar',
itemProgressBar: 'linear-item-progress-bar',
progressOverall: 'linear-progress-overall',
progressNode: 'linear-progress-node'
},
appMode: {
widgetItem: 'app-mode-widget-item',
welcome: 'linear-welcome',
@@ -198,7 +180,6 @@ export type TestIdValue =
| (typeof TestIds.selectionToolbox)[keyof typeof TestIds.selectionToolbox]
| (typeof TestIds.widgets)[keyof typeof TestIds.widgets]
| (typeof TestIds.builder)[keyof typeof TestIds.builder]
| (typeof TestIds.outputHistory)[keyof typeof TestIds.outputHistory]
| (typeof TestIds.appMode)[keyof typeof TestIds.appMode]
| (typeof TestIds.breadcrumb)[keyof typeof TestIds.breadcrumb]
| Exclude<

View File

@@ -14,11 +14,10 @@ function makeMatcher<T>(
) {
await expect(async () => {
const value = await getValue(node)
if (this.isNot) {
expect(value, 'Node is ' + type).not.toBeTruthy()
} else {
expect(value, 'Node is not ' + type).toBeTruthy()
}
const assertion = this.isNot
? expect(value, 'Node is ' + type).not
: expect(value, 'Node is not ' + type)
assertion.toBeTruthy()
}).toPass({ timeout: 5000, ...options })
return {
pass: !this.isNot,

View File

@@ -15,11 +15,13 @@ export class VueNodeFixture {
constructor(private readonly locator: Locator) {
this.header = locator.locator('[data-testid^="node-header-"]')
this.title = locator.getByTestId('node-title')
this.titleInput = locator.getByTestId('node-title-input')
this.title = locator.locator('[data-testid="node-title"]')
this.titleInput = locator.locator('[data-testid="node-title-input"]')
this.body = locator.locator('[data-testid^="node-body-"]')
this.pinIndicator = locator.getByTestId(TestIds.node.pinIndicator)
this.collapseButton = locator.getByTestId('node-collapse-button')
this.collapseButton = locator.locator(
'[data-testid="node-collapse-button"]'
)
this.collapseIcon = this.collapseButton.locator('i')
this.root = locator
}

View File

@@ -1,31 +1,53 @@
import { test as base } from '@playwright/test'
import type { WebSocketRoute } from '@playwright/test'
export const webSocketFixture = base.extend<{
getWebSocket: () => Promise<WebSocketRoute>
ws: { trigger(data: unknown, url?: string): Promise<void> }
}>({
getWebSocket: [
async ({ context }, use) => {
let latest: WebSocketRoute | undefined
let resolve: ((ws: WebSocketRoute) => void) | undefined
await context.routeWebSocket(/\/ws/, (ws) => {
const server = ws.connectToServer()
server.onMessage((message) => {
ws.send(message)
ws: [
async ({ page }, use) => {
// Each time a page loads, to catch navigations
page.on('load', async () => {
await page.evaluate(function () {
// Create a wrapper for WebSocket that stores them globally
// so we can look it up to trigger messages
const store: Record<string, WebSocket> = (window.__ws__ = {})
window.WebSocket = class extends window.WebSocket {
constructor(
...rest: ConstructorParameters<typeof window.WebSocket>
) {
super(...rest)
store[this.url] = this
}
}
})
latest = ws
resolve?.(ws)
})
await use(() => {
if (latest) return Promise.resolve(latest)
return new Promise<WebSocketRoute>((r) => {
resolve = r
})
await use({
async trigger(data, url) {
// Trigger a websocket event on the page
await page.evaluate(
function ([data, url]) {
if (!url) {
// If no URL specified, use page URL
const u = new URL(window.location.href)
u.hash = ''
u.protocol = 'ws:'
u.pathname = '/'
url = u.toString() + 'ws'
}
const ws: WebSocket = window.__ws__![url]
ws.dispatchEvent(
new MessageEvent('message', {
data
})
)
},
[JSON.stringify(data), url]
)
}
})
},
// We need this to run automatically as the first thing so it adds handlers as soon as the page loads
{ auto: true }
]
})

View File

@@ -1,6 +1,7 @@
import type { Response } from '@playwright/test'
import { expect, mergeTests } from '@playwright/test'
import type { StatusWsMessage } from '@/schemas/apiSchema'
import { comfyPageFixture } from '@e2e/fixtures/ComfyPage'
import { webSocketFixture } from '@e2e/fixtures/ws'
import type { WorkspaceStore } from '@e2e/types/globals'
@@ -17,10 +18,8 @@ test.describe('Actionbar', { tag: '@ui' }, () => {
*/
test('Does not auto-queue multiple changes at a time', async ({
comfyPage,
getWebSocket
ws
}) => {
const ws = await getWebSocket()
// Enable change auto-queue mode
const queueOpts = await comfyPage.actionbar.queueButton.toggleOptions()
await expect.poll(() => queueOpts.getMode()).toBe('disabled')
@@ -63,19 +62,17 @@ test.describe('Actionbar', { tag: '@ui' }, () => {
}
// Trigger a status websocket message
const triggerStatus = (queueSize: number) => {
ws.send(
JSON.stringify({
type: 'status',
data: {
status: {
exec_info: {
queue_remaining: queueSize
}
const triggerStatus = async (queueSize: number) => {
await ws.trigger({
type: 'status',
data: {
status: {
exec_info: {
queue_remaining: queueSize
}
}
})
)
}
} as StatusWsMessage)
}
// Extract the width from the queue response
@@ -107,8 +104,8 @@ test.describe('Actionbar', { tag: '@ui' }, () => {
).toBe(1)
// Trigger a status update so auto-queue re-runs
triggerStatus(1)
triggerStatus(0)
await triggerStatus(1)
await triggerStatus(0)
// Ensure the queued width is the last queued value
expect(

View File

@@ -16,7 +16,7 @@ test.describe('App mode welcome states', { tag: '@ui' }, () => {
await expect(comfyPage.appMode.welcome).toBeVisible()
await expect(comfyPage.appMode.emptyWorkflowText).toBeVisible()
await expect(comfyPage.appMode.buildAppButton).toBeHidden()
await expect(comfyPage.appMode.buildAppButton).not.toBeVisible()
})
test('Build app button is visible when no outputs selected', async ({
@@ -26,7 +26,7 @@ test.describe('App mode welcome states', { tag: '@ui' }, () => {
await expect(comfyPage.appMode.welcome).toBeVisible()
await expect(comfyPage.appMode.buildAppButton).toBeVisible()
await expect(comfyPage.appMode.emptyWorkflowText).toBeHidden()
await expect(comfyPage.appMode.emptyWorkflowText).not.toBeVisible()
})
test('Empty workflow and build app are hidden when app has outputs', async ({
@@ -35,8 +35,8 @@ test.describe('App mode welcome states', { tag: '@ui' }, () => {
await comfyPage.appMode.enterAppModeWithInputs([['3', 'seed']])
await expect(comfyPage.appMode.linearWidgets).toBeVisible()
await expect(comfyPage.appMode.emptyWorkflowText).toBeHidden()
await expect(comfyPage.appMode.buildAppButton).toBeHidden()
await expect(comfyPage.appMode.emptyWorkflowText).not.toBeVisible()
await expect(comfyPage.appMode.buildAppButton).not.toBeVisible()
})
test('Back to workflow returns to graph mode', async ({ comfyPage }) => {
@@ -46,7 +46,7 @@ test.describe('App mode welcome states', { tag: '@ui' }, () => {
await comfyPage.appMode.backToWorkflowButton.click()
await expect(comfyPage.canvas).toBeVisible()
await expect(comfyPage.appMode.welcome).toBeHidden()
await expect(comfyPage.appMode.welcome).not.toBeVisible()
})
test('Load template opens template selector', async ({ comfyPage }) => {

View File

@@ -11,7 +11,7 @@ test.describe('Bottom Panel Logs', { tag: '@ui' }, () => {
test('should open bottom panel via toggle button', async ({ comfyPage }) => {
const { bottomPanel } = comfyPage
await expect(bottomPanel.root).toBeHidden()
await expect(bottomPanel.root).not.toBeVisible()
await bottomPanel.toggleButton.click()
await expect(bottomPanel.root).toBeVisible()
})
@@ -35,7 +35,7 @@ test.describe('Bottom Panel Logs', { tag: '@ui' }, () => {
await expect(bottomPanel.root).toBeVisible()
await bottomPanel.toggleButton.click()
await expect(bottomPanel.root).toBeHidden()
await expect(bottomPanel.root).not.toBeVisible()
})
test('should switch between shortcuts and terminal panels', async ({
@@ -55,7 +55,7 @@ test.describe('Bottom Panel Logs', { tag: '@ui' }, () => {
await expect(logsTab).toBeVisible()
await expect(
comfyPage.page.locator('[id*="tab_shortcuts-essentials"]')
).toBeHidden()
).not.toBeVisible()
})
test('should persist Logs tab content in bottom panel', async ({

View File

@@ -10,11 +10,11 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => {
test('should toggle shortcuts panel visibility', async ({ comfyPage }) => {
const { bottomPanel } = comfyPage
await expect(bottomPanel.root).toBeHidden()
await expect(bottomPanel.root).not.toBeVisible()
await bottomPanel.keyboardShortcutsButton.click()
await expect(bottomPanel.root).toBeVisible()
await bottomPanel.keyboardShortcutsButton.click()
await expect(bottomPanel.root).toBeHidden()
await expect(bottomPanel.root).not.toBeVisible()
})
test('should display essentials shortcuts tab', async ({ comfyPage }) => {
@@ -182,7 +182,7 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => {
await expect(bottomPanel.root).toBeVisible()
await bottomPanel.keyboardShortcutsButton.click()
await expect(bottomPanel.root).toBeHidden()
await expect(bottomPanel.root).not.toBeVisible()
})
test('should display shortcuts in organized columns', async ({
@@ -192,7 +192,9 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => {
await bottomPanel.keyboardShortcutsButton.click()
await expect(comfyPage.page.getByTestId('shortcuts-columns')).toBeVisible()
await expect(
comfyPage.page.locator('[data-testid="shortcuts-columns"]')
).toBeVisible()
const subcategoryTitles = bottomPanel.shortcuts.subcategoryTitles
await expect.poll(() => subcategoryTitles.count()).toBeGreaterThanOrEqual(2)
@@ -203,7 +205,7 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => {
}) => {
const { bottomPanel } = comfyPage
await expect(bottomPanel.root).toBeHidden()
await expect(bottomPanel.root).not.toBeVisible()
await comfyPage.page.keyboard.press('Control+Shift+KeyK')

View File

@@ -22,7 +22,7 @@ async function saveCloseAndReopenAsApp(
await appMode.steps.goToPreview()
await builderSaveAs(appMode, workflowName)
await appMode.saveAs.closeButton.click()
await expect(appMode.saveAs.successDialog).toBeHidden()
await expect(appMode.saveAs.successDialog).not.toBeVisible()
await appMode.footer.exitBuilder()
await openWorkflowFromSidebar(comfyPage, workflowName)

View File

@@ -31,7 +31,7 @@ async function dismissSuccessDialog(
) {
const btn = button === 'close' ? saveAs.closeButton : saveAs.dismissButton
await btn.click()
await expect(saveAs.successDialog).toBeHidden()
await expect(saveAs.successDialog).not.toBeVisible()
}
test.describe('Builder save flow', { tag: ['@ui'] }, () => {
@@ -113,7 +113,7 @@ test.describe('Builder save flow', { tag: ['@ui'] }, () => {
await comfyPage.page.keyboard.press('Escape')
await comfyPage.nextFrame()
await expect(comfyPage.appMode.steps.toolbar).toBeHidden()
await expect(comfyPage.appMode.steps.toolbar).not.toBeVisible()
})
test('Exit builder button exits builder mode', async ({ comfyPage }) => {
@@ -121,7 +121,7 @@ test.describe('Builder save flow', { tag: ['@ui'] }, () => {
await expect(comfyPage.appMode.steps.toolbar).toBeVisible()
await comfyPage.appMode.footer.exitBuilder()
await expect(comfyPage.appMode.steps.toolbar).toBeHidden()
await expect(comfyPage.appMode.steps.toolbar).not.toBeVisible()
})
test('Save button directly saves for previously saved workflow', async ({
@@ -141,7 +141,7 @@ test.describe('Builder save flow', { tag: ['@ui'] }, () => {
await footer.saveButton.click()
await comfyPage.nextFrame()
await expect(saveAs.dialog).toBeHidden()
await expect(saveAs.dialog).not.toBeVisible()
await expect(footer.saveButton).toBeDisabled()
})
@@ -253,7 +253,7 @@ test.describe('Builder save flow', { tag: ['@ui'] }, () => {
await builderSaveAs(comfyPage.appMode, `${Date.now()} app-view`, 'App')
await comfyPage.appMode.saveAs.viewAppButton.click()
await expect(comfyPage.appMode.saveAs.successDialog).toBeHidden()
await expect(comfyPage.appMode.saveAs.successDialog).not.toBeVisible()
await expect
.poll(() => comfyPage.workflow.getActiveWorkflowActiveAppMode())
@@ -271,9 +271,9 @@ test.describe('Builder save flow', { tag: ['@ui'] }, () => {
)
await comfyPage.appMode.saveAs.exitBuilderButton.click()
await expect(comfyPage.appMode.saveAs.successDialog).toBeHidden()
await expect(comfyPage.appMode.saveAs.successDialog).not.toBeVisible()
await expect(comfyPage.appMode.steps.toolbar).toBeHidden()
await expect(comfyPage.appMode.steps.toolbar).not.toBeVisible()
})
test('save as with different mode does not modify the original workflow', async ({
@@ -327,7 +327,7 @@ test.describe('Builder save flow', { tag: ['@ui'] }, () => {
await expect(appMode.saveAs.overwriteDialog).toBeVisible()
await appMode.saveAs.overwriteButton.click()
await expect(appMode.saveAs.overwriteDialog).toBeHidden()
await expect(appMode.saveAs.overwriteDialog).not.toBeVisible()
await expect(appMode.saveAs.successMessage).toBeVisible()

View File

@@ -70,7 +70,7 @@ test.describe('CanvasModeSelector', { tag: '@canvas' }, () => {
await expect(menu).toBeVisible()
await trigger.click()
await comfyPage.nextFrame()
await expect(menu).toBeHidden()
await expect(menu).not.toBeVisible()
await expect(trigger).toHaveAttribute('aria-expanded', 'false')
})
@@ -81,7 +81,7 @@ test.describe('CanvasModeSelector', { tag: '@canvas' }, () => {
await expect(menu).toBeVisible()
await handItem.click()
await comfyPage.nextFrame()
await expect(menu).toBeHidden()
await expect(menu).not.toBeVisible()
})
test('closes when Escape is pressed', async ({ comfyPage }) => {
@@ -91,7 +91,7 @@ test.describe('CanvasModeSelector', { tag: '@canvas' }, () => {
await expect(menu).toBeVisible()
await selectItem.press('Escape')
await comfyPage.nextFrame()
await expect(menu).toBeHidden()
await expect(menu).not.toBeVisible()
await expect(trigger).toHaveAttribute('aria-expanded', 'false')
})
})
@@ -197,7 +197,7 @@ test.describe('CanvasModeSelector', { tag: '@canvas' }, () => {
await selectItem.press('ArrowDown')
await handItem.press('Escape')
await comfyPage.nextFrame()
await expect(menu).toBeHidden()
await expect(menu).not.toBeVisible()
await expect(trigger).toBeFocused()
})
})

View File

@@ -38,13 +38,13 @@ test.describe('Default Keybindings', { tag: '@keyboard' }, () => {
`.${tabId}-tab-button.side-bar-button-selected`
)
await expect(selectedButton).toBeHidden()
await expect(selectedButton).not.toBeVisible()
await comfyPage.canvas.press(key)
await expect(selectedButton).toBeVisible()
await comfyPage.canvas.press(key)
await expect(selectedButton).toBeHidden()
await expect(selectedButton).not.toBeVisible()
})
}
})
@@ -172,7 +172,7 @@ test.describe('Default Keybindings', { tag: '@keyboard' }, () => {
// Toggle off with Alt+m
await comfyPage.page.keyboard.press('Alt+KeyM')
await expect(comfyPage.appMode.linearWidgets).toBeHidden()
await expect(comfyPage.appMode.linearWidgets).not.toBeVisible()
// Toggle on again
await comfyPage.page.keyboard.press('Alt+KeyM')
@@ -189,7 +189,7 @@ test.describe('Default Keybindings', { tag: '@keyboard' }, () => {
await expect(minimap).toBeVisible()
await comfyPage.page.keyboard.press('Alt+Shift+KeyM')
await expect(minimap).toBeHidden()
await expect(minimap).not.toBeVisible()
await comfyPage.page.keyboard.press('Alt+Shift+KeyM')
await expect(minimap).toBeVisible()
@@ -198,13 +198,13 @@ test.describe('Default Keybindings', { tag: '@keyboard' }, () => {
test("'Ctrl+`' toggles terminal/logs panel", async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await expect(comfyPage.bottomPanel.root).toBeHidden()
await expect(comfyPage.bottomPanel.root).not.toBeVisible()
await comfyPage.page.keyboard.press('Control+Backquote')
await expect(comfyPage.bottomPanel.root).toBeVisible()
await comfyPage.page.keyboard.press('Control+Backquote')
await expect(comfyPage.bottomPanel.root).toBeHidden()
await expect(comfyPage.bottomPanel.root).not.toBeVisible()
})
})

View File

@@ -11,7 +11,9 @@ test.beforeEach(async ({ comfyPage }) => {
test.describe('Settings', () => {
test('@mobile Should be visible on mobile', async ({ comfyPage }) => {
await comfyPage.page.keyboard.press('Control+,')
const settingsDialog = comfyPage.page.getByTestId('settings-dialog')
const settingsDialog = comfyPage.page.locator(
'[data-testid="settings-dialog"]'
)
await expect(settingsDialog).toBeVisible()
const contentArea = settingsDialog.locator('main')
await expect(contentArea).toBeVisible()
@@ -24,16 +26,17 @@ test.describe('Settings', () => {
await comfyPage.page.keyboard.down('ControlOrMeta')
await comfyPage.page.keyboard.press(',')
await comfyPage.page.keyboard.up('ControlOrMeta')
const settingsLocator = comfyPage.page.getByTestId('settings-dialog')
const settingsLocator = comfyPage.page.locator(
'[data-testid="settings-dialog"]'
)
await expect(settingsLocator).toBeVisible()
await comfyPage.page.keyboard.press('Escape')
await expect(settingsLocator).toBeHidden()
await expect(settingsLocator).not.toBeVisible()
})
test('Can change canvas zoom speed setting', async ({ comfyPage }) => {
const maxSpeed = 2.5
await comfyPage.settings.setSetting('Comfy.Graph.ZoomSpeed', maxSpeed)
await test.step('Setting should persist', async () => {
await expect
.poll(() => comfyPage.settings.getSetting('Comfy.Graph.ZoomSpeed'))
@@ -46,7 +49,9 @@ test.describe('Settings', () => {
await comfyPage.page.keyboard.press('Control+,')
// Open the keybinding tab
const settingsDialog = comfyPage.page.getByTestId('settings-dialog')
const settingsDialog = comfyPage.page.locator(
'[data-testid="settings-dialog"]'
)
await expect(settingsDialog).toBeVisible()
await settingsDialog
.locator('nav [role="button"]', { hasText: 'Keybinding' })

View File

@@ -307,7 +307,7 @@ test.describe('ManagerDialog', { tag: '@ui' }, () => {
await searchInput.fill('Test Pack B')
await expect(dialog.getByText('Test Pack B')).toBeVisible()
await expect(dialog.getByText('Test Pack A')).toBeHidden()
await expect(dialog.getByText('Test Pack A')).not.toBeVisible()
})
test('Clicking a pack card opens the info panel', async ({ comfyPage }) => {
@@ -360,7 +360,7 @@ test.describe('ManagerDialog', { tag: '@ui' }, () => {
await expect(dialog).toBeVisible()
await comfyPage.page.keyboard.press('Escape')
await expect(dialog).toBeHidden()
await expect(dialog).not.toBeVisible()
})
test('Empty search shows no results message', async ({ comfyPage }) => {

View File

@@ -60,7 +60,7 @@ test.describe('Queue Clear History Dialog', { tag: '@ui' }, () => {
})
await dialog.getByRole('button', { name: 'Cancel' }).click()
await expect(dialog).toBeHidden()
await expect(dialog).not.toBeVisible()
expect(clearCalled).toBe(false)
await comfyPage.page.unroute('**/api/history')
@@ -83,7 +83,7 @@ test.describe('Queue Clear History Dialog', { tag: '@ui' }, () => {
})
await dialog.getByLabel('Close').click()
await expect(dialog).toBeHidden()
await expect(dialog).not.toBeVisible()
expect(clearCalled).toBe(false)
await comfyPage.page.unroute('**/api/history')
@@ -106,7 +106,7 @@ test.describe('Queue Clear History Dialog', { tag: '@ui' }, () => {
const request = await clearPromise
expect(request.postDataJSON()).toEqual({ clear: true })
await expect(dialog).toBeHidden()
await expect(dialog).not.toBeVisible()
})
test('Dialog state resets after close and reopen', async ({ comfyPage }) => {
@@ -114,7 +114,7 @@ test.describe('Queue Clear History Dialog', { tag: '@ui' }, () => {
const dialog = comfyPage.confirmDialog.root
await expect(dialog).toBeVisible()
await dialog.getByRole('button', { name: 'Cancel' }).click()
await expect(dialog).toBeHidden()
await expect(dialog).not.toBeVisible()
await comfyPage.queuePanel.openClearHistoryDialog()
await expect(dialog).toBeVisible()

View File

@@ -61,7 +61,7 @@ test.describe('Settings dialog', { tag: '@ui' }, () => {
await expect(dialog.root).toBeVisible()
await dialog.close()
await expect(dialog.root).toBeHidden()
await expect(dialog.root).not.toBeVisible()
})
test('Escape key closes dialog', async ({ comfyPage }) => {
@@ -70,7 +70,7 @@ test.describe('Settings dialog', { tag: '@ui' }, () => {
await expect(dialog.root).toBeVisible()
await comfyPage.page.keyboard.press('Escape')
await expect(dialog.root).toBeHidden()
await expect(dialog.root).not.toBeVisible()
})
test('Search filters settings list', async ({ comfyPage }) => {

View File

@@ -10,7 +10,7 @@ test.describe('DOM Widget', { tag: '@widget' }, () => {
test('Collapsed multiline textarea is not visible', async ({ comfyPage }) => {
await comfyPage.workflow.loadWorkflow('widgets/collapsed_multiline')
const textareaWidget = comfyPage.page.locator('.comfy-multiline-input')
await expect(textareaWidget).toBeHidden()
await expect(textareaWidget).not.toBeVisible()
})
test('Multiline textarea correctly collapses', async ({ comfyPage }) => {
@@ -25,8 +25,8 @@ test.describe('DOM Widget', { tag: '@widget' }, () => {
for (const node of nodes) {
await node.click('collapse')
}
await expect(firstMultiline).toBeHidden()
await expect(lastMultiline).toBeHidden()
await expect(firstMultiline).not.toBeVisible()
await expect(lastMultiline).not.toBeVisible()
})
test(
@@ -35,7 +35,7 @@ test.describe('DOM Widget', { tag: '@widget' }, () => {
async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.command.executeCommand('Workspace.ToggleFocusMode')
await expect(comfyPage.menu.sideToolbar).toBeHidden()
await expect(comfyPage.menu.sideToolbar).not.toBeVisible()
await expect(comfyPage.canvas).toHaveScreenshot('focus-mode-on.png')
}
)

View File

@@ -74,7 +74,7 @@ test.describe('Error dialog', () => {
}) => {
const errorDialog = await triggerConfigureError(comfyPage)
await expect(errorDialog).toBeVisible()
await expect(errorDialog.locator('pre')).toBeHidden()
await expect(errorDialog.locator('pre')).not.toBeVisible()
await errorDialog.getByTestId(TestIds.dialogs.errorDialogShowReport).click()
@@ -83,7 +83,7 @@ test.describe('Error dialog', () => {
await expect(reportPre).toHaveText(/\S/)
await expect(
errorDialog.getByTestId(TestIds.dialogs.errorDialogShowReport)
).toBeHidden()
).not.toBeVisible()
})
test('Should copy report to clipboard when "Copy to Clipboard" is clicked', async ({

View File

@@ -100,7 +100,7 @@ test.describe('Error overlay', { tag: '@ui' }, () => {
await errorOverlay
.getByTestId(TestIds.dialogs.errorOverlayDismiss)
.click()
await expect(errorOverlay).toBeHidden()
await expect(errorOverlay).not.toBeVisible()
await comfyPage.canvas.click()
await comfyPage.nextFrame()
@@ -112,10 +112,10 @@ test.describe('Error overlay', { tag: '@ui' }, () => {
await comfyPage.nextFrame()
await comfyPage.keyboard.undo()
await expect(errorOverlay).toBeHidden()
await expect(errorOverlay).not.toBeVisible()
await comfyPage.keyboard.redo()
await expect(errorOverlay).toBeHidden()
await expect(errorOverlay).not.toBeVisible()
})
})
@@ -156,7 +156,7 @@ test.describe('Error overlay', { tag: '@ui' }, () => {
await overlay.getByTestId(TestIds.dialogs.errorOverlaySeeErrors).click()
await expect(overlay).toBeHidden()
await expect(overlay).not.toBeVisible()
await expect(comfyPage.page.getByTestId('properties-panel')).toBeVisible()
})
@@ -168,7 +168,7 @@ test.describe('Error overlay', { tag: '@ui' }, () => {
await overlay.getByTestId(TestIds.dialogs.errorOverlaySeeErrors).click()
await expect(overlay).toBeHidden()
await expect(overlay).not.toBeVisible()
})
test('"Dismiss" closes overlay without opening panel', async ({
@@ -181,8 +181,10 @@ test.describe('Error overlay', { tag: '@ui' }, () => {
await overlay.getByTestId(TestIds.dialogs.errorOverlayDismiss).click()
await expect(overlay).toBeHidden()
await expect(comfyPage.page.getByTestId('properties-panel')).toBeHidden()
await expect(overlay).not.toBeVisible()
await expect(
comfyPage.page.getByTestId('properties-panel')
).not.toBeVisible()
})
test('Close button (X) dismisses overlay', async ({ comfyPage }) => {
@@ -193,7 +195,7 @@ test.describe('Error overlay', { tag: '@ui' }, () => {
await overlay.getByRole('button', { name: /close/i }).click()
await expect(overlay).toBeHidden()
await expect(overlay).not.toBeVisible()
})
})
})

View File

@@ -69,7 +69,7 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
expect(flags?.data).not.toBeNull()
expect(flags?.data).toHaveProperty('supports_preview_metadata')
expect(typeof flags?.data?.supports_preview_metadata).toBe('boolean')
}).toPass({ timeout: 5000 })
}).toPass()
// Verify server sent feature flags back
await expect(async () => {
@@ -82,7 +82,7 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
expect(flags).toHaveProperty('max_upload_size')
expect(typeof flags?.max_upload_size).toBe('number')
expect(Object.keys(flags ?? {}).length).toBeGreaterThan(0)
}).toPass({ timeout: 5000 })
}).toPass()
await newPage.close()
})
@@ -102,7 +102,7 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
expect(typeof flags.supports_preview_metadata).toBe('boolean')
expect(flags).toHaveProperty('max_upload_size')
expect(typeof flags.max_upload_size).toBe('number')
}).toPass({ timeout: 5000 })
}).toPass()
})
test('serverSupportsFeature method works with real backend flags', async ({
@@ -182,7 +182,7 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
)
expect(typeof maxUpload).toBe('number')
expect(maxUpload as number).toBeGreaterThan(0)
}).toPass({ timeout: 5000 })
}).toPass()
// Test getServerFeature with default value for non-existent feature
await expect
@@ -210,7 +210,7 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
expect(typeof features.supports_preview_metadata).toBe('boolean')
expect(features).toHaveProperty('max_upload_size')
expect(Object.keys(features).length).toBeGreaterThan(0)
}).toPass({ timeout: 5000 })
}).toPass()
})
test('Client feature flags are immutable', async ({ comfyPage }) => {
@@ -348,14 +348,14 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
expect(flags).toHaveProperty('supports_preview_metadata')
expect(typeof flags?.supports_preview_metadata).toBe('boolean')
expect(flags).toHaveProperty('max_upload_size')
}).toPass({ timeout: 5000 })
}).toPass()
// Verify feature flags were received and API was initialized
await expect(async () => {
const readiness = await newPage.evaluate(() => window.__appReadiness)
expect(readiness?.featureFlagsReceived).toBe(true)
expect(readiness?.apiInitialized).toBe(true)
}).toPass({ timeout: 5000 })
}).toPass()
await newPage.close()
})

View File

@@ -14,12 +14,12 @@ test.describe('Focus Mode', { tag: '@ui' }, () => {
await comfyPage.setFocusMode(true)
await expect(comfyPage.menu.sideToolbar).toBeHidden()
await expect(comfyPage.menu.sideToolbar).not.toBeVisible()
})
test('Focus mode restores UI chrome', async ({ comfyPage }) => {
await comfyPage.setFocusMode(true)
await expect(comfyPage.menu.sideToolbar).toBeHidden()
await expect(comfyPage.menu.sideToolbar).not.toBeVisible()
await comfyPage.setFocusMode(false)
await expect(comfyPage.menu.sideToolbar).toBeVisible()
@@ -29,7 +29,7 @@ test.describe('Focus Mode', { tag: '@ui' }, () => {
await expect(comfyPage.menu.sideToolbar).toBeVisible()
await comfyPage.command.executeCommand('Workspace.ToggleFocusMode')
await expect(comfyPage.menu.sideToolbar).toBeHidden()
await expect(comfyPage.menu.sideToolbar).not.toBeVisible()
await comfyPage.command.executeCommand('Workspace.ToggleFocusMode')
await expect(comfyPage.menu.sideToolbar).toBeVisible()
@@ -41,7 +41,7 @@ test.describe('Focus Mode', { tag: '@ui' }, () => {
await comfyPage.setFocusMode(true)
await expect(topMenu).toBeHidden()
await expect(topMenu).not.toBeVisible()
})
test('Canvas remains visible in focus mode', async ({ comfyPage }) => {
@@ -52,12 +52,12 @@ test.describe('Focus Mode', { tag: '@ui' }, () => {
test('Focus mode can be toggled multiple times', async ({ comfyPage }) => {
await comfyPage.setFocusMode(true)
await expect(comfyPage.menu.sideToolbar).toBeHidden()
await expect(comfyPage.menu.sideToolbar).not.toBeVisible()
await comfyPage.setFocusMode(false)
await expect(comfyPage.menu.sideToolbar).toBeVisible()
await comfyPage.setFocusMode(true)
await expect(comfyPage.menu.sideToolbar).toBeHidden()
await expect(comfyPage.menu.sideToolbar).not.toBeVisible()
})
})

View File

@@ -109,6 +109,6 @@ test.describe('Graph', { tag: ['@smoke', '@canvas'] }, () => {
expect(r.switchOutputLinkIds).toEqual(
expect.arrayContaining([r.cfg85LinkId, r.cfg86LinkId])
)
}).toPass({ timeout: 5000 })
}).toPass()
})
})

View File

@@ -94,6 +94,6 @@ test.describe('Graph Canvas Menu', { tag: ['@screenshot', '@canvas'] }, () => {
await backdrop.click()
// Modal should be hidden
await expect(zoomModal).toBeHidden()
await expect(zoomModal).not.toBeVisible()
})
})

View File

@@ -88,10 +88,7 @@ test.describe('Group Node', { tag: '@node' }, () => {
.getNode(groupNodeName)
.locator('.bookmark-button')
.click()
await comfyPage.page
.locator('.p-tree-node-label.tree-explorer-node-label')
.first()
.hover()
await comfyPage.page.hover('.p-tree-node-label.tree-explorer-node-label')
await expect(
comfyPage.page.locator('.node-lib-node-preview')
).toBeVisible()
@@ -102,7 +99,6 @@ test.describe('Group Node', { tag: '@node' }, () => {
.click()
})
})
test(
'Can be added to canvas using search',
{ tag: '@screenshot' },
@@ -158,7 +154,7 @@ test.describe('Group Node', { tag: '@node' }, () => {
await comfyPage.nextFrame()
await expect(manage1.selectedNodeTypeSelect).toHaveValue('g1')
await manage1.close()
await expect(manage1.root).toBeHidden()
await expect(manage1.root).not.toBeVisible()
const manage2 = await group2.manageGroupNode()
await expect(manage2.selectedNodeTypeSelect).toHaveValue('g2')
@@ -245,7 +241,7 @@ test.describe('Group Node', { tag: '@node' }, () => {
await expect.poll(() => comfyPage.nodeOps.getGraphNodesCount()).toBe(1)
await expect(
comfyPage.page.getByTestId(TestIds.dialogs.errorOverlay)
).toBeHidden()
).not.toBeVisible()
})
test.describe('Copy and paste', () => {
@@ -353,7 +349,6 @@ test.describe('Group Node', { tag: '@node' }, () => {
await comfyPage.page.keyboard.press('Alt+g')
await expect(comfyPage.toast.visibleToasts).toHaveCount(1)
})
test('Convert to group node, selected 1 node', async ({ comfyPage }) => {
await expect(comfyPage.toast.visibleToasts).toHaveCount(0)
await comfyPage.canvas.click({

View File

@@ -46,7 +46,7 @@ test.describe('Image Compare', () => {
await expect(node).toContainText('No images to compare')
await expect(node.locator('img')).toHaveCount(0)
await expect(node.getByRole('presentation')).toHaveCount(0)
await expect(node.locator('[role="presentation"]')).toHaveCount(0)
}
)
@@ -67,7 +67,7 @@ test.describe('Image Compare', () => {
await expect(beforeImg).toBeVisible()
await expect(afterImg).toBeVisible()
const handle = node.getByRole('presentation')
const handle = node.locator('[role="presentation"]')
await expect(handle).toBeVisible()
expect(

View File

@@ -180,48 +180,6 @@ test.describe('Node Interaction', () => {
})
})
test.describe('Node Duplication', () => {
test.beforeEach(async ({ comfyPage }) => {
// Pin this suite to the legacy canvas path so Alt+drag exercises
// LGraphCanvas, not the Vue node drag handler.
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', false)
await comfyPage.nextFrame()
})
test('Can duplicate a regular node via Alt+drag', async ({ comfyPage }) => {
const before = await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
expect(
before,
'Expected exactly 2 CLIPTextEncode nodes in default graph'
).toHaveLength(2)
const target = before[0]
const pos = await target.getPosition()
const src = { x: pos.x + 16, y: pos.y + 16 }
await comfyPage.page.mouse.move(src.x, src.y)
await comfyPage.page.keyboard.down('Alt')
try {
await comfyPage.page.mouse.down()
await comfyPage.nextFrame()
await comfyPage.page.mouse.move(src.x + 120, src.y + 80, { steps: 20 })
await comfyPage.page.mouse.up()
await comfyPage.nextFrame()
} finally {
await comfyPage.page.keyboard.up('Alt')
}
await comfyPage.canvasOps.moveMouseToEmptyArea()
await expect
.poll(
async () =>
(await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')).length
)
.toBe(3)
expect(await target.exists()).toBe(true)
})
})
test.describe('Edge Interaction', { tag: '@screenshot' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting(
@@ -1294,7 +1252,7 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => {
test('Space + left-click drag should pan canvas', async ({ comfyPage }) => {
// Click canvas to focus it
await comfyPage.canvas.click()
await comfyPage.page.click('canvas')
await comfyPage.nextFrame()
await comfyPage.page.keyboard.down('Space')
@@ -1363,7 +1321,7 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => {
'panning'
)
await comfyPage.canvas.click()
await comfyPage.page.click('canvas')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('standard-initial.png')

View File

@@ -25,7 +25,7 @@ test.describe('Job History Actions', { tag: '@ui' }, () => {
await openMoreOptionsPopover(comfyPage)
await expect(
comfyPage.page.getByTestId('docked-job-history-action')
comfyPage.page.locator('[data-testid="docked-job-history-action"]')
).toBeVisible()
})
@@ -34,7 +34,9 @@ test.describe('Job History Actions', { tag: '@ui' }, () => {
}) => {
await openMoreOptionsPopover(comfyPage)
const action = comfyPage.page.getByTestId('docked-job-history-action')
const action = comfyPage.page.locator(
'[data-testid="docked-job-history-action"]'
)
await expect(action).toBeVisible()
await expect(action).not.toBeEmpty()
})
@@ -43,7 +45,7 @@ test.describe('Job History Actions', { tag: '@ui' }, () => {
await openMoreOptionsPopover(comfyPage)
await expect(
comfyPage.page.getByTestId('show-run-progress-bar-action')
comfyPage.page.locator('[data-testid="show-run-progress-bar-action"]')
).toBeVisible()
})
@@ -51,18 +53,20 @@ test.describe('Job History Actions', { tag: '@ui' }, () => {
await openMoreOptionsPopover(comfyPage)
await expect(
comfyPage.page.getByTestId('clear-history-action')
comfyPage.page.locator('[data-testid="clear-history-action"]')
).toBeVisible()
})
test('Clicking docked job history closes popover', async ({ comfyPage }) => {
await openMoreOptionsPopover(comfyPage)
const action = comfyPage.page.getByTestId('docked-job-history-action')
const action = comfyPage.page.locator(
'[data-testid="docked-job-history-action"]'
)
await expect(action).toBeVisible()
await action.click()
await expect(action).toBeHidden()
await expect(action).not.toBeVisible()
})
test('Clicking show run progress bar toggles setting', async ({
@@ -74,7 +78,9 @@ test.describe('Job History Actions', { tag: '@ui' }, () => {
await openMoreOptionsPopover(comfyPage)
const action = comfyPage.page.getByTestId('show-run-progress-bar-action')
const action = comfyPage.page.locator(
'[data-testid="show-run-progress-bar-action"]'
)
await action.click()
await expect

View File

@@ -14,38 +14,48 @@ test.describe('Linear Mode', { tag: '@ui' }, () => {
}) => {
await comfyPage.appMode.enterAppModeWithInputs([])
await expect(comfyPage.page.getByTestId('linear-widgets')).toBeVisible()
await expect(
comfyPage.page.locator('[data-testid="linear-widgets"]')
).toBeVisible()
})
test('Run button visible in linear mode', async ({ comfyPage }) => {
await comfyPage.appMode.enterAppModeWithInputs([])
await expect(comfyPage.page.getByTestId('linear-run-button')).toBeVisible()
await expect(
comfyPage.page.locator('[data-testid="linear-run-button"]')
).toBeVisible()
})
test('Workflow info section visible', async ({ comfyPage }) => {
await comfyPage.appMode.enterAppModeWithInputs([])
await expect(
comfyPage.page.getByTestId('linear-workflow-info')
comfyPage.page.locator('[data-testid="linear-workflow-info"]')
).toBeVisible()
})
test('Returns to graph mode', async ({ comfyPage }) => {
await comfyPage.appMode.enterAppModeWithInputs([])
await expect(comfyPage.page.getByTestId('linear-widgets')).toBeVisible()
await expect(
comfyPage.page.locator('[data-testid="linear-widgets"]')
).toBeVisible()
await comfyPage.appMode.toggleAppMode()
await expect(comfyPage.canvas).toBeVisible()
await expect(comfyPage.page.getByTestId('linear-widgets')).toBeHidden()
await expect(
comfyPage.page.locator('[data-testid="linear-widgets"]')
).not.toBeVisible()
})
test('Canvas not visible in app mode', async ({ comfyPage }) => {
await comfyPage.appMode.enterAppModeWithInputs([])
await expect(comfyPage.page.getByTestId('linear-widgets')).toBeVisible()
await expect(comfyPage.canvas).toBeHidden()
await expect(
comfyPage.page.locator('[data-testid="linear-widgets"]')
).toBeVisible()
await expect(comfyPage.canvas).not.toBeVisible()
})
})

View File

@@ -101,7 +101,7 @@ test.describe('Menu', { tag: '@ui' }, () => {
// Check initial state of bottom panel (it's initially hidden)
const { bottomPanel } = comfyPage
await expect(bottomPanel.root).toBeHidden()
await expect(bottomPanel.root).not.toBeVisible()
// Checkmark should be invisible initially (panel is hidden)
await expect(checkmark).toHaveClass(/invisible/)
@@ -126,7 +126,7 @@ test.describe('Menu', { tag: '@ui' }, () => {
await expect(viewSubmenu).toBeVisible()
// Verify bottom panel is hidden again
await expect(bottomPanel.root).toBeHidden()
await expect(bottomPanel.root).not.toBeVisible()
// Checkmark should be invisible again (panel is hidden)
await expect(checkmark).toHaveClass(/invisible/)
@@ -138,7 +138,7 @@ test.describe('Menu', { tag: '@ui' }, () => {
.click({ position: { x: viewport.width - 10, y: 10 } })
// Verify menu is now closed
await expect(menu).toBeHidden()
await expect(menu).not.toBeVisible()
})
test('Displays keybinding next to item', async ({ comfyPage }) => {

View File

@@ -53,7 +53,7 @@ test.describe('Minimap', { tag: '@canvas' }, () => {
await expect(minimapContainer).toBeVisible()
await toggleButton.click()
await expect(minimapContainer).toBeHidden()
await expect(minimapContainer).not.toBeVisible()
await toggleButton.click()
await expect(minimapContainer).toBeVisible()
@@ -65,7 +65,7 @@ test.describe('Minimap', { tag: '@canvas' }, () => {
await expect(minimapContainer).toBeVisible()
await comfyPage.page.keyboard.press('Alt+KeyM')
await expect(minimapContainer).toBeHidden()
await expect(minimapContainer).not.toBeVisible()
await comfyPage.page.keyboard.press('Alt+KeyM')
await expect(minimapContainer).toBeVisible()
@@ -76,7 +76,7 @@ test.describe('Minimap', { tag: '@canvas' }, () => {
await expect(minimap).toBeVisible()
await comfyPage.page.getByTestId(TestIds.canvas.closeMinimapButton).click()
await expect(minimap).toBeHidden()
await expect(minimap).not.toBeVisible()
const toggleButton = comfyPage.page.getByTestId(
TestIds.canvas.toggleMinimapButton

View File

@@ -39,7 +39,9 @@ test.describe(
await expect(comfyPage.page.locator('.selection-toolbox')).toBeVisible()
const moreOptionsBtn = comfyPage.page.getByTestId('more-options-button')
const moreOptionsBtn = comfyPage.page.locator(
'[data-testid="more-options-button"]'
)
await expect(moreOptionsBtn).toBeVisible()
await moreOptionsBtn.click()
await comfyPage.nextFrame()

View File

@@ -40,14 +40,13 @@ test.describe('Optional input', { tag: ['@screenshot', '@node'] }, () => {
await expect.poll(() => comfyPage.nodeOps.getGraphNodesCount()).toBe(1)
await expect(
comfyPage.page.getByTestId(TestIds.dialogs.errorOverlay)
).toBeHidden()
).not.toBeVisible()
// If the node's multiline text widget is visible, then it was loaded successfully
await expect(comfyPage.page.locator('.comfy-multiline-input')).toHaveCount(
1
)
})
test('Old workflow with converted input', async ({ comfyPage }) => {
await comfyPage.workflow.loadWorkflow('inputs/old_workflow_converted_input')
const node = await comfyPage.nodeOps.getNodeRefById('1')
@@ -63,7 +62,6 @@ test.describe('Optional input', { tag: ['@screenshot', '@node'] }, () => {
expect(vaeInput!.link).toBeNull()
expect(convertedInput!.link).not.toBeNull()
})
test('Renamed converted input', async ({ comfyPage }) => {
await comfyPage.workflow.loadWorkflow('inputs/renamed_converted_widget')
const node = await comfyPage.nodeOps.getNodeRefById('3')
@@ -71,12 +69,10 @@ test.describe('Optional input', { tag: ['@screenshot', '@node'] }, () => {
const renamedInput = inputs.find((w) => w.name === 'breadth')
expect(renamedInput).toBeUndefined()
})
test('slider', async ({ comfyPage }) => {
await comfyPage.workflow.loadWorkflow('inputs/simple_slider')
await expect(comfyPage.canvas).toHaveScreenshot('simple_slider.png')
})
test('unknown converted widget', async ({ comfyPage }) => {
await comfyPage.workflow.loadWorkflow(
'missing/missing_nodes_converted_widget'
@@ -85,7 +81,6 @@ test.describe('Optional input', { tag: ['@screenshot', '@node'] }, () => {
'missing_nodes_converted_widget.png'
)
})
test('dynamically added input', async ({ comfyPage }) => {
await comfyPage.workflow.loadWorkflow('inputs/dynamically_added_input')
await expect(comfyPage.canvas).toHaveScreenshot(

View File

@@ -191,7 +191,7 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => {
).toBeVisible()
// Verify help page is no longer visible
await expect(helpPage.locator('.node-help-content')).toBeHidden()
await expect(helpPage.locator('.node-help-content')).not.toBeVisible()
})
})
@@ -505,7 +505,7 @@ This is English documentation.
// Should show fallback content (node description)
await expect(helpPage).toBeVisible()
await expect(helpPage.locator('.p-progressspinner')).toBeHidden()
await expect(helpPage.locator('.p-progressspinner')).not.toBeVisible()
// Should show some content even on error
await expect(helpPage).not.toHaveText('')

View File

@@ -203,7 +203,7 @@ test.describe('Node search box', { tag: '@node' }, () => {
await comfyPage.page.keyboard.press('Escape')
// Verify the filter selection panel is hidden
await expect(panel.header).toBeHidden()
await expect(panel.header).not.toBeVisible()
// Verify the node search dialog is still visible
await expect(comfyPage.searchBox.input).toBeVisible()

View File

@@ -29,7 +29,7 @@ test.describe('Node search box V2', { tag: '@node' }, () => {
await expect(searchBoxV2.results.first()).toBeVisible()
await comfyPage.page.keyboard.press('Enter')
await expect(searchBoxV2.input).toBeHidden()
await expect(searchBoxV2.input).not.toBeVisible()
await expect
.poll(() => comfyPage.nodeOps.getGraphNodesCount())
@@ -48,7 +48,7 @@ test.describe('Node search box V2', { tag: '@node' }, () => {
// Enter should add the first (selected) result
await comfyPage.page.keyboard.press('Enter')
await expect(searchBoxV2.input).toBeHidden()
await expect(searchBoxV2.input).not.toBeVisible()
await expect
.poll(() => comfyPage.nodeOps.getGraphNodesCount())
@@ -141,7 +141,7 @@ test.describe('Node search box V2', { tag: '@node' }, () => {
// Enter selects and adds node
await comfyPage.page.keyboard.press('Enter')
await expect(searchBoxV2.input).toBeHidden()
await expect(searchBoxV2.input).not.toBeVisible()
await expect
.poll(() => comfyPage.nodeOps.getGraphNodesCount())

View File

@@ -39,7 +39,7 @@ test.describe('Node search box V2 extended', { tag: '@node' }, () => {
await expect(searchBoxV2.results.first()).toBeVisible()
await comfyPage.page.keyboard.press('Escape')
await expect(searchBoxV2.input).toBeHidden()
await expect(searchBoxV2.input).not.toBeVisible()
await expect
.poll(() => comfyPage.nodeOps.getGraphNodesCount())
@@ -56,7 +56,7 @@ test.describe('Node search box V2 extended', { tag: '@node' }, () => {
await expect(searchBoxV2.results.first()).toBeVisible()
await comfyPage.page.keyboard.press('Escape')
await expect(searchBoxV2.input).toBeHidden()
await expect(searchBoxV2.input).not.toBeVisible()
await comfyPage.canvasOps.doubleClick()
await expect(searchBoxV2.input).toBeVisible()
@@ -104,7 +104,9 @@ test.describe('Node search box V2 extended', { tag: '@node' }, () => {
.click()
// Verify filter chip appeared and results changed
const filterChip = searchBoxV2.dialog.getByTestId('filter-chip')
const filterChip = searchBoxV2.dialog.locator(
'[data-testid="filter-chip"]'
)
await expect(filterChip).toBeVisible()
await expect(searchBoxV2.results.first()).toBeVisible()
await expect
@@ -115,7 +117,7 @@ test.describe('Node search box V2 extended', { tag: '@node' }, () => {
await filterChip.getByTestId('chip-delete').click()
// Filter chip should be removed
await expect(filterChip).toBeHidden()
await expect(filterChip).not.toBeVisible()
await expect(searchBoxV2.results.first()).toBeVisible()
})
})

View File

@@ -1,413 +0,0 @@
import type { WebSocketRoute } from '@playwright/test'
import { mergeTests } from '@playwright/test'
import type { RawJobListItem } from '@/platform/remote/comfyui/jobs/jobTypes'
import { comfyPageFixture, comfyExpect as expect } from '../fixtures/ComfyPage'
import type { ComfyPage } from '../fixtures/ComfyPage'
import { webSocketFixture } from '../fixtures/ws'
import { ExecutionHelper } from '../fixtures/helpers/ExecutionHelper'
const test = mergeTests(comfyPageFixture, webSocketFixture)
// Node IDs from the default workflow (browser_tests/assets/default.json, 7 nodes)
const SAVE_IMAGE_NODE = '9'
const KSAMPLER_NODE = '3'
const ALL_NODE_IDS = ['4', '6', '7', '5', KSAMPLER_NODE, '8', SAVE_IMAGE_NODE]
/** Queue a prompt, intercept it, and send execution_start. */
async function startExecution(
comfyPage: ComfyPage,
ws: WebSocketRoute,
exec?: ExecutionHelper
) {
exec ??= new ExecutionHelper(comfyPage, ws)
const jobId = await exec.run()
// Allow storeJob() to complete before sending WS events
await comfyPage.nextFrame()
exec.executionStart(jobId)
return { exec, jobId }
}
function imageOutput(...filenames: string[]) {
return {
images: filenames.map((filename) => ({
filename,
subfolder: '',
type: 'output'
}))
}
}
test.describe('Output History', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.appMode.enterAppModeWithInputs([[KSAMPLER_NODE, 'seed']])
await expect(comfyPage.appMode.linearWidgets).toBeVisible()
await comfyPage.nextFrame()
})
test('Skeleton appears on execution start', async ({
comfyPage,
getWebSocket
}) => {
const ws = await getWebSocket()
await startExecution(comfyPage, ws)
await expect(
comfyPage.appMode.outputHistory.skeletons.first()
).toBeVisible()
})
test('Latent preview replaces skeleton', async ({
comfyPage,
getWebSocket
}) => {
const ws = await getWebSocket()
const { exec, jobId } = await startExecution(comfyPage, ws)
await expect(
comfyPage.appMode.outputHistory.skeletons.first()
).toBeVisible()
exec.latentPreview(jobId, SAVE_IMAGE_NODE)
await expect(
comfyPage.appMode.outputHistory.latentPreviews.first()
).toBeVisible()
await expect(comfyPage.appMode.outputHistory.skeletons).toHaveCount(0)
})
test('Image output replaces skeleton on executed', async ({
comfyPage,
getWebSocket
}) => {
const ws = await getWebSocket()
const { exec, jobId } = await startExecution(comfyPage, ws)
await expect(
comfyPage.appMode.outputHistory.inProgressItems.first()
).toBeVisible()
exec.executed(jobId, SAVE_IMAGE_NODE, imageOutput('test_output.png'))
await expect(
comfyPage.appMode.outputHistory.imageOutputs.first()
).toBeVisible()
await expect(comfyPage.appMode.outputHistory.skeletons).toHaveCount(0)
})
test('Multiple outputs from single execution', async ({
comfyPage,
getWebSocket
}) => {
const ws = await getWebSocket()
const { exec, jobId } = await startExecution(comfyPage, ws)
await expect(
comfyPage.appMode.outputHistory.inProgressItems.first()
).toBeVisible()
exec.executed(
jobId,
SAVE_IMAGE_NODE,
imageOutput('output_001.png', 'output_002.png', 'output_003.png')
)
await expect(comfyPage.appMode.outputHistory.imageOutputs).toHaveCount(3)
})
test('Video output renders video element', async ({
comfyPage,
getWebSocket
}) => {
const ws = await getWebSocket()
const { exec, jobId } = await startExecution(comfyPage, ws)
await expect(
comfyPage.appMode.outputHistory.inProgressItems.first()
).toBeVisible()
exec.executed(jobId, SAVE_IMAGE_NODE, {
gifs: [{ filename: 'output.mp4', subfolder: '', type: 'output' }]
})
await expect(
comfyPage.appMode.outputHistory.videoOutputs.first()
).toBeVisible()
})
test('Cancel button sends interrupt during execution', async ({
comfyPage,
getWebSocket
}) => {
const ws = await getWebSocket()
const { exec, jobId } = await startExecution(comfyPage, ws)
const job: RawJobListItem = {
id: jobId,
status: 'in_progress',
create_time: Date.now() / 1000,
priority: 0
}
await comfyPage.page.route(
/\/api\/jobs\?status=in_progress/,
async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
jobs: [job],
pagination: { offset: 0, limit: 200, total: 1, has_more: false }
})
})
},
{ times: 1 }
)
// Trigger queue refresh
exec.status(1)
await comfyPage.nextFrame()
await expect(comfyPage.appMode.cancelRunButton).toBeVisible()
await comfyPage.page.route('**/interrupt', (route) =>
route.fulfill({ status: 200 })
)
const interruptRequest = comfyPage.page.waitForRequest('**/interrupt')
await comfyPage.appMode.cancelRunButton.click()
await interruptRequest
})
test('Full execution lifecycle cleans up in-progress items', async ({
comfyPage,
getWebSocket
}) => {
const ws = await getWebSocket()
const { exec, jobId } = await startExecution(comfyPage, ws)
// Skeleton appears
await expect(
comfyPage.appMode.outputHistory.skeletons.first()
).toBeVisible()
// Latent preview replaces skeleton
exec.latentPreview(jobId, SAVE_IMAGE_NODE)
await expect(
comfyPage.appMode.outputHistory.latentPreviews.first()
).toBeVisible()
// Image output replaces latent
exec.executed(jobId, SAVE_IMAGE_NODE, imageOutput('lifecycle_out.png'))
await expect(
comfyPage.appMode.outputHistory.imageOutputs.first()
).toBeVisible()
// Job completes with history mock - in-progress items fully resolved
await exec.completeWithHistory(jobId, SAVE_IMAGE_NODE, 'lifecycle_out.png')
await expect(comfyPage.appMode.outputHistory.inProgressItems).toHaveCount(0)
// Output now appears as a history item
await expect(
comfyPage.appMode.outputHistory.historyItems.first()
).toBeVisible()
})
test('Auto-selection follows latest in-progress item', async ({
comfyPage,
getWebSocket
}) => {
const ws = await getWebSocket()
const { exec, jobId } = await startExecution(comfyPage, ws)
// Skeleton is auto-selected
await expect(
comfyPage.appMode.outputHistory.selectedInProgressItem
).toBeVisible()
// First image is auto-selected
exec.executed(jobId, SAVE_IMAGE_NODE, imageOutput('first.png'))
await expect(
comfyPage.appMode.outputHistory.selectedInProgressItem.getByTestId(
'linear-image-output'
)
).toHaveAttribute('src', /first\.png/)
// Second image arrives - selection auto-follows without user click
exec.executed(jobId, SAVE_IMAGE_NODE, imageOutput('second.png'))
await expect(
comfyPage.appMode.outputHistory.selectedInProgressItem.getByTestId(
'linear-image-output'
)
).toHaveAttribute('src', /second\.png/)
})
test('Clicking item breaks auto-follow during execution', async ({
comfyPage,
getWebSocket
}) => {
const ws = await getWebSocket()
const { exec, jobId } = await startExecution(comfyPage, ws)
// Send first image
exec.executed(jobId, SAVE_IMAGE_NODE, imageOutput('first.png'))
await expect(comfyPage.appMode.outputHistory.imageOutputs).toHaveCount(1)
// Click the first image to break auto-follow
await comfyPage.appMode.outputHistory.inProgressItems.first().click()
// Send second image - selection should NOT move to it
exec.executed(jobId, SAVE_IMAGE_NODE, imageOutput('second.png'))
await expect(comfyPage.appMode.outputHistory.imageOutputs).toHaveCount(2)
// The first item should still be selected (not auto-followed to second)
await expect(
comfyPage.appMode.outputHistory.selectedInProgressItem
).toHaveCount(1)
await expect(
comfyPage.appMode.outputHistory.selectedInProgressItem.getByTestId(
'linear-image-output'
)
).toHaveAttribute('src', /first\.png/)
})
test('Non-output node executed events are filtered', async ({
comfyPage,
getWebSocket
}) => {
const ws = await getWebSocket()
const { exec, jobId } = await startExecution(comfyPage, ws)
await expect(
comfyPage.appMode.outputHistory.inProgressItems.first()
).toBeVisible()
// KSampler is not an output node - should be filtered
exec.executed(jobId, KSAMPLER_NODE, imageOutput('ksampler_out.png'))
await comfyPage.nextFrame()
// KSampler output should not create image outputs
await expect(comfyPage.appMode.outputHistory.imageOutputs).toHaveCount(0)
// Now send from the actual output node (SaveImage)
exec.executed(jobId, SAVE_IMAGE_NODE, imageOutput('save_image_out.png'))
await expect(
comfyPage.appMode.outputHistory.imageOutputs.first()
).toBeVisible()
})
test('In-progress items are outside the scrollable area', async ({
comfyPage,
getWebSocket
}) => {
const ws = await getWebSocket()
// Complete one execution with 100 image outputs
const { exec, jobId } = await startExecution(comfyPage, ws)
exec.executed(
jobId,
SAVE_IMAGE_NODE,
imageOutput(
...Array.from(
{ length: 100 },
(_, i) => `image_${String(i).padStart(3, '0')}.png`
)
)
)
await exec.completeWithHistory(jobId, SAVE_IMAGE_NODE, 'image_000.png')
await expect(comfyPage.appMode.outputHistory.historyItems).toHaveCount(100)
// First history item is visible before scrolling
const firstItem = comfyPage.appMode.outputHistory.historyItems.first()
await expect(firstItem).toBeInViewport()
// Scroll the history feed all the way to the right
await comfyPage.appMode.outputHistory.outputs.evaluate((el) => {
el.scrollLeft = el.scrollWidth
})
// First history item is now off-screen
await expect(firstItem).not.toBeInViewport()
// Start a new execution to get an in-progress item
await startExecution(comfyPage, ws, exec)
// In-progress item is visible despite scrolling
await expect(
comfyPage.appMode.outputHistory.inProgressItems.first()
).toBeInViewport()
})
test('Execution error cleans up in-progress items', async ({
comfyPage,
getWebSocket
}) => {
const ws = await getWebSocket()
const { exec, jobId } = await startExecution(comfyPage, ws)
await expect(
comfyPage.appMode.outputHistory.inProgressItems.first()
).toBeVisible()
exec.executionError(jobId, KSAMPLER_NODE, 'Test error')
await expect(comfyPage.appMode.outputHistory.inProgressItems).toHaveCount(0)
})
test('Progress bars update for both node and overall progress', async ({
comfyPage,
getWebSocket
}) => {
const ws = await getWebSocket()
const { exec, jobId } = await startExecution(comfyPage, ws)
const {
inProgressItems,
headerOverallProgress,
headerNodeProgress,
itemOverallProgress,
itemNodeProgress
} = comfyPage.appMode.outputHistory
await expect(inProgressItems.first()).toBeVisible()
// Initially both bars are at 0%
await expect(headerOverallProgress).toHaveAttribute('style', /width:\s*0%/)
await expect(headerNodeProgress).toHaveAttribute('style', /width:\s*0%/)
// KSampler starts executing - node progress at 50%
exec.executing(jobId, KSAMPLER_NODE)
exec.progress(jobId, KSAMPLER_NODE, 5, 10)
await expect(headerNodeProgress).toHaveAttribute('style', /width:\s*50%/)
await expect(itemNodeProgress).toHaveAttribute('style', /width:\s*50%/)
// Overall still 0% - no nodes completed yet
await expect(headerOverallProgress).toHaveAttribute('style', /width:\s*0%/)
// KSampler finishes - overall advances (1 of 7 nodes)
exec.executed(jobId, KSAMPLER_NODE, {})
const oneNodePercent = Math.round((1 / ALL_NODE_IDS.length) * 100)
const pct = new RegExp(`width:\\s*${oneNodePercent}%`)
await expect(headerOverallProgress).toHaveAttribute('style', pct)
await expect(itemOverallProgress).toHaveAttribute('style', pct)
// Node progress reaches 100%
exec.progress(jobId, KSAMPLER_NODE, 10, 10)
await expect(headerNodeProgress).toHaveAttribute('style', /width:\s*100%/)
await expect(itemNodeProgress).toHaveAttribute('style', /width:\s*100%/)
// Complete remaining nodes - overall reaches 100%
const remainingNodes = ALL_NODE_IDS.filter((id) => id !== KSAMPLER_NODE)
for (const nodeId of remainingNodes) {
exec.executing(jobId, nodeId)
exec.executed(jobId, nodeId, {})
}
await expect(headerOverallProgress).toHaveAttribute(
'style',
/width:\s*100%/
)
await expect(itemOverallProgress).toHaveAttribute('style', /width:\s*100%/)
})
})

View File

@@ -202,7 +202,6 @@ test.describe('Performance', { tag: ['@perf'] }, () => {
'domNodes'
])
})
test('subgraph DOM widget clipping during node selection', async ({
comfyPage
}) => {

View File

@@ -13,5 +13,5 @@ export async function openErrorsTabViaSeeErrors(
await expect(errorOverlay).toBeVisible()
await errorOverlay.getByTestId(TestIds.dialogs.errorOverlaySeeErrors).click()
await expect(errorOverlay).toBeHidden()
await expect(errorOverlay).not.toBeVisible()
}

View File

@@ -76,7 +76,7 @@ export class PropertiesPanelHelper {
async close(): Promise<void> {
if (await this.root.isVisible()) {
await this.closeButton.click()
await expect(this.root).toBeHidden()
await expect(this.root).not.toBeVisible()
}
}

View File

@@ -33,7 +33,7 @@ test.describe('Errors tab - common', { tag: '@ui' }, () => {
await comfyPage.actionbar.propertiesButton.click()
const panel = new PropertiesPanelHelper(comfyPage.page)
await expect(panel.errorsTabIcon).toBeHidden()
await expect(panel.errorsTabIcon).not.toBeVisible()
})
})
@@ -55,7 +55,7 @@ test.describe('Errors tab - common', { tag: '@ui' }, () => {
await errorOverlay
.getByTestId(TestIds.dialogs.errorOverlaySeeErrors)
.click()
await expect(errorOverlay).toBeHidden()
await expect(errorOverlay).not.toBeVisible()
const runtimePanel = comfyPage.page.getByTestId(
TestIds.dialogs.runtimeErrorPanel

View File

@@ -26,7 +26,7 @@ test.describe('Errors tab - Execution errors', { tag: '@ui' }, () => {
await errorOverlay
.getByTestId(TestIds.dialogs.errorOverlaySeeErrors)
.click()
await expect(errorOverlay).toBeHidden()
await expect(errorOverlay).not.toBeVisible()
}
test('Should show Find on GitHub and Copy buttons in error card', async ({

View File

@@ -104,7 +104,6 @@ test.describe('Errors tab - Missing media', { tag: '@ui' }, () => {
const optionCount = await comfyPage.page.getByRole('option').count()
if (optionCount === 0) {
// oxlint-disable-next-line playwright/no-skipped-test -- no library options available in CI
test.skip()
return
}
@@ -126,13 +125,13 @@ test.describe('Errors tab - Missing media', { tag: '@ui' }, () => {
await uploadFileViaDropzone(comfyPage)
await expect(getStatusCard(comfyPage)).toBeVisible()
await expect(getDropzone(comfyPage)).toBeHidden()
await expect(getDropzone(comfyPage)).not.toBeVisible()
await comfyPage.page
.getByTestId(TestIds.dialogs.missingMediaCancelButton)
.click()
await expect(getStatusCard(comfyPage)).toBeHidden()
await expect(getStatusCard(comfyPage)).not.toBeVisible()
await expect(getDropzone(comfyPage)).toBeVisible()
})
})
@@ -147,7 +146,7 @@ test.describe('Errors tab - Missing media', { tag: '@ui' }, () => {
await expect(
comfyPage.page.getByTestId(TestIds.dialogs.missingMediaGroup)
).toBeHidden()
).not.toBeVisible()
})
})

View File

@@ -57,7 +57,7 @@ test.describe('Errors tab - Missing models', { tag: '@ui' }, () => {
const locateButton = comfyPage.page.getByTestId(
TestIds.dialogs.missingModelLocate
)
await expect(locateButton.first()).toBeHidden()
await expect(locateButton.first()).not.toBeVisible()
const expandButton = comfyPage.page.getByTestId(
TestIds.dialogs.missingModelExpand

View File

@@ -74,7 +74,7 @@ test.describe('Errors tab - Missing nodes', { tag: '@ui' }, () => {
.click()
await expect(
missingNodeCard.getByText('MISSING_NODE_TYPE_IN_SUBGRAPH')
).toBeHidden()
).not.toBeVisible()
})
test('Locate node button is visible for expanded pack nodes', async ({

View File

@@ -27,7 +27,7 @@ test.describe('Properties panel - Node selection', () => {
})
test('should not show Nodes tab for single node', async () => {
await expect(panel.getTab('Nodes')).toBeHidden()
await expect(panel.getTab('Nodes')).not.toBeVisible()
})
test('should display node widgets in Parameters tab', async () => {
@@ -65,7 +65,7 @@ test.describe('Properties panel - Node selection', () => {
'KSampler',
'CLIP Text Encode (Prompt)'
])
await expect(panel.getTab('Info')).toBeHidden()
await expect(panel.getTab('Info')).not.toBeVisible()
})
})

View File

@@ -42,8 +42,8 @@ test.describe('Properties panel - Node settings', () => {
await expect(nodeLocator.getByText('Bypassed')).toBeVisible()
await panel.getNodeStateButton('Normal').click()
await expect(nodeLocator.getByText('Bypassed')).toBeHidden()
await expect(nodeLocator.getByText('Muted')).toBeHidden()
await expect(nodeLocator.getByText('Bypassed')).not.toBeVisible()
await expect(nodeLocator.getByText('Muted')).not.toBeVisible()
})
})
@@ -114,7 +114,9 @@ test.describe('Properties panel - Node settings', () => {
await expect(nodeLocator.getByTestId('node-pin-indicator')).toBeVisible()
await panel.pinnedSwitch.click()
await expect(nodeLocator.getByTestId('node-pin-indicator')).toBeHidden()
await expect(
nodeLocator.getByTestId('node-pin-indicator')
).not.toBeVisible()
})
})
})

View File

@@ -11,7 +11,7 @@ test.describe('Properties panel - Open and close', () => {
})
test('should open via actionbar toggle button', async ({ comfyPage }) => {
await expect(panel.root).toBeHidden()
await expect(panel.root).not.toBeVisible()
await comfyPage.actionbar.propertiesButton.click()
await expect(panel.root).toBeVisible()
})
@@ -20,13 +20,13 @@ test.describe('Properties panel - Open and close', () => {
await comfyPage.actionbar.propertiesButton.click()
await expect(panel.root).toBeVisible()
await panel.closeButton.click()
await expect(panel.root).toBeHidden()
await expect(panel.root).not.toBeVisible()
})
test('should close via close button after opening', async ({ comfyPage }) => {
await comfyPage.actionbar.propertiesButton.click()
await expect(panel.root).toBeVisible()
await panel.close()
await expect(panel.root).toBeHidden()
await expect(panel.root).not.toBeVisible()
})
})

View File

@@ -34,7 +34,7 @@ test.describe('Properties panel - Title editing', () => {
'KSampler',
'CLIP Text Encode (Prompt)'
])
await expect(panel.titleEditIcon).toBeHidden()
await expect(panel.titleEditIcon).not.toBeVisible()
})
test('should not show pencil icon when nothing is selected', async ({
@@ -44,6 +44,6 @@ test.describe('Properties panel - Title editing', () => {
window.app!.canvas.deselectAll()
})
await expect(panel.panelTitle).toContainText('Workflow Overview')
await expect(panel.titleEditIcon).toBeHidden()
await expect(panel.titleEditIcon).not.toBeVisible()
})
})

View File

@@ -23,7 +23,7 @@ test.describe('Properties panel - Workflow Overview', () => {
})
test('should not show Info tab when nothing is selected', async () => {
await expect(panel.getTab('Info')).toBeHidden()
await expect(panel.getTab('Info')).not.toBeVisible()
})
test('should switch to Nodes tab and list all workflow nodes', async ({

View File

@@ -93,7 +93,7 @@ test.describe('Queue overlay', () => {
).toBeVisible()
await expect(
comfyPage.page.locator('[data-job-id="job-failed-1"]')
).toBeHidden()
).not.toBeVisible()
})
test('Toggling overlay again closes it', async ({ comfyPage }) => {
@@ -104,6 +104,8 @@ test.describe('Queue overlay', () => {
await toggle.click()
await expect(comfyPage.page.locator('[data-job-id]').first()).toBeHidden()
await expect(
comfyPage.page.locator('[data-job-id]').first()
).not.toBeVisible()
})
})

View File

@@ -72,8 +72,8 @@ test.describe('Release Notifications', () => {
).toBeVisible()
// Close help center by dismissable mask
await comfyPage.page.locator('.help-center-backdrop').click()
await expect(helpMenu).toBeHidden()
await comfyPage.page.click('.help-center-backdrop')
await expect(helpMenu).not.toBeVisible()
})
test('should not show release notifications when mocked (default behavior)', async ({
@@ -103,10 +103,10 @@ test.describe('Release Notifications', () => {
).toBeVisible()
// Should not show any popups or toasts
await expect(comfyPage.page.locator('.whats-new-popup')).toBeHidden()
await expect(comfyPage.page.locator('.whats-new-popup')).not.toBeVisible()
await expect(
comfyPage.page.locator('.release-notification-toast')
).toBeHidden()
).not.toBeVisible()
})
test('should handle release API errors gracefully', async ({ comfyPage }) => {
@@ -189,13 +189,13 @@ test.describe('Release Notifications', () => {
const whatsNewSection = comfyPage.page.getByTestId(
TestIds.dialogs.whatsNewSection
)
await expect(whatsNewSection).toBeHidden()
await expect(whatsNewSection).not.toBeVisible()
// Should not show any popups or toasts
await expect(comfyPage.page.locator('.whats-new-popup')).toBeHidden()
await expect(comfyPage.page.locator('.whats-new-popup')).not.toBeVisible()
await expect(
comfyPage.page.locator('.release-notification-toast')
).toBeHidden()
).not.toBeVisible()
})
test('should not make API calls when notifications are disabled', async ({
@@ -325,7 +325,7 @@ test.describe('Release Notifications', () => {
await expect(whatsNewSection).toBeVisible()
// Close help center
await comfyPage.page.locator('.help-center-backdrop').click()
await comfyPage.page.click('.help-center-backdrop')
// Disable notifications
await comfyPage.settings.setSetting(
@@ -337,7 +337,7 @@ test.describe('Release Notifications', () => {
await helpCenterButton.click()
// Verify "What's New?" section is now hidden
await expect(whatsNewSection).toBeHidden()
await expect(whatsNewSection).not.toBeVisible()
})
test('should handle edge case with empty releases and disabled notifications', async ({
@@ -381,6 +381,6 @@ test.describe('Release Notifications', () => {
const whatsNewSection = comfyPage.page.getByTestId(
TestIds.dialogs.whatsNewSection
)
await expect(whatsNewSection).toBeHidden()
await expect(whatsNewSection).not.toBeVisible()
})
})

View File

@@ -31,7 +31,7 @@ test.describe('MediaLightbox', { tag: ['@slow'] }, () => {
// Wait for any asset card to appear (may contain img or video)
const assetCard = comfyPage.page
.getByRole('button')
.locator('[role="button"]')
.filter({ has: comfyPage.page.locator('img, video') })
.first()
@@ -56,13 +56,13 @@ test.describe('MediaLightbox', { tag: ['@slow'] }, () => {
await runAndOpenGallery(comfyPage)
await comfyPage.page.keyboard.press('Escape')
await expect(comfyPage.mediaLightbox.root).toBeHidden()
await expect(comfyPage.mediaLightbox.root).not.toBeVisible()
})
test('closes gallery when clicking close button', async ({ comfyPage }) => {
await runAndOpenGallery(comfyPage)
await comfyPage.mediaLightbox.closeButton.click()
await expect(comfyPage.mediaLightbox.root).toBeHidden()
await expect(comfyPage.mediaLightbox.root).not.toBeVisible()
})
})

View File

@@ -135,7 +135,7 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => {
await comfyPage.page.mouse.move(10, 10)
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
await comfyPage.page.locator('.litemenu-entry:has-text("Pin")').click()
await comfyPage.page.click('.litemenu-entry:has-text("Pin")')
await comfyPage.contextMenu.waitForHidden()
await comfyPage.nextFrame()
@@ -153,7 +153,7 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => {
await expect(comfyPage.canvas).toHaveScreenshot(
'right-click-pinned-node.png'
)
await comfyPage.page.locator('.litemenu-entry:has-text("Unpin")').click()
await comfyPage.page.click('.litemenu-entry:has-text("Unpin")')
await comfyPage.contextMenu.waitForHidden()
await comfyPage.canvas.click({
position: DefaultGraphPositions.emptyLatentWidgetClick,
@@ -173,7 +173,7 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => {
})
await comfyPage.page.mouse.move(10, 10)
await comfyPage.nextFrame()
await comfyPage.page.locator('.litemenu-entry:has-text("Pin")').click()
await comfyPage.page.click('.litemenu-entry:has-text("Pin")')
await comfyPage.contextMenu.waitForHidden()
await comfyPage.canvas.click({
position: DefaultGraphPositions.emptyLatentWidgetClick,
@@ -181,7 +181,7 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => {
})
await comfyPage.page.mouse.move(10, 10)
await comfyPage.nextFrame()
await comfyPage.page.locator('.litemenu-entry:has-text("Unpin")').click()
await comfyPage.page.click('.litemenu-entry:has-text("Unpin")')
await comfyPage.contextMenu.waitForHidden()
await comfyPage.nextFrame()
@@ -203,7 +203,7 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => {
})
await comfyPage.page.mouse.move(10, 10)
await comfyPage.nextFrame()
await comfyPage.page.locator('.litemenu-entry:has-text("Pin")').click()
await comfyPage.page.click('.litemenu-entry:has-text("Pin")')
await comfyPage.page.keyboard.up('Control')
await comfyPage.contextMenu.waitForHidden()
await comfyPage.nextFrame()
@@ -214,7 +214,7 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => {
})
await comfyPage.page.mouse.move(10, 10)
await comfyPage.nextFrame()
await comfyPage.page.locator('.litemenu-entry:has-text("Unpin")').click()
await comfyPage.page.click('.litemenu-entry:has-text("Unpin")')
await comfyPage.contextMenu.waitForHidden()
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(

View File

@@ -9,7 +9,6 @@ const test = comfyPageFixture
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
const BLUE_COLOR = 'rgb(51, 51, 85)'
const RED_COLOR = 'rgb(85, 51, 51)'
@@ -31,7 +30,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
test('shows selection toolbox', async ({ comfyPage }) => {
// By default, selection toolbox should be enabled
await expect(comfyPage.selectionToolbox).toBeHidden()
await expect(comfyPage.selectionToolbox).not.toBeVisible()
// Select multiple nodes
await comfyPage.nodeOps.selectNodes([
@@ -87,7 +86,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
await comfyPage.page.mouse.down()
await comfyPage.page.mouse.move(nodePos.x + 200, nodePos.y + 200)
await comfyPage.nextFrame()
await expect(comfyPage.selectionToolbox).toBeHidden()
await expect(comfyPage.selectionToolbox).not.toBeVisible()
})
test('shows border only with multiple selections', async ({ comfyPage }) => {
@@ -128,7 +127,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
await comfyPage.workflow.loadWorkflow('groups/single_group')
// Select group + node should show bypass button
await comfyPage.canvas.focus()
await comfyPage.page.focus('canvas')
await comfyPage.page.keyboard.press('Control+A')
await expect(
comfyPage.page.locator(
@@ -142,7 +141,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
comfyPage.page.locator(
'.selection-toolbox *[data-testid="bypass-button"]'
)
).toBeHidden()
).not.toBeVisible()
})
test.describe('Color Picker', () => {
@@ -170,7 +169,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
await blueColorOption.click()
// Dropdown should close after selection
await expect(colorPickerGroup).toBeHidden()
await expect(colorPickerGroup).not.toBeVisible()
// Node should have the selected color class/style
// Note: Exact verification method depends on how color is applied to nodes

View File

@@ -204,9 +204,7 @@ test.describe('Selection Toolbox - Button Actions', { tag: '@ui' }, () => {
name: /Frame Nodes/i
})
await expect(frameButton).toBeVisible()
await comfyPage.page
.getByRole('button', { name: /Frame Nodes/i })
.click({ force: true })
await frameButton.click({ force: true })
await comfyPage.nextFrame()
await expect
@@ -225,7 +223,7 @@ test.describe('Selection Toolbox - Button Actions', { tag: '@ui' }, () => {
const frameButton = comfyPage.page.getByRole('button', {
name: /Frame Nodes/i
})
await expect(frameButton).toBeHidden()
await expect(frameButton).not.toBeVisible()
})
test('execute button visible when output node selected', async ({
@@ -255,6 +253,6 @@ test.describe('Selection Toolbox - Button Actions', { tag: '@ui' }, () => {
const executeButton = comfyPage.page.getByRole('button', {
name: /Execute to selected output nodes/i
})
await expect(executeButton).toBeHidden()
await expect(executeButton).not.toBeVisible()
})
})

View File

@@ -47,10 +47,12 @@ test.describe(
await expect(comfyPage.page.locator('.selection-toolbox')).toBeVisible()
const moreOptionsBtn = comfyPage.page.getByTestId('more-options-button')
const moreOptionsBtn = comfyPage.page.locator(
'[data-testid="more-options-button"]'
)
await expect(moreOptionsBtn).toBeVisible()
await moreOptionsBtn.click()
await comfyPage.page.click('[data-testid="more-options-button"]')
await comfyPage.nextFrame()
@@ -111,7 +113,7 @@ test.describe(
await openMoreOptions(comfyPage)
await comfyPage.page.getByText('Color', { exact: true }).click()
const blueSwatch = comfyPage.page.getByTitle('Blue')
const blueSwatch = comfyPage.page.locator('[title="Blue"]')
await expect(blueSwatch.first()).toBeVisible()
await blueSwatch.first().click()
await comfyPage.nextFrame()
@@ -160,7 +162,7 @@ test.describe(
await comfyPage.nextFrame()
await expect(
comfyPage.page.getByText('Rename', { exact: true })
).toBeHidden()
).not.toBeVisible()
})
test('closes More Options menu when clicking the button again (toggle)', async ({
@@ -189,7 +191,7 @@ test.describe(
await expect(
comfyPage.page.getByText('Rename', { exact: true })
).toBeHidden()
).not.toBeVisible()
})
}
)

View File

@@ -192,7 +192,6 @@ test.describe('Assets sidebar - grid view display', () => {
// Imported tab should show the mocked files
await expect.poll(() => tab.assetCards.count()).toBeGreaterThanOrEqual(1)
})
test('Displays svg outputs', async ({ comfyPage }) => {
await comfyPage.assets.mockOutputHistory([
createMockJob({
@@ -746,7 +745,7 @@ test.describe('Assets sidebar - delete confirmation', () => {
await comfyPage.confirmDialog.delete.click()
await expect(dialog).toBeHidden()
await expect(dialog).not.toBeVisible()
await expect(tab.assetCards).toHaveCount(initialCount - 1)
const successToast = comfyPage.page.locator('.p-toast-message-success')
@@ -768,7 +767,7 @@ test.describe('Assets sidebar - delete confirmation', () => {
await comfyPage.confirmDialog.reject.click()
await expect(dialog).toBeHidden()
await expect(dialog).not.toBeVisible()
await expect(tab.assetCards).toHaveCount(initialCount)
})
})

View File

@@ -118,7 +118,7 @@ test.describe('Model library sidebar - search', () => {
await expect(tab.getLeafByLabel('dreamshaper_8')).toBeVisible()
// Other models should not be visible
await expect(tab.getLeafByLabel('sd_xl_base_1.0')).toBeHidden()
await expect(tab.getLeafByLabel('sd_xl_base_1.0')).not.toBeVisible()
})
test('Clearing search restores folder view', async ({ comfyPage }) => {

View File

@@ -58,7 +58,7 @@ test.describe('Node library sidebar', () => {
// Hover over a node to display the preview
const nodeSelector = tab.nodeSelector('KSampler (Advanced)')
await comfyPage.page.locator(nodeSelector).hover()
await comfyPage.page.hover(nodeSelector)
// Verify the preview is displayed
await expect(tab.nodePreview).toBeVisible()
@@ -78,9 +78,9 @@ test.describe('Node library sidebar', () => {
y: canvasBoundingBox.y + canvasBoundingBox.height / 2
}
await comfyPage.page
.locator(nodeSelector)
.dragTo(comfyPage.page.locator(canvasSelector), { targetPosition })
await comfyPage.page.dragAndDrop(nodeSelector, canvasSelector, {
targetPosition
})
await comfyPage.nextFrame()
// Verify the node is added to the canvas
@@ -102,9 +102,7 @@ test.describe('Node library sidebar', () => {
await expect(tab.getNode('KSampler (Advanced)')).toHaveCount(2)
// Hover on the bookmark node to display the preview
await comfyPage.page
.locator('.node-lib-bookmark-tree-explorer .tree-leaf')
.hover()
await comfyPage.page.hover('.node-lib-bookmark-tree-explorer .tree-leaf')
await expect(tab.nodePreview).toBeVisible()
})
@@ -222,7 +220,6 @@ test.describe('Node library sidebar', () => {
.click()
await expectBookmarks(comfyPage, [])
})
test('Can customize icon', async ({ comfyPage }) => {
await comfyPage.settings.setSetting(bookmarksSettingId, ['foo/'])
const tab = comfyPage.menu.nodeLibraryTab
@@ -250,7 +247,6 @@ test.describe('Node library sidebar', () => {
}
})
})
// If color is left as default, it should not be saved
test('Can customize icon (default field)', async ({ comfyPage }) => {
await comfyPage.settings.setSetting(bookmarksSettingId, ['foo/'])

View File

@@ -42,11 +42,11 @@ test.describe('Node library sidebar V2', () => {
test('Search filters nodes in All tab', async ({ comfyPage }) => {
const tab = comfyPage.menu.nodeLibraryTabV2
await expect(tab.getNode('KSampler (Advanced)')).toBeHidden()
await expect(tab.getNode('KSampler (Advanced)')).not.toBeVisible()
await tab.searchInput.fill('KSampler')
await expect(tab.getNode('KSampler (Advanced)')).toBeVisible()
await expect(tab.getNode('CLIPLoader')).toBeHidden()
await expect(tab.getNode('CLIPLoader')).not.toBeVisible()
})
test('Drag node to canvas adds it', async ({ comfyPage }) => {

View File

@@ -27,7 +27,9 @@ test.describe('Workflow sidebar - search', () => {
await searchInput.fill('alpha')
await expect(findWorkflow(comfyPage.page, 'alpha-workflow')).toBeVisible()
await expect(findWorkflow(comfyPage.page, 'beta-workflow')).toBeHidden()
await expect(
findWorkflow(comfyPage.page, 'beta-workflow')
).not.toBeVisible()
})
test('Clearing search restores all workflows', async ({ comfyPage }) => {
@@ -36,7 +38,9 @@ test.describe('Workflow sidebar - search', () => {
const searchInput = comfyPage.page.getByPlaceholder('Search Workflow...')
await searchInput.fill('alpha')
await expect(findWorkflow(comfyPage.page, 'beta-workflow')).toBeHidden()
await expect(
findWorkflow(comfyPage.page, 'beta-workflow')
).not.toBeVisible()
await searchInput.fill('')
@@ -51,7 +55,11 @@ test.describe('Workflow sidebar - search', () => {
const searchInput = comfyPage.page.getByPlaceholder('Search Workflow...')
await searchInput.fill('nonexistent_xyz')
await expect(findWorkflow(comfyPage.page, 'alpha-workflow')).toBeHidden()
await expect(findWorkflow(comfyPage.page, 'beta-workflow')).toBeHidden()
await expect(
findWorkflow(comfyPage.page, 'alpha-workflow')
).not.toBeVisible()
await expect(
findWorkflow(comfyPage.page, 'beta-workflow')
).not.toBeVisible()
})
})

View File

@@ -265,7 +265,7 @@ test.describe('Workflows sidebar', () => {
// Dismiss the error overlay
await errorOverlay.getByTestId(TestIds.dialogs.errorOverlayDismiss).click()
await expect(errorOverlay).toBeHidden()
await expect(errorOverlay).not.toBeVisible()
// Load blank workflow
await comfyPage.menu.workflowsTab.open()
@@ -316,7 +316,7 @@ test.describe('Workflows sidebar', () => {
await workflowsTab.getOpenedItem(filename).click({ button: 'right' })
await comfyPage.nextFrame()
await comfyPage.contextMenu.clickMenuItem('Delete')
await expect(workflowsTab.getOpenedItem(filename)).toBeHidden()
await expect(workflowsTab.getOpenedItem(filename)).not.toBeVisible()
await expect
.poll(() => workflowsTab.getOpenedWorkflowNames())
.toEqual(['*Unsaved Workflow'])
@@ -337,7 +337,7 @@ test.describe('Workflows sidebar', () => {
await comfyPage.confirmDialog.click('delete')
await expect(workflowsTab.getOpenedItem(filename)).toBeHidden()
await expect(workflowsTab.getOpenedItem(filename)).not.toBeVisible()
await expect
.poll(() => workflowsTab.getOpenedWorkflowNames())
.toEqual(['*Unsaved Workflow'])

View File

@@ -52,11 +52,11 @@ test.describe(
await comfyPage.workflow.waitForDraftPersisted()
// Reload the page (draft auto-loads with hash preserved)
await comfyPage.page.reload({ waitUntil: 'domcontentloaded' })
await comfyPage.page.reload({ waitUntil: 'networkidle' })
await comfyPage.page.waitForFunction(
() => window.app && window.app.extensionManager
)
await comfyPage.page.locator('.p-blockui-mask').waitFor({
await comfyPage.page.waitForSelector('.p-blockui-mask', {
state: 'hidden'
})
await comfyPage.nextFrame()

View File

@@ -131,7 +131,7 @@ test.describe(
const enterButton = subgraphVueNode.getByTestId('subgraph-enter-button')
await expect(enterButton).toBeVisible()
const nodeBody = subgraphVueNode.getByTestId('node-body-11')
const nodeBody = subgraphVueNode.locator('[data-testid="node-body-11"]')
await expect(nodeBody).toBeVisible()
const widgets = nodeBody.locator('.lg-node-widgets > div')
@@ -400,7 +400,7 @@ test.describe(
await comfyPage.command.executeCommand('Comfy.QueuePrompt')
const nodeBody = subgraphVueNode.getByTestId('node-body-5')
const nodeBody = subgraphVueNode.locator('[data-testid="node-body-5"]')
await expect(nodeBody).toBeVisible()
await expect(
nodeBody.locator('.lg-node-widgets > div').first()

View File

@@ -132,9 +132,7 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
await comfyPage.nextFrame()
await expect(comfyPage.page.locator(SELECTORS.promptDialog)).toBeVisible()
await comfyPage.page
.locator(SELECTORS.promptDialog)
.fill(RENAMED_INPUT_NAME)
await comfyPage.page.fill(SELECTORS.promptDialog, RENAMED_INPUT_NAME)
await comfyPage.page.keyboard.press('Enter')
await comfyPage.canvas.click({ position: { x: 100, y: 100 } })
@@ -155,12 +153,10 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
await comfyPage.subgraph.doubleClickInputSlot(initialInputLabel!)
await comfyPage.page.locator(SELECTORS.promptDialog).waitFor({
await comfyPage.page.waitForSelector(SELECTORS.promptDialog, {
state: 'visible'
})
await comfyPage.page
.locator(SELECTORS.promptDialog)
.fill(RENAMED_INPUT_NAME)
await comfyPage.page.fill(SELECTORS.promptDialog, RENAMED_INPUT_NAME)
await comfyPage.page.keyboard.press('Enter')
// Force re-render
@@ -182,13 +178,11 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
await comfyPage.subgraph.doubleClickOutputSlot(initialOutputLabel!)
await comfyPage.page.locator(SELECTORS.promptDialog).waitFor({
await comfyPage.page.waitForSelector(SELECTORS.promptDialog, {
state: 'visible'
})
const renamedOutputName = 'renamed_output'
await comfyPage.page
.locator(SELECTORS.promptDialog)
.fill(renamedOutputName)
await comfyPage.page.fill(SELECTORS.promptDialog, renamedOutputName)
await comfyPage.page.keyboard.press('Enter')
// Force re-render
@@ -215,13 +209,11 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
await comfyPage.contextMenu.clickLitegraphMenuItem('Rename Slot')
await comfyPage.nextFrame()
await comfyPage.page.locator(SELECTORS.promptDialog).waitFor({
await comfyPage.page.waitForSelector(SELECTORS.promptDialog, {
state: 'visible'
})
const rightClickRenamedName = 'right_click_renamed'
await comfyPage.page
.locator(SELECTORS.promptDialog)
.fill(rightClickRenamedName)
await comfyPage.page.fill(SELECTORS.promptDialog, rightClickRenamedName)
await comfyPage.page.keyboard.press('Enter')
// Force re-render
@@ -278,9 +270,7 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
await expect(comfyPage.page.locator(SELECTORS.promptDialog)).toBeVisible()
const labelClickRenamedName = 'label_click_renamed'
await comfyPage.page
.locator(SELECTORS.promptDialog)
.fill(labelClickRenamedName)
await comfyPage.page.fill(SELECTORS.promptDialog, labelClickRenamedName)
await comfyPage.page.keyboard.press('Enter')
await comfyPage.canvas.click({ position: { x: 100, y: 100 } })
@@ -313,10 +303,8 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
await comfyPage.nextFrame()
await expect(comfyPage.page.locator(SELECTORS.promptDialog)).toBeVisible()
await comfyPage.page.locator(SELECTORS.promptDialog).fill('')
await comfyPage.page
.locator(SELECTORS.promptDialog)
.fill(RENAMED_SLOT_NAME)
await comfyPage.page.fill(SELECTORS.promptDialog, '')
await comfyPage.page.fill(SELECTORS.promptDialog, RENAMED_SLOT_NAME)
await comfyPage.page.keyboard.press('Enter')
await expect(comfyPage.page.locator(SELECTORS.promptDialog)).toBeHidden()
@@ -344,10 +332,8 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
RENAMED_SLOT_NAME
)
await comfyPage.page.locator(SELECTORS.promptDialog).fill('')
await comfyPage.page
.locator(SELECTORS.promptDialog)
.fill(SECOND_RENAMED_NAME)
await comfyPage.page.fill(SELECTORS.promptDialog, '')
await comfyPage.page.fill(SELECTORS.promptDialog, SECOND_RENAMED_NAME)
await comfyPage.page.keyboard.press('Enter')
await expect(comfyPage.page.locator(SELECTORS.promptDialog)).toBeHidden()
@@ -380,10 +366,8 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
await comfyPage.nextFrame()
await expect(comfyPage.page.locator(SELECTORS.promptDialog)).toBeVisible()
await comfyPage.page.locator(SELECTORS.promptDialog).fill('')
await comfyPage.page
.locator(SELECTORS.promptDialog)
.fill(RENAMED_SLOT_NAME)
await comfyPage.page.fill(SELECTORS.promptDialog, '')
await comfyPage.page.fill(SELECTORS.promptDialog, RENAMED_SLOT_NAME)
await comfyPage.page.keyboard.press('Enter')
await expect(comfyPage.page.locator(SELECTORS.promptDialog)).toBeHidden()
@@ -450,8 +434,8 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
await comfyPage.nextFrame()
await expect(comfyPage.page.locator(SELECTORS.promptDialog)).toBeVisible()
await comfyPage.page.locator(SELECTORS.promptDialog).fill('')
await comfyPage.page.locator(SELECTORS.promptDialog).fill(RENAMED_LABEL)
await comfyPage.page.fill(SELECTORS.promptDialog, '')
await comfyPage.page.fill(SELECTORS.promptDialog, RENAMED_LABEL)
await comfyPage.page.keyboard.press('Enter')
await expect(comfyPage.page.locator(SELECTORS.promptDialog)).toBeHidden()
@@ -549,10 +533,8 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
await comfyPage.nextFrame()
await expect(comfyPage.page.locator(SELECTORS.promptDialog)).toBeVisible()
await comfyPage.page.locator(SELECTORS.promptDialog).fill('')
await comfyPage.page
.locator(SELECTORS.promptDialog)
.fill('my_custom_prompt')
await comfyPage.page.fill(SELECTORS.promptDialog, '')
await comfyPage.page.fill(SELECTORS.promptDialog, 'my_custom_prompt')
await comfyPage.page.keyboard.press('Enter')
await expect(comfyPage.page.locator(SELECTORS.promptDialog)).toBeHidden()

View File

@@ -40,7 +40,6 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => {
// page.route(), and change checkTemplateFileExists to use browser-context
// fetch (page.request.head bypasses Playwright routing).
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/3992
// oxlint-disable-next-line playwright/no-skipped-test -- https://github.com/Comfy-Org/ComfyUI_frontend/issues/3992
test.skip('should have all required thumbnail media for each template', async ({
comfyPage
}) => {
@@ -186,8 +185,8 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => {
await comfyPage.command.executeCommand('Comfy.BrowseTemplates')
await comfyPage.templates.content.waitFor({ state: 'visible' })
const templateGrid = comfyPage.page.getByTestId(
'template-workflows-content'
const templateGrid = comfyPage.page.locator(
'[data-testid="template-workflows-content"]'
)
const nav = comfyPage.page.locator('header', { hasText: 'Templates' })
@@ -303,18 +302,20 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => {
// Wait for cards to load
await expect(
comfyPage.page.getByTestId('template-workflow-short-description')
comfyPage.page.locator(
'[data-testid="template-workflow-short-description"]'
)
).toBeVisible()
// Verify all three cards with different descriptions are visible
const shortDescCard = comfyPage.page.getByTestId(
'template-workflow-short-description'
const shortDescCard = comfyPage.page.locator(
'[data-testid="template-workflow-short-description"]'
)
const mediumDescCard = comfyPage.page.getByTestId(
'template-workflow-medium-description'
const mediumDescCard = comfyPage.page.locator(
'[data-testid="template-workflow-medium-description"]'
)
const longDescCard = comfyPage.page.getByTestId(
'template-workflow-long-description'
const longDescCard = comfyPage.page.locator(
'[data-testid="template-workflow-long-description"]'
)
await expect(shortDescCard).toBeVisible()
@@ -332,8 +333,8 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => {
await expect(longDesc).toContainText('much longer description')
// Verify grid layout maintains consistency
const templateGrid = comfyPage.page.getByTestId(
'template-workflows-content'
const templateGrid = comfyPage.page.locator(
'[data-testid="template-workflows-content"]'
)
await expect(templateGrid).toBeVisible()
await expect(templateGrid).toHaveScreenshot(

View File

@@ -64,9 +64,9 @@ test.describe('Workflow tabs', () => {
await topbar.getTab(0).click({ button: 'right' })
// Reka UI ContextMenuContent gets data-state="open" when active
const contextMenu = comfyPage.page
.getByRole('menu')
.and(comfyPage.page.locator('[data-state="open"]'))
const contextMenu = comfyPage.page.locator(
'[role="menu"][data-state="open"]'
)
await expect(contextMenu).toBeVisible()
await expect(
@@ -86,9 +86,9 @@ test.describe('Workflow tabs', () => {
await expect.poll(() => topbar.getTabNames()).toHaveLength(2)
await topbar.getTab(1).click({ button: 'right' })
const contextMenu = comfyPage.page
.getByRole('menu')
.and(comfyPage.page.locator('[data-state="open"]'))
const contextMenu = comfyPage.page.locator(
'[role="menu"][data-state="open"]'
)
await expect(contextMenu).toBeVisible()
await contextMenu

View File

@@ -112,7 +112,7 @@ test.describe('Settings Search functionality', { tag: '@settings' }, () => {
await dialog.open()
await comfyPage.page.keyboard.press('Escape')
await expect(dialog.root).toBeHidden()
await expect(dialog.root).not.toBeVisible()
})
test('search box has proper debouncing behavior', async ({ comfyPage }) => {

View File

@@ -83,7 +83,7 @@ test.describe('Version Mismatch Warnings', { tag: '@slow' }, () => {
// Expect no warning toast to be shown
await expect(
comfyPage.page.getByText('Version Compatibility Warning')
).toBeHidden()
).not.toBeVisible()
})
test('should persist dismissed state across sessions', async ({
@@ -121,6 +121,6 @@ test.describe('Version Mismatch Warnings', { tag: '@slow' }, () => {
// The same warning from same versions should not be shown to the user again
await expect(
comfyPage.page.getByText('Version Compatibility Warning')
).toBeHidden()
).not.toBeVisible()
})
})

View File

@@ -143,7 +143,7 @@ test.describe('Vue Node Context Menu', () => {
await openContextMenu(comfyPage, nodeTitle)
await clickExactMenuItem(comfyPage, 'Unpin')
await expect(fixture.pinIndicator).toBeHidden()
await expect(fixture.pinIndicator).not.toBeVisible()
await expect.poll(() => nodeRef.isPinned()).toBe(false)
})
@@ -178,7 +178,7 @@ test.describe('Vue Node Context Menu', () => {
await openContextMenu(comfyPage, 'KSampler')
await clickExactMenuItem(comfyPage, 'Minimize Node')
await expect(fixture.body).toBeHidden()
await expect(fixture.body).not.toBeVisible()
await openContextMenu(comfyPage, 'KSampler')
await clickExactMenuItem(comfyPage, 'Expand Node')
@@ -194,7 +194,9 @@ test.describe('Vue Node Context Menu', () => {
const subgraphNode = comfyPage.vueNodes.getNodeByTitle('New Subgraph')
await expect(subgraphNode).toBeVisible()
await expect(comfyPage.vueNodes.getNodeByTitle('KSampler')).toBeHidden()
await expect(
comfyPage.vueNodes.getNodeByTitle('KSampler')
).not.toBeVisible()
})
})
@@ -307,7 +309,9 @@ test.describe('Vue Node Context Menu', () => {
const subgraphNode = comfyPage.vueNodes.getNodeByTitle('New Subgraph')
await expect(subgraphNode).toBeVisible()
await expect(comfyPage.vueNodes.getNodeByTitle('KSampler')).toBeHidden()
await expect(
comfyPage.vueNodes.getNodeByTitle('KSampler')
).not.toBeVisible()
// Unpack the subgraph
await openContextMenu(comfyPage, 'New Subgraph')
@@ -316,7 +320,7 @@ test.describe('Vue Node Context Menu', () => {
await expect(comfyPage.vueNodes.getNodeByTitle('KSampler')).toBeVisible()
await expect(
comfyPage.vueNodes.getNodeByTitle('New Subgraph')
).toBeHidden()
).not.toBeVisible()
})
test('should open properties panel via Edit Subgraph Widgets', async ({
@@ -429,7 +433,7 @@ test.describe('Vue Node Context Menu', () => {
for (const title of nodeTitles) {
const fixture = await comfyPage.vueNodes.getFixtureByTitle(title)
await expect(fixture.pinIndicator).toBeHidden()
await expect(fixture.pinIndicator).not.toBeVisible()
}
})
@@ -470,8 +474,8 @@ test.describe('Vue Node Context Menu', () => {
await openMultiNodeContextMenu(comfyPage, nodeTitles)
await clickExactMenuItem(comfyPage, 'Minimize Node')
await expect(fixture1.body).toBeHidden()
await expect(fixture2.body).toBeHidden()
await expect(fixture1.body).not.toBeVisible()
await expect(fixture2.body).not.toBeVisible()
await openMultiNodeContextMenu(comfyPage, nodeTitles)
await clickExactMenuItem(comfyPage, 'Expand Node')

View File

@@ -11,21 +11,16 @@ test.describe('Vue Node Moving', () => {
await comfyPage.vueNodes.waitForNodes()
})
const getHeaderPos = async (
comfyPage: ComfyPage,
title: string
): Promise<{ x: number; y: number; width: number; height: number }> => {
const box = await comfyPage.vueNodes
.getNodeByTitle(title)
.getByTestId('node-title')
.first()
const getLoadCheckpointHeaderPos = async (comfyPage: ComfyPage) => {
const loadCheckpointHeaderPos = await comfyPage.page
.getByText('Load Checkpoint')
.boundingBox()
if (!box) throw new Error(`${title} header not found`)
return box
}
const getLoadCheckpointHeaderPos = async (comfyPage: ComfyPage) =>
getHeaderPos(comfyPage, 'Load Checkpoint')
if (!loadCheckpointHeaderPos)
throw new Error('Load Checkpoint header not found')
return loadCheckpointHeaderPos
}
const expectPosChanged = async (pos1: Position, pos2: Position) => {
const diffX = Math.abs(pos2.x - pos1.x)
@@ -34,16 +29,6 @@ test.describe('Vue Node Moving', () => {
expect(diffY).toBeGreaterThan(0)
}
const deltaBetween = (before: Position, after: Position) => ({
x: after.x - before.x,
y: after.y - before.y
})
const expectSameDelta = (a: Position, b: Position, tol = 2) => {
expect(Math.abs(a.x - b.x)).toBeLessThanOrEqual(tol)
expect(Math.abs(a.y - b.y)).toBeLessThanOrEqual(tol)
}
test('should allow moving nodes by dragging', async ({ comfyPage }) => {
const loadCheckpointHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
await comfyPage.canvasOps.dragAndDrop(loadCheckpointHeaderPos, {
@@ -95,73 +80,6 @@ test.describe('Vue Node Moving', () => {
await expectPosChanged(headerPos, afterPos)
})
test('should move all selected nodes together when dragging one with Meta held', async ({
comfyPage
}) => {
const checkpointBefore = await getHeaderPos(comfyPage, 'Load Checkpoint')
const ksamplerBefore = await getHeaderPos(comfyPage, 'KSampler')
const latentBefore = await getHeaderPos(comfyPage, 'Empty Latent Image')
const dx = 120
const dy = 80
const clickNodeTitleWithMeta = async (title: string) => {
await comfyPage.vueNodes
.getNodeByTitle(title)
.getByTestId('node-title')
.first()
.click({ modifiers: ['Meta'] })
}
await comfyPage.page.keyboard.down('Meta')
try {
await clickNodeTitleWithMeta('Load Checkpoint')
await clickNodeTitleWithMeta('KSampler')
await clickNodeTitleWithMeta('Empty Latent Image')
await expect(comfyPage.vueNodes.selectedNodes).toHaveCount(3)
// Re-fetch drag source after clicks in case the header reflowed.
const dragSrc = await getHeaderPos(comfyPage, 'Load Checkpoint')
const centerX = dragSrc.x + dragSrc.width / 2
const centerY = dragSrc.y + dragSrc.height / 2
await comfyPage.page.mouse.move(centerX, centerY)
await comfyPage.page.mouse.down()
await comfyPage.nextFrame()
await comfyPage.page.mouse.move(centerX + dx, centerY + dy, {
steps: 20
})
await comfyPage.page.mouse.up()
await comfyPage.nextFrame()
} finally {
await comfyPage.page.keyboard.up('Meta')
await comfyPage.nextFrame()
}
await expect(comfyPage.vueNodes.selectedNodes).toHaveCount(3)
const checkpointAfter = await getHeaderPos(comfyPage, 'Load Checkpoint')
const ksamplerAfter = await getHeaderPos(comfyPage, 'KSampler')
const latentAfter = await getHeaderPos(comfyPage, 'Empty Latent Image')
// All three nodes should have moved together by the same delta.
// We don't assert the exact screen delta equals the dragged pixel delta,
// because canvas scaling and snap-to-grid can introduce offsets.
const checkpointDelta = deltaBetween(checkpointBefore, checkpointAfter)
const ksamplerDelta = deltaBetween(ksamplerBefore, ksamplerAfter)
const latentDelta = deltaBetween(latentBefore, latentAfter)
// Confirm an actual drag happened (not zero movement).
expect(Math.abs(checkpointDelta.x)).toBeGreaterThan(10)
expect(Math.abs(checkpointDelta.y)).toBeGreaterThan(10)
// Confirm all selected nodes moved by the same delta.
expectSameDelta(checkpointDelta, ksamplerDelta)
expectSameDelta(checkpointDelta, latentDelta)
await comfyPage.canvasOps.moveMouseToEmptyArea()
})
test(
'@mobile should allow moving nodes by dragging on touch devices',
{ tag: '@screenshot' },

View File

@@ -50,6 +50,6 @@ test.describe('Vue Nodes Renaming', () => {
const editingTitleInput = comfyPage.page.getByTestId(
TestIds.node.titleInput
)
await expect(editingTitleInput).toBeHidden()
await expect(editingTitleInput).not.toBeVisible()
})
})

View File

@@ -30,7 +30,7 @@ test.describe('Vue Node Collapse', () => {
await comfyPage.nextFrame()
// Verify node content is hidden
await expect(body).toBeHidden()
await expect(body).not.toBeVisible()
await expect
.poll(async () => (await vueNode.boundingBox())?.height)
.toBeLessThan(expandedBoundingBox.height)

View File

@@ -24,7 +24,7 @@ test.describe('Vue Node Pin', () => {
await expect(pinIndicator).toBeVisible()
await comfyPage.page.keyboard.press(PIN_HOTKEY)
await expect(pinIndicator).toBeHidden()
await expect(pinIndicator).not.toBeVisible()
})
test('should allow toggling pin on multiple selected nodes with hotkey', async ({
@@ -43,8 +43,8 @@ test.describe('Vue Node Pin', () => {
await expect(pinIndicator2).toBeVisible()
await comfyPage.page.keyboard.press(PIN_HOTKEY)
await expect(pinIndicator1).toBeHidden()
await expect(pinIndicator2).toBeHidden()
await expect(pinIndicator1).not.toBeVisible()
await expect(pinIndicator2).not.toBeVisible()
})
test('should not allow dragging pinned nodes', async ({ comfyPage }) => {

View File

@@ -43,8 +43,12 @@ test.describe('Advanced Widget Visibility', () => {
await expect(node.getByLabel('height', { exact: true })).toBeVisible()
// Advanced widgets should not be rendered
await expect(node.getByLabel('max_shift', { exact: true })).toBeHidden()
await expect(node.getByLabel('base_shift', { exact: true })).toBeHidden()
await expect(
node.getByLabel('max_shift', { exact: true })
).not.toBeVisible()
await expect(
node.getByLabel('base_shift', { exact: true })
).not.toBeVisible()
// "Show advanced inputs" button should be present
await expect(node.getByText('Show advanced inputs')).toBeVisible()
@@ -93,6 +97,6 @@ test.describe('Advanced Widget Visibility', () => {
await expect(node.getByLabel('base_shift', { exact: true })).toBeVisible()
// The toggle button should not be shown when global setting is active
await expect(node.getByText('Show advanced inputs')).toBeHidden()
await expect(node.getByText('Show advanced inputs')).not.toBeVisible()
})
})

View File

@@ -17,7 +17,7 @@ test.describe('Vue Upload Widgets', () => {
await expect(
comfyPage.page.getByText('choose file to upload', { exact: true })
).toBeHidden()
).not.toBeVisible()
await expect
.poll(() =>

View File

@@ -46,14 +46,13 @@ test.describe('Vue Multiline String Widget', () => {
await expect(textarea).toHaveValue('Keep me around')
})
test('should use native context menu when focused', async ({ comfyPage }) => {
const textarea = getFirstMultilineStringWidget(comfyPage)
const vueContextMenu = comfyPage.page.locator('.p-contextmenu')
await textarea.focus()
await textarea.click({ button: 'right' })
await expect(vueContextMenu).toBeHidden()
await expect(vueContextMenu).not.toBeVisible()
await textarea.blur()
await textarea.click({ button: 'right' })

View File

@@ -9,7 +9,6 @@ test.describe('Vue Widget Reactivity', () => {
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()
})
test('Should display added widgets', async ({ comfyPage }) => {
const loadCheckpointNode = comfyPage.page.locator(
'css=[data-testid="node-body-4"] > .lg-node-widgets > div'
@@ -33,7 +32,6 @@ test.describe('Vue Widget Reactivity', () => {
})
await expect(loadCheckpointNode).toHaveCount(4)
})
test('Should hide removed widgets', async ({ comfyPage }) => {
const loadCheckpointNode = comfyPage.page.locator(
'css=[data-testid="node-body-3"] > .lg-node-widgets > div'

View File

@@ -272,7 +272,6 @@ test.describe('Image widget', { tag: ['@screenshot', '@widget'] }, () => {
// Expect the filename combo value to be updated
await expect.poll(() => fileComboWidget.getValue()).toBe('image32x32.webp')
})
test('Displays buttons when viewing single image of batch', async ({
comfyPage
}) => {

View File

@@ -83,7 +83,7 @@ test.describe('Workflow Tab Thumbnails', { tag: '@workflow' }, () => {
1,
'Unsaved Workflow (2)'
)
await expect(thumbnailImg).toBeHidden()
await expect(thumbnailImg).not.toBeVisible()
})
async function addNode(comfyPage: ComfyPage, category: string, node: string) {

Some files were not shown because too many files have changed in this diff Show More