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
281 changed files with 4881 additions and 13040 deletions

View File

@@ -1,23 +1,14 @@
# Release workflow for ComfyUI frontend: version bump → PyPI publish → ComfyUI PR.
# Runs on a bi-weekly schedule for minor releases, or manually for patch/hotfix releases.
name: 'Release: ComfyUI'
# Automated bi-weekly workflow to bump ComfyUI frontend RC releases
name: 'Release: Bi-weekly ComfyUI'
on:
# Bi-weekly schedule: Monday at 20:00 UTC
# Schedule for Monday at 12:00 PM PST (20:00 UTC)
schedule:
- cron: '0 20 * * 1'
# Manual trigger for both on-demand minor and patch/hotfix releases
# Allow manual triggering (bypasses bi-weekly check)
workflow_dispatch:
inputs:
release_type:
description: 'minor = next minor version (bi-weekly cadence), patch = hotfix for current production version'
required: true
default: 'minor'
type: choice
options:
- minor
- patch
comfyui_fork:
description: 'ComfyUI fork to use for PR (e.g., Comfy-Org/ComfyUI)'
required: false
@@ -50,11 +41,10 @@ jobs:
- name: Summary
run: |
echo "## Release Check" >> $GITHUB_STEP_SUMMARY
echo "## Bi-weekly Check" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- Is release week: ${{ steps.check.outputs.is_release_week }}" >> $GITHUB_STEP_SUMMARY
echo "- Manual trigger: ${{ github.event_name == 'workflow_dispatch' }}" >> $GITHUB_STEP_SUMMARY
echo "- Release type: ${{ inputs.release_type || 'minor (scheduled)' }}" >> $GITHUB_STEP_SUMMARY
resolve-version:
needs: check-release-week
@@ -86,8 +76,6 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0
with:
package_json_file: frontend/package.json
- name: Setup Node.js
uses: actions/setup-node@v6
@@ -101,8 +89,6 @@ jobs:
- name: Resolve release information
id: resolve
working-directory: frontend
env:
RELEASE_TYPE: ${{ inputs.release_type || 'minor' }}
run: |
set -euo pipefail

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": "error",
"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

@@ -1,34 +0,0 @@
{
"last_node_id": 10,
"last_link_id": 0,
"nodes": [
{
"id": 10,
"type": "LoadImage",
"pos": [50, 200],
"size": [315, 314],
"flags": {},
"order": 0,
"mode": 4,
"inputs": [],
"outputs": [
{ "name": "IMAGE", "type": "IMAGE", "links": null },
{ "name": "MASK", "type": "MASK", "links": null }
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": ["nonexistent_test_image_12345.png", "image"]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [0, 0],
"scale": 1
}
},
"version": 0.4
}

View File

@@ -5,7 +5,7 @@
{
"id": 10,
"type": "LoadImage",
"pos": [50, 200],
"pos": [50, 50],
"size": [315, 314],
"flags": {},
"order": 0,
@@ -31,7 +31,7 @@
{
"id": 11,
"type": "LoadImage",
"pos": [450, 200],
"pos": [450, 50],
"size": [315, 314],
"flags": {},
"order": 1,

View File

@@ -5,7 +5,7 @@
{
"id": 10,
"type": "LoadImage",
"pos": [50, 200],
"pos": [50, 50],
"size": [315, 314],
"flags": {},
"order": 0,

View File

@@ -1,27 +1,7 @@
{
"last_node_id": 1,
"last_node_id": 0,
"last_link_id": 0,
"nodes": [
{
"id": 1,
"type": "CheckpointLoaderSimple",
"pos": [256, 256],
"size": [315, 98],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{ "name": "MODEL", "type": "MODEL", "links": null },
{ "name": "CLIP", "type": "CLIP", "links": null },
{ "name": "VAE", "type": "VAE", "links": null }
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": ["fake_model.safetensors"]
}
],
"nodes": [],
"links": [],
"groups": [],
"config": {},
@@ -35,7 +15,7 @@
{
"name": "fake_model.safetensors",
"url": "http://localhost:8188/api/devtools/fake_model.safetensors",
"directory": "checkpoints"
"directory": "text_encoders"
}
],
"version": 0.4

View File

@@ -1,42 +0,0 @@
{
"last_node_id": 1,
"last_link_id": 0,
"nodes": [
{
"id": 1,
"type": "CheckpointLoaderSimple",
"pos": [256, 256],
"size": [315, 98],
"flags": {},
"order": 0,
"mode": 4,
"inputs": [],
"outputs": [
{ "name": "MODEL", "type": "MODEL", "links": null },
{ "name": "CLIP", "type": "CLIP", "links": null },
{ "name": "VAE", "type": "VAE", "links": null }
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": ["fake_model.safetensors"]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [0, 0]
}
},
"models": [
{
"name": "fake_model.safetensors",
"url": "http://localhost:8188/api/devtools/fake_model.safetensors",
"directory": "checkpoints"
}
],
"version": 0.4
}

View File

@@ -1,141 +0,0 @@
{
"id": "test-missing-models-in-subgraph",
"revision": 0,
"last_node_id": 2,
"last_link_id": 0,
"nodes": [
{
"id": 1,
"type": "KSampler",
"pos": [100, 100],
"size": [270, 262],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{ "name": "model", "type": "MODEL", "link": null },
{ "name": "positive", "type": "CONDITIONING", "link": null },
{ "name": "negative", "type": "CONDITIONING", "link": null },
{ "name": "latent_image", "type": "LATENT", "link": null }
],
"outputs": [{ "name": "LATENT", "type": "LATENT", "links": [] }],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [0, "randomize", 20, 8, "euler", "simple", 1]
},
{
"id": 2,
"type": "subgraph-with-missing-model",
"pos": [450, 100],
"size": [400, 200],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [{ "name": "model", "type": "MODEL", "link": null }],
"outputs": [{ "name": "MODEL", "type": "MODEL", "links": null }],
"properties": {},
"widgets_values": []
}
],
"links": [],
"groups": [],
"definitions": {
"subgraphs": [
{
"id": "subgraph-with-missing-model",
"version": 1,
"state": {
"lastGroupId": 0,
"lastNodeId": 1,
"lastLinkId": 2,
"lastRerouteId": 0
},
"revision": 0,
"config": {},
"name": "Subgraph with Missing Model",
"inputNode": {
"id": -10,
"bounding": [100, 200, 120, 60]
},
"outputNode": {
"id": -20,
"bounding": [500, 200, 120, 60]
},
"inputs": [
{
"id": "input1-id",
"name": "model",
"type": "MODEL",
"linkIds": [1],
"pos": { "0": 150, "1": 220 }
}
],
"outputs": [
{
"id": "output1-id",
"name": "MODEL",
"type": "MODEL",
"linkIds": [2],
"pos": { "0": 520, "1": 220 }
}
],
"widgets": [],
"nodes": [
{
"id": 1,
"type": "CheckpointLoaderSimple",
"pos": [250, 180],
"size": [315, 98],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{ "name": "MODEL", "type": "MODEL", "links": [2] },
{ "name": "CLIP", "type": "CLIP", "links": null },
{ "name": "VAE", "type": "VAE", "links": null }
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": ["fake_model.safetensors"]
}
],
"links": [
{
"id": 1,
"origin_id": -10,
"origin_slot": 0,
"target_id": 1,
"target_slot": 0,
"type": "MODEL"
},
{
"id": 2,
"origin_id": 1,
"origin_slot": 0,
"target_id": -20,
"target_slot": 0,
"type": "MODEL"
}
]
}
]
},
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [0, 0]
}
},
"models": [
{
"name": "fake_model.safetensors",
"url": "http://localhost:8188/api/devtools/fake_model.safetensors",
"directory": "checkpoints"
}
],
"version": 0.4
}

View File

@@ -78,7 +78,7 @@
{
"name": "fake_model.safetensors",
"url": "http://localhost:8188/api/devtools/fake_model.safetensors",
"directory": "checkpoints"
"directory": "text_encoders"
}
],
"version": 0.4

View File

@@ -1,50 +0,0 @@
{
"last_node_id": 1,
"last_link_id": 0,
"nodes": [
{
"id": 1,
"type": "ImageCropV2",
"pos": [50, 50],
"size": [400, 500],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": null
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null
}
],
"properties": {
"Node name for S&R": "ImageCropV2"
},
"widgets_values": [
{
"x": 0,
"y": 0,
"width": 512,
"height": 512
}
]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [0, 0],
"scale": 1
}
},
"version": 0.4
}

View File

@@ -1,100 +0,0 @@
{
"last_node_id": 3,
"last_link_id": 2,
"nodes": [
{
"id": 1,
"type": "LoadImage",
"pos": [50, 50],
"size": [315, 314],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [1]
},
{
"name": "MASK",
"type": "MASK",
"links": null
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": ["example.png", "image"]
},
{
"id": 2,
"type": "ImageCropV2",
"pos": [450, 50],
"size": [400, 500],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 1
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [2]
}
],
"properties": {
"Node name for S&R": "ImageCropV2"
},
"widgets_values": [
{
"x": 10,
"y": 10,
"width": 100,
"height": 100
}
]
},
{
"id": 3,
"type": "PreviewImage",
"pos": [900, 50],
"size": [315, 270],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 2
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
}
],
"links": [
[1, 1, 0, 2, 0, "IMAGE"],
[2, 2, 0, 3, 0, "IMAGE"]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [0, 0],
"scale": 1
}
},
"version": 0.4
}

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()
}
@@ -414,8 +414,6 @@ export const comfyPageFixture = base.extend<{
const userId = await comfyPage.setupUser(username)
comfyPage.userIds[parallelIndex] = userId
const isVueNodes = testInfo.tags.includes('@vue-nodes')
try {
await comfyPage.setupSettings({
'Comfy.UseNewMenu': 'Top',
@@ -438,8 +436,7 @@ export const comfyPageFixture = base.extend<{
'Comfy.VersionCompatibility.DisableWarnings': true,
// Disable errors tab to prevent missing model detection from
// rendering error indicators on nodes during unrelated tests.
'Comfy.RightSidePanel.ShowErrorsTab': false,
...(isVueNodes && { 'Comfy.VueNodes.Enabled': true })
'Comfy.RightSidePanel.ShowErrorsTab': false
})
} catch (e) {
console.error(e)
@@ -451,10 +448,6 @@ export const comfyPageFixture = base.extend<{
await comfyPage.setup()
if (isVueNodes) {
await comfyPage.vueNodes.waitForNodes()
}
const needsPerf =
testInfo.tags.includes('@perf') || testInfo.tags.includes('@audit')
if (needsPerf) await comfyPage.perf.init()

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 '@e2e/fixtures/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"]'
@@ -351,7 +349,7 @@ export class AssetsSidebarTab extends SidebarTab {
async dismissToasts() {
const closeButtons = this.page.locator('.p-toast-close-button')
for (const btn of await closeButtons.all()) {
await btn.click().catch(() => {})
await btn.click({ force: true }).catch(() => {})
}
// Wait for all toast elements to fully animate out and detach from DOM
await expect(this.page.locator('.p-toast-message'))

View File

@@ -71,7 +71,7 @@ export class Topbar {
async closeWorkflowTab(tabName: string) {
const tab = this.getWorkflowTab(tabName)
await tab.hover()
await tab.locator('.close-button').click()
await tab.locator('.close-button').click({ force: true })
}
getSaveDialog(): Locator {

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

@@ -151,7 +151,6 @@ export class BuilderSelectHelper {
const widgetLocator = this.comfyPage.vueNodes
.getNodeLocator(String(nodeRef.id))
.getByLabel(widgetName, { exact: true })
// oxlint-disable-next-line playwright/no-force-option -- Node container has conditional pointer-events:none that blocks actionability
await widgetLocator.click({ force: true })
await this.comfyPage.nextFrame()
}
@@ -200,7 +199,6 @@ export class BuilderSelectHelper {
const nodeLocator = this.comfyPage.vueNodes.getNodeLocator(
String(nodeRef.id)
)
// oxlint-disable-next-line playwright/no-force-option -- Node container has conditional pointer-events:none that blocks actionability
await nodeLocator.click({ force: true })
await this.comfyPage.nextFrame()
}

View File

@@ -74,51 +74,6 @@ export class CanvasHelper {
await this.nextFrame()
}
/**
* Convert a canvas-element-relative position to absolute page coordinates.
* Use with `page.mouse` APIs when Vue DOM overlays above the canvas would
* cause Playwright's actionability check to fail on the canvas locator.
*/
private async toAbsolute(position: Position): Promise<Position> {
const box = await this.canvas.boundingBox()
if (!box) throw new Error('Canvas bounding box not available')
return { x: box.x + position.x, y: box.y + position.y }
}
/**
* Click at canvas-element-relative coordinates using `page.mouse.click()`.
* Bypasses Playwright's actionability checks on the canvas locator, which
* can fail when Vue-rendered DOM nodes overlay the `<canvas>` element.
*/
async mouseClickAt(
position: Position,
options?: {
button?: 'left' | 'right' | 'middle'
modifiers?: ('Shift' | 'Control' | 'Alt' | 'Meta')[]
}
): Promise<void> {
const abs = await this.toAbsolute(position)
const modifiers = options?.modifiers ?? []
for (const mod of modifiers) await this.page.keyboard.down(mod)
try {
await this.page.mouse.click(abs.x, abs.y, {
button: options?.button
})
} finally {
for (const mod of modifiers) await this.page.keyboard.up(mod)
}
await this.nextFrame()
}
/**
* Double-click at canvas-element-relative coordinates using `page.mouse`.
*/
async mouseDblclickAt(position: Position): Promise<void> {
const abs = await this.toAbsolute(position)
await this.page.mouse.dblclick(abs.x, abs.y)
await this.nextFrame()
}
async clickEmptySpace(): Promise<void> {
await this.canvas.click({ position: DefaultGraphPositions.emptySpaceClick })
await this.nextFrame()

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 '@e2e/fixtures/ComfyPage'
import { createMockJob } from '@e2e/fixtures/helpers/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

@@ -21,10 +21,6 @@ export const TestIds = {
contextMenu: 'canvas-context-menu',
toggleMinimapButton: 'toggle-minimap-button',
closeMinimapButton: 'close-minimap-button',
minimapContainer: 'minimap-container',
minimapCanvas: 'minimap-canvas',
minimapViewport: 'minimap-viewport',
minimapInteractionOverlay: 'minimap-interaction-overlay',
toggleLinkVisibilityButton: 'toggle-link-visibility-button',
zoomControlsButton: 'zoom-controls-button',
zoomInAction: 'zoom-in-action',
@@ -83,8 +79,7 @@ export const TestIds = {
bookmarksSection: 'node-library-bookmarks-section'
},
propertiesPanel: {
root: 'properties-panel',
errorsTab: 'panel-tab-errors'
root: 'properties-panel'
},
subgraphEditor: {
toggle: 'subgraph-editor-toggle',
@@ -135,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',
@@ -203,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

@@ -1,4 +1,5 @@
import { expect } from '@playwright/test'
import type { Page } from '@playwright/test'
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
import { ManageGroupNode } from '@e2e/helpers/manageGroupNode'
@@ -355,11 +356,7 @@ export class NodeReference {
}
async click(
position: 'title' | 'collapse',
options?: {
button?: 'left' | 'right' | 'middle'
modifiers?: ('Shift' | 'Control' | 'Alt' | 'Meta')[]
moveMouseToEmptyArea?: boolean
}
options?: Parameters<Page['click']>[1] & { moveMouseToEmptyArea?: boolean }
) {
let clickPos: Position
switch (position) {
@@ -380,7 +377,12 @@ export class NodeReference {
delete options.moveMouseToEmptyArea
}
await this.comfyPage.canvasOps.mouseClickAt(clickPos, options)
await this.comfyPage.canvas.click({
...options,
position: clickPos,
force: true
})
await this.comfyPage.nextFrame()
if (moveMouseToEmptyArea) {
await this.comfyPage.canvasOps.moveMouseToEmptyArea()
}
@@ -497,18 +499,31 @@ export class NodeReference {
await expect(async () => {
// Try just clicking the enter button first
await this.comfyPage.canvasOps.mouseClickAt({ x: 250, y: 250 })
await this.comfyPage.canvas.click({
position: { x: 250, y: 250 },
force: true
})
await this.comfyPage.nextFrame()
await this.comfyPage.canvasOps.mouseClickAt(subgraphButtonPos)
await this.comfyPage.canvas.click({
position: subgraphButtonPos,
force: true
})
await this.comfyPage.nextFrame()
if (await checkIsInSubgraph()) return
for (const position of clickPositions) {
// Clear any selection first
await this.comfyPage.canvasOps.mouseClickAt({ x: 250, y: 250 })
await this.comfyPage.canvas.click({
position: { x: 250, y: 250 },
force: true
})
await this.comfyPage.nextFrame()
// Double-click to enter subgraph
await this.comfyPage.canvasOps.mouseDblclickAt(position)
await this.comfyPage.canvas.dblclick({ position, force: true })
await this.comfyPage.nextFrame()
if (await checkIsInSubgraph()) return
}

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,30 +0,0 @@
import { expect } from '@playwright/test'
import type { Locator, Page } from '@playwright/test'
export async function clientPointOnMinimapOverlay(
overlay: Locator,
relX: number,
relY: number
): Promise<{ clientX: number; clientY: number }> {
const box = await overlay.boundingBox()
expect(box, 'Minimap interaction overlay not found').toBeTruthy()
return {
clientX: box!.x + box!.width * relX,
clientY: box!.y + box!.height * relY
}
}
export const MINIMAP_POINTER_OPTS = {
bubbles: true,
cancelable: true,
pointerId: 1
} as const
export async function readMainCanvasOffset(
page: Page
): Promise<{ x: number; y: number }> {
return page.evaluate(() => ({
x: window.app!.canvas.ds.offset[0],
y: window.app!.canvas.ds.offset[1]
}))
}

View File

@@ -1,66 +0,0 @@
import type { Locator, Page } from '@playwright/test'
import type { TestGraphAccess } from '@e2e/types/globals'
export async function drawStroke(
page: Page,
canvas: Locator,
opts: { startXPct?: number; endXPct?: number; yPct?: number } = {}
): Promise<void> {
const { startXPct = 0.3, endXPct = 0.7, yPct = 0.5 } = opts
const box = await canvas.boundingBox()
if (!box) throw new Error('Canvas bounding box not found')
await page.mouse.move(
box.x + box.width * startXPct,
box.y + box.height * yPct
)
await page.mouse.down()
await page.mouse.move(
box.x + box.width * endXPct,
box.y + box.height * yPct,
{ steps: 10 }
)
await page.mouse.up()
}
export async function hasCanvasContent(canvas: Locator): Promise<boolean> {
return canvas.evaluate((el: HTMLCanvasElement) => {
const ctx = el.getContext('2d')
if (!ctx) return false
const { data } = ctx.getImageData(0, 0, el.width, el.height)
for (let i = 3; i < data.length; i += 4) {
if (data[i] > 0) return true
}
return false
})
}
export async function triggerSerialization(page: Page): Promise<void> {
await page.evaluate(async () => {
const graph = window.graph as TestGraphAccess | undefined
if (!graph) {
throw new Error(
'Global window.graph is absent. Ensure workflow fixture is loaded.'
)
}
const node = graph._nodes_by_id?.['1']
if (!node) {
throw new Error(
'Target node with ID "1" not found in graph._nodes_by_id.'
)
}
const widget = node.widgets?.find((w) => w.name === 'mask')
if (!widget) {
throw new Error('Widget "mask" not found on target node 1.')
}
if (typeof widget.serializeValue !== 'function') {
throw new Error(
'mask widget on node 1 does not have a serializeValue function.'
)
}
await widget.serializeValue(node, 0)
})
}

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'
@@ -8,15 +9,17 @@ import type { WorkspaceStore } from '@e2e/types/globals'
const test = mergeTests(comfyPageFixture, webSocketFixture)
test.describe('Actionbar', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
})
/**
* This test ensures that the autoqueue change mode can only queue one change at a time
*/
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')
@@ -59,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
@@ -103,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

@@ -1,10 +1,10 @@
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
import type { ComfyPage } from '../fixtures/ComfyPage'
import {
comfyPageFixture as test,
comfyExpect as expect
} from '@e2e/fixtures/ComfyPage'
import { setupBuilder } from '@e2e/helpers/builderTestUtils'
import { fitToViewInstant } from '@e2e/helpers/fitToView'
} from '../fixtures/ComfyPage'
import { setupBuilder } from '../helpers/builderTestUtils'
import { fitToViewInstant } from '../helpers/fitToView'
const RESIZE_NODE_TITLE = 'Resize Image/Mask'
const RESIZE_NODE_ID = '1'

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

@@ -4,10 +4,14 @@ import {
} from '@e2e/fixtures/ComfyPage'
test.describe('Bottom Panel Logs', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
})
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()
})
@@ -31,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 ({
@@ -51,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

@@ -3,14 +3,18 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
})
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 }) => {
@@ -178,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 ({
@@ -188,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)
@@ -199,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

@@ -1,5 +1,4 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
import { expect, test } from '@playwright/test'
/**
* Cloud distribution E2E tests.

View File

@@ -8,9 +8,10 @@ const NODE_TITLE = 'KSampler'
test.describe(
'Collapsed node link positions',
{ tag: ['@canvas', '@node', '@vue-nodes'] },
{ tag: ['@canvas', '@node'] },
() => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.workflow.loadWorkflow('default')
await comfyPage.vueNodes.waitForNodes()
})

View File

@@ -20,6 +20,10 @@ async function verifyCustomIconSvg(iconElement: Locator) {
}
test.describe('Custom Icons', { tag: '@settings' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
})
test('sidebar tab icons use custom SVGs', async ({ comfyPage }) => {
// Find the icon in the sidebar
const icon = comfyPage.page.locator(

View File

@@ -20,6 +20,7 @@ async function pressKeyAndExpectRequest(
test.describe('Default Keybindings', { tag: '@keyboard' }, () => {
test.describe('Sidebar Toggle Shortcuts', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.canvas.click({ position: { x: 400, y: 400 } })
await comfyPage.nextFrame()
})
@@ -37,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()
})
}
})
@@ -163,13 +164,15 @@ test.describe('Default Keybindings', { tag: '@keyboard' }, () => {
test.describe('Mode and Panel Toggles', () => {
test("'Alt+m' toggles app mode", async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
// Set up linearData so app mode has something to show
await comfyPage.appMode.enterAppModeWithInputs([])
await expect(comfyPage.appMode.linearWidgets).toBeVisible()
// 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')
@@ -177,6 +180,7 @@ test.describe('Default Keybindings', { tag: '@keyboard' }, () => {
})
test("'Alt+Shift+m' toggles minimap", async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.Minimap.Visible', true)
await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', true)
await comfyPage.workflow.loadWorkflow('default')
@@ -185,20 +189,22 @@ 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()
})
test("'Ctrl+`' toggles terminal/logs panel", async ({ comfyPage }) => {
await expect(comfyPage.bottomPanel.root).toBeHidden()
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
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' })
@@ -103,6 +108,7 @@ test.describe('Support', () => {
comfyPage
}) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
// Prevent loading the external page
await comfyPage.page
.context()

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

@@ -7,6 +7,7 @@ test.describe('Sign In dialog', { tag: '@ui' }, () => {
let dialog: SignInDialog
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
dialog = new SignInDialog(comfyPage.page)
await dialog.open()
})

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

@@ -5,10 +5,10 @@ import {
comfyExpect as expect
} from '@e2e/fixtures/ComfyPage'
import { TestIds } from '@e2e/fixtures/selectors'
import { cleanupFakeModel } from '@e2e/tests/propertiesPanel/ErrorsTabHelper'
test.describe('Error overlay', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting(
'Comfy.RightSidePanel.ShowErrorsTab',
true
@@ -47,7 +47,16 @@ test.describe('Error overlay', { tag: '@ui' }, () => {
test('Should display "Show missing models" button for missing model errors', async ({
comfyPage
}) => {
await cleanupFakeModel(comfyPage)
await expect
.poll(() =>
comfyPage.page.evaluate(async (url: string) => {
const response = await fetch(
`${url}/api/devtools/cleanup_fake_model`
)
return response.ok
}, comfyPage.url)
)
.toBeTruthy()
await comfyPage.workflow.loadWorkflow('missing/missing_models')
@@ -91,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()
@@ -103,37 +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()
})
test('Does not resurface error overlay when switching back to workflow with missing nodes', async ({
comfyPage
}) => {
await comfyPage.settings.setSetting(
'Comfy.Workflow.WorkflowTabsPosition',
'Sidebar'
)
await comfyPage.menu.workflowsTab.open()
await comfyPage.workflow.loadWorkflow('missing/missing_nodes')
const errorOverlay = getOverlay(comfyPage.page)
await expect(errorOverlay).toBeVisible()
await errorOverlay
.getByTestId(TestIds.dialogs.errorOverlayDismiss)
.click()
await expect(errorOverlay).toBeHidden()
await comfyPage.menu.workflowsTab.open()
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
await comfyPage.menu.workflowsTab.switchToWorkflow('missing_nodes')
await expect(errorOverlay).toBeHidden()
await expect(errorOverlay).not.toBeVisible()
})
})
@@ -174,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()
})
@@ -186,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 ({
@@ -199,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 }) => {
@@ -211,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

@@ -13,6 +13,10 @@ import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
type TestSettingId = keyof Settings
test.describe('Topbar commands', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
})
test('Should allow registering topbar commands', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
window.app!.registerExtension({

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

@@ -5,6 +5,7 @@ import {
test.describe('Focus Mode', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setup()
})
@@ -13,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()
@@ -28,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()
@@ -40,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 }) => {
@@ -51,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

@@ -1,11 +1,11 @@
import type { Locator, Page } from '@playwright/test'
import { expect } from '@playwright/test'
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
test.describe('Image Compare', { tag: ['@widget', '@vue-nodes'] }, () => {
test.describe('Image Compare', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.workflow.loadWorkflow('widgets/image_compare_widget')
await comfyPage.vueNodes.waitForNodes()
})
@@ -21,12 +21,7 @@ test.describe('Image Compare', { tag: ['@widget', '@vue-nodes'] }, () => {
async function setImageCompareValue(
comfyPage: ComfyPage,
value: {
beforeImages: string[]
afterImages: string[]
beforeAlt?: string
afterAlt?: string
}
value: { beforeImages: string[]; afterImages: string[] }
) {
await comfyPage.page.evaluate(
({ value }) => {
@@ -42,48 +37,6 @@ test.describe('Image Compare', { tag: ['@widget', '@vue-nodes'] }, () => {
await comfyPage.nextFrame()
}
async function moveToPercentage(
page: Page,
containerLocator: Locator,
percentage: number
) {
const box = await containerLocator.boundingBox()
if (!box) throw new Error('Container not found')
await page.mouse.move(
box.x + box.width * (percentage / 100),
box.y + box.height / 2
)
}
async function waitForImagesLoaded(node: Locator) {
await expect
.poll(() =>
node.evaluate((el) => {
const imgs = el.querySelectorAll('img')
return (
imgs.length > 0 &&
Array.from(imgs).every(
(img) => img.complete && img.naturalWidth > 0
)
)
})
)
.toBe(true)
}
async function getClipPathInsetRightPercent(imgLocator: Locator) {
return imgLocator.evaluate((el) => {
// Accessing raw style avoids cross-browser getComputedStyle normalization issues
// Format is uniformly "inset(0 60% 0 0)" per Vue runtime inline style bindings
const parts = (el as HTMLElement).style.clipPath.split(' ')
return parts.length > 1 ? parseFloat(parts[1]) : -1
})
}
// ---------------------------------------------------------------------------
// Rendering
// ---------------------------------------------------------------------------
test(
'Shows empty state when no images are set',
{ tag: '@smoke' },
@@ -93,14 +46,10 @@ test.describe('Image Compare', { tag: ['@widget', '@vue-nodes'] }, () => {
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)
}
)
// ---------------------------------------------------------------------------
// Slider defaults
// ---------------------------------------------------------------------------
test(
'Slider defaults to 50% with both images set',
{ tag: ['@smoke', '@screenshot'] },
@@ -118,444 +67,15 @@ test.describe('Image Compare', { tag: ['@widget', '@vue-nodes'] }, () => {
await expect(beforeImg).toBeVisible()
await expect(afterImg).toBeVisible()
const handle = node.getByRole('presentation')
const handle = node.locator('[role="presentation"]')
await expect(handle).toBeVisible()
expect(
await handle.evaluate((el) => (el as HTMLElement).style.left),
'Slider should default to 50% before screenshot'
await handle.evaluate((el) => (el as HTMLElement).style.left)
).toBe('50%')
await expect
.poll(() => getClipPathInsetRightPercent(beforeImg))
.toBeCloseTo(50, 0)
await expect(beforeImg).toHaveCSS('clip-path', /50%/)
await waitForImagesLoaded(node)
await comfyPage.page.mouse.move(0, 0)
await expect(node).toHaveScreenshot('image-compare-default-50.png')
}
)
// ---------------------------------------------------------------------------
// Slider interaction
// ---------------------------------------------------------------------------
test(
'Mouse hover moves slider position',
{ tag: '@smoke' },
async ({ comfyPage }) => {
const beforeUrl = createTestImageDataUrl('Before', '#c00')
const afterUrl = createTestImageDataUrl('After', '#00c')
await setImageCompareValue(comfyPage, {
beforeImages: [beforeUrl],
afterImages: [afterUrl]
})
const node = comfyPage.vueNodes.getNodeLocator('1')
const handle = node.getByRole('presentation')
const beforeImg = node.locator('img[alt="Before image"]')
const afterImg = node.locator('img[alt="After image"]')
await expect(afterImg).toBeVisible()
// Left edge: sliderPosition ≈ 5 → clip-path inset right ≈ 95%
await moveToPercentage(comfyPage.page, afterImg, 5)
await expect
.poll(() => getClipPathInsetRightPercent(beforeImg))
.toBeGreaterThan(90)
await expect
.poll(() =>
handle.evaluate((el) => parseFloat((el as HTMLElement).style.left))
)
.toBeLessThan(10)
// Right edge: sliderPosition ≈ 95 → clip-path inset right ≈ 5%
await moveToPercentage(comfyPage.page, afterImg, 95)
await expect
.poll(() => getClipPathInsetRightPercent(beforeImg))
.toBeLessThan(10)
await expect
.poll(() =>
handle.evaluate((el) => parseFloat((el as HTMLElement).style.left))
)
.toBeGreaterThan(90)
}
)
test('Slider preserves last position when mouse leaves widget', async ({
comfyPage
}) => {
const beforeUrl = createTestImageDataUrl('Before', '#c00')
const afterUrl = createTestImageDataUrl('After', '#00c')
await setImageCompareValue(comfyPage, {
beforeImages: [beforeUrl],
afterImages: [afterUrl]
})
const node = comfyPage.vueNodes.getNodeLocator('1')
const handle = node.getByRole('presentation')
const afterImg = node.locator('img[alt="After image"]')
await expect(afterImg).toBeVisible()
await moveToPercentage(comfyPage.page, afterImg, 30)
// Wait for Vue to commit the slider update
await expect
.poll(() =>
handle.evaluate((el) => parseFloat((el as HTMLElement).style.left))
)
.toBeCloseTo(30, 0)
const positionWhileInside = parseFloat(
await handle.evaluate((el) => (el as HTMLElement).style.left)
)
await comfyPage.page.mouse.move(0, 0)
// Position must not reset to default 50%
await expect
.poll(() =>
handle.evaluate((el) => parseFloat((el as HTMLElement).style.left))
)
.toBeCloseTo(positionWhileInside, 0)
})
test('Slider clamps to 0% at left edge of container', async ({
comfyPage
}) => {
const beforeUrl = createTestImageDataUrl('Before', '#c00')
const afterUrl = createTestImageDataUrl('After', '#00c')
await setImageCompareValue(comfyPage, {
beforeImages: [beforeUrl],
afterImages: [afterUrl]
})
const node = comfyPage.vueNodes.getNodeLocator('1')
const handle = node.getByRole('presentation')
const afterImg = node.locator('img[alt="After image"]')
await expect(afterImg).toBeVisible()
const box = await afterImg.boundingBox()
if (!box) throw new Error('Container not found')
// Move to the leftmost pixel (elementX = 0 → sliderPosition = 0)
await comfyPage.page.mouse.move(box.x, box.y + box.height / 2)
await expect
.poll(() => handle.evaluate((el) => (el as HTMLElement).style.left))
.toBe('0%')
})
// ---------------------------------------------------------------------------
// Single image modes
// ---------------------------------------------------------------------------
test('Only before image shows without slider when afterImages is empty', async ({
comfyPage
}) => {
const url = createTestImageDataUrl('Before', '#c00')
await setImageCompareValue(comfyPage, {
beforeImages: [url],
afterImages: []
})
const node = comfyPage.vueNodes.getNodeLocator('1')
await expect(node.locator('img')).toHaveCount(1)
await expect(node.getByRole('presentation')).toBeHidden()
})
test('Only after image shows without slider when beforeImages is empty', async ({
comfyPage
}) => {
const url = createTestImageDataUrl('After', '#00c')
await setImageCompareValue(comfyPage, {
beforeImages: [],
afterImages: [url]
})
const node = comfyPage.vueNodes.getNodeLocator('1')
await expect(node.locator('img')).toHaveCount(1)
await expect(node.getByRole('presentation')).toBeHidden()
})
// ---------------------------------------------------------------------------
// Batch navigation
// ---------------------------------------------------------------------------
test(
'Batch navigation appears when before side has multiple images',
{ tag: '@smoke' },
async ({ comfyPage }) => {
const url1 = createTestImageDataUrl('A1', '#c00')
const url2 = createTestImageDataUrl('A2', '#0c0')
const url3 = createTestImageDataUrl('A3', '#00c')
const afterUrl = createTestImageDataUrl('B1', '#888')
await setImageCompareValue(comfyPage, {
beforeImages: [url1, url2, url3],
afterImages: [afterUrl]
})
const node = comfyPage.vueNodes.getNodeLocator('1')
const beforeBatch = node.getByTestId('before-batch')
await expect(node.getByTestId('batch-nav')).toBeVisible()
await expect(beforeBatch.getByTestId('batch-counter')).toHaveText('1 / 3')
// after-batch renders only when afterBatchCount > 1
await expect(node.getByTestId('after-batch')).toBeHidden()
await expect(beforeBatch.getByTestId('batch-prev')).toBeDisabled()
}
)
test('Batch navigation is hidden when both sides have single images', async ({
comfyPage
}) => {
const url = createTestImageDataUrl('Image', '#c00')
await setImageCompareValue(comfyPage, {
beforeImages: [url],
afterImages: [url]
})
const node = comfyPage.vueNodes.getNodeLocator('1')
await expect(node.getByTestId('batch-nav')).toBeHidden()
})
test(
'Navigate forward through before images',
{ tag: '@smoke' },
async ({ comfyPage }) => {
const url1 = createTestImageDataUrl('A1', '#c00')
const url2 = createTestImageDataUrl('A2', '#0c0')
const url3 = createTestImageDataUrl('A3', '#00c')
await setImageCompareValue(comfyPage, {
beforeImages: [url1, url2, url3],
afterImages: [createTestImageDataUrl('B1', '#888')]
})
const node = comfyPage.vueNodes.getNodeLocator('1')
const beforeBatch = node.getByTestId('before-batch')
const counter = beforeBatch.getByTestId('batch-counter')
const nextBtn = beforeBatch.getByTestId('batch-next')
const prevBtn = beforeBatch.getByTestId('batch-prev')
await nextBtn.click()
await expect(counter).toHaveText('2 / 3')
await expect(node.locator('img[alt="Before image"]')).toHaveAttribute(
'src',
url2
)
await expect(prevBtn).toBeEnabled()
await nextBtn.click()
await expect(counter).toHaveText('3 / 3')
await expect(nextBtn).toBeDisabled()
}
)
test('Navigate backward through before images', async ({ comfyPage }) => {
const url1 = createTestImageDataUrl('A1', '#c00')
const url2 = createTestImageDataUrl('A2', '#0c0')
const url3 = createTestImageDataUrl('A3', '#00c')
await setImageCompareValue(comfyPage, {
beforeImages: [url1, url2, url3],
afterImages: [createTestImageDataUrl('B1', '#888')]
})
const node = comfyPage.vueNodes.getNodeLocator('1')
const beforeBatch = node.getByTestId('before-batch')
const counter = beforeBatch.getByTestId('batch-counter')
const nextBtn = beforeBatch.getByTestId('batch-next')
const prevBtn = beforeBatch.getByTestId('batch-prev')
await nextBtn.click()
await nextBtn.click()
await expect(counter).toHaveText('3 / 3')
await prevBtn.click()
await expect(counter).toHaveText('2 / 3')
await expect(prevBtn).toBeEnabled()
await expect(nextBtn).toBeEnabled()
})
test('Before and after batch navigation are independent', async ({
comfyPage
}) => {
const url1 = createTestImageDataUrl('A1', '#c00')
const url2 = createTestImageDataUrl('A2', '#0c0')
const url3 = createTestImageDataUrl('A3', '#00c')
const urlA = createTestImageDataUrl('B1', '#880')
const urlB = createTestImageDataUrl('B2', '#008')
await setImageCompareValue(comfyPage, {
beforeImages: [url1, url2, url3],
afterImages: [urlA, urlB]
})
const node = comfyPage.vueNodes.getNodeLocator('1')
const beforeBatch = node.getByTestId('before-batch')
const afterBatch = node.getByTestId('after-batch')
await beforeBatch.getByTestId('batch-next').click()
await afterBatch.getByTestId('batch-next').click()
await expect(beforeBatch.getByTestId('batch-counter')).toHaveText('2 / 3')
await expect(afterBatch.getByTestId('batch-counter')).toHaveText('2 / 2')
await expect(node.locator('img[alt="Before image"]')).toHaveAttribute(
'src',
url2
)
await expect(node.locator('img[alt="After image"]')).toHaveAttribute(
'src',
urlB
)
})
// ---------------------------------------------------------------------------
// Visual regression screenshots
// ---------------------------------------------------------------------------
for (const { pct, expectedClipMin, expectedClipMax } of [
{ pct: 25, expectedClipMin: 70, expectedClipMax: 80 },
{ pct: 75, expectedClipMin: 20, expectedClipMax: 30 }
]) {
test(
`Screenshot at ${pct}% slider position`,
{ tag: '@screenshot' },
async ({ comfyPage }) => {
const beforeUrl = createTestImageDataUrl('Before', '#c00')
const afterUrl = createTestImageDataUrl('After', '#00c')
await setImageCompareValue(comfyPage, {
beforeImages: [beforeUrl],
afterImages: [afterUrl]
})
const node = comfyPage.vueNodes.getNodeLocator('1')
const beforeImg = node.locator('img[alt="Before image"]')
const afterImg = node.locator('img[alt="After image"]')
await waitForImagesLoaded(node)
await moveToPercentage(comfyPage.page, afterImg, pct)
await expect
.poll(() => getClipPathInsetRightPercent(beforeImg))
.toBeGreaterThan(expectedClipMin)
await expect
.poll(() => getClipPathInsetRightPercent(beforeImg))
.toBeLessThan(expectedClipMax)
await expect(node).toHaveScreenshot(`image-compare-slider-${pct}.png`)
}
)
}
// ---------------------------------------------------------------------------
// Edge cases
// ---------------------------------------------------------------------------
test('Widget remains stable with broken image URLs', async ({
comfyPage
}) => {
await setImageCompareValue(comfyPage, {
beforeImages: ['https://example.invalid/broken.png'],
afterImages: ['https://example.invalid/broken2.png']
})
const node = comfyPage.vueNodes.getNodeLocator('1')
await expect(node.locator('img')).toHaveCount(2)
await expect(node.getByRole('presentation')).toBeVisible()
await expect
.poll(() =>
node.evaluate((el) => {
const imgs = el.querySelectorAll('img')
let errors = 0
imgs.forEach((img) => {
if (img.complete && img.naturalWidth === 0 && img.src) errors++
})
return errors
})
)
.toBe(2)
})
test('Rapid value updates show latest images and reset batch index', async ({
comfyPage
}) => {
const redUrl = createTestImageDataUrl('Red', '#c00')
const green1Url = createTestImageDataUrl('G1', '#0c0')
const green2Url = createTestImageDataUrl('G2', '#090')
const blueUrl = createTestImageDataUrl('Blue', '#00c')
await setImageCompareValue(comfyPage, {
beforeImages: [redUrl, green1Url],
afterImages: [blueUrl]
})
const node = comfyPage.vueNodes.getNodeLocator('1')
await node.getByTestId('before-batch').getByTestId('batch-next').click()
await expect(
node.getByTestId('before-batch').getByTestId('batch-counter')
).toHaveText('2 / 2')
await setImageCompareValue(comfyPage, {
beforeImages: [green1Url, green2Url],
afterImages: [blueUrl]
})
await expect(node.locator('img[alt="Before image"]')).toHaveAttribute(
'src',
green1Url
)
await expect(
node.getByTestId('before-batch').getByTestId('batch-counter')
).toHaveText('1 / 2')
})
test('Legacy string value shows single image without slider', async ({
comfyPage
}) => {
const url = createTestImageDataUrl('Legacy', '#c00')
await comfyPage.page.evaluate(
({ url }) => {
const node = window.app!.graph.getNodeById(1)
const widget = node?.widgets?.find((w) => w.type === 'imagecompare')
if (widget) {
widget.value = url
widget.callback?.(url)
}
},
{ url }
)
await comfyPage.nextFrame()
const node = comfyPage.vueNodes.getNodeLocator('1')
await expect(node.locator('img')).toHaveCount(1)
await expect(node.getByRole('presentation')).toBeHidden()
})
test('Custom beforeAlt and afterAlt are used as img alt text', async ({
comfyPage
}) => {
const beforeUrl = createTestImageDataUrl('Before', '#c00')
const afterUrl = createTestImageDataUrl('After', '#00c')
await setImageCompareValue(comfyPage, {
beforeImages: [beforeUrl],
afterImages: [afterUrl],
beforeAlt: 'Custom before',
afterAlt: 'Custom after'
})
const node = comfyPage.vueNodes.getNodeLocator('1')
await expect(node.locator('img[alt="Custom before"]')).toBeVisible()
await expect(node.locator('img[alt="Custom after"]')).toBeVisible()
})
test('Large batch sizes show correct counter', async ({ comfyPage }) => {
const images = Array.from({ length: 20 }, (_, i) =>
createTestImageDataUrl(String(i + 1), '#c00')
)
await setImageCompareValue(comfyPage, {
beforeImages: images,
afterImages: images
})
const node = comfyPage.vueNodes.getNodeLocator('1')
await expect(
node.getByTestId('before-batch').getByTestId('batch-counter')
).toHaveText('1 / 20')
await expect(
node.getByTestId('after-batch').getByTestId('batch-counter')
).toHaveText('1 / 20')
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

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(
@@ -895,6 +853,7 @@ test.describe('Load workflow', { tag: '@screenshot' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
workflowA = generateUniqueFilename()
await comfyPage.menu.topbar.saveWorkflow(workflowA)
workflowB = generateUniqueFilename()
@@ -969,6 +928,7 @@ test.describe('Load workflow', { tag: '@screenshot' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
workflowA = generateUniqueFilename()
await comfyPage.menu.topbar.saveWorkflow(workflowA)
workflowB = generateUniqueFilename()
@@ -1058,10 +1018,13 @@ test.describe('Load workflow', { tag: '@screenshot' }, () => {
})
test.describe('Load duplicate workflow', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
})
test('A workflow can be loaded multiple times in a row', async ({
comfyPage
}) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
await comfyPage.menu.workflowsTab.open()
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
@@ -1289,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')
@@ -1358,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

@@ -7,6 +7,7 @@ import {
test.describe('Job History Actions', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setup()
// Expand the queue overlay so the JobHistoryActionsMenu is visible
@@ -24,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()
})
@@ -33,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()
})
@@ -42,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()
})
@@ -50,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 ({
@@ -73,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

@@ -5,6 +5,7 @@ import {
test.describe('Linear Mode', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setup()
})
@@ -13,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

@@ -3,7 +3,11 @@ import { expect } from '@playwright/test'
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
test.describe('Mask Editor', { tag: '@vue-nodes' }, () => {
test.describe('Mask Editor', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
})
async function loadImageOnNode(comfyPage: ComfyPage) {
await comfyPage.workflow.loadWorkflow('widgets/load_image_widget')
await comfyPage.vueNodes.waitForNodes()

View File

@@ -3,6 +3,10 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
test.describe('Menu', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
})
test('Can register sidebar tab', async ({ comfyPage }) => {
const initialChildrenCount = await comfyPage.menu.buttons.count()
@@ -97,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/)
@@ -122,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/)
@@ -134,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

@@ -1,45 +1,11 @@
import { expect } from '@playwright/test'
import type { Locator, Page } from '@playwright/test'
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
import { TestIds } from '@e2e/fixtures/selectors'
import {
clientPointOnMinimapOverlay,
MINIMAP_POINTER_OPTS,
readMainCanvasOffset
} from '@e2e/helpers/minimap'
function hasCanvasContent(canvas: Locator): Promise<boolean> {
return canvas.evaluate((el: HTMLCanvasElement) => {
const ctx = el.getContext('2d')
if (!ctx) return false
const { data } = ctx.getImageData(0, 0, el.width, el.height)
for (let i = 3; i < data.length; i += 4) {
if (data[i] > 0) return true
}
return false
})
}
async function clickMinimapAt(
overlay: Locator,
page: Page,
relX: number,
relY: number
) {
const box = await overlay.boundingBox()
expect(box, 'Minimap interaction overlay not found').toBeTruthy()
// Click area — avoiding the settings button (top-left, 32×32px)
// and close button (top-right, 32×32px)
await page.mouse.click(
box!.x + box!.width * relX,
box!.y + box!.height * relY
)
}
test.describe('Minimap', { tag: '@canvas' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.Minimap.Visible', true)
await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', true)
await comfyPage.workflow.loadWorkflow('default')
@@ -47,20 +13,14 @@ test.describe('Minimap', { tag: '@canvas' }, () => {
})
test('Validate minimap is visible by default', async ({ comfyPage }) => {
const minimapContainer = comfyPage.page.getByTestId(
TestIds.canvas.minimapContainer
)
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
await expect(minimapContainer).toBeVisible()
const minimapCanvas = minimapContainer.getByTestId(
TestIds.canvas.minimapCanvas
)
const minimapCanvas = minimapContainer.locator('.minimap-canvas')
await expect(minimapCanvas).toBeVisible()
const minimapViewport = minimapContainer.getByTestId(
TestIds.canvas.minimapViewport
)
const minimapViewport = minimapContainer.locator('.minimap-viewport')
await expect(minimapViewport).toBeVisible()
await expect(minimapContainer).toHaveCSS('position', 'relative')
@@ -80,16 +40,12 @@ test.describe('Minimap', { tag: '@canvas' }, () => {
await expect(toggleButton).toBeVisible()
const minimapContainer = comfyPage.page.getByTestId(
TestIds.canvas.minimapContainer
)
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
await expect(minimapContainer).toBeVisible()
})
test('Validate minimap can be toggled off and on', async ({ comfyPage }) => {
const minimapContainer = comfyPage.page.getByTestId(
TestIds.canvas.minimapContainer
)
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
const toggleButton = comfyPage.page.getByTestId(
TestIds.canvas.toggleMinimapButton
)
@@ -97,66 +53,42 @@ 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()
})
test('Validate minimap keyboard shortcut Alt+M', async ({ comfyPage }) => {
const minimapContainer = comfyPage.page.getByTestId(
TestIds.canvas.minimapContainer
)
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
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()
})
test('Close button hides minimap; toolbar toggle reopens it', async ({
comfyPage
}) => {
const minimap = comfyPage.page.getByTestId(TestIds.canvas.minimapContainer)
const viewport = minimap.getByTestId(TestIds.canvas.minimapViewport)
const toggleButton = comfyPage.page.getByTestId(
TestIds.canvas.toggleMinimapButton
)
test('Close button hides minimap', async ({ comfyPage }) => {
const minimap = comfyPage.page.locator('.litegraph-minimap')
await expect(minimap).toBeVisible()
await comfyPage.page.getByTestId(TestIds.canvas.closeMinimapButton).click()
await expect(minimap).toBeHidden()
await expect
.poll(() =>
comfyPage.settings.getSetting<boolean>('Comfy.Minimap.Visible')
)
.toBe(false)
await expect(minimap).not.toBeVisible()
const toggleButton = comfyPage.page.getByTestId(
TestIds.canvas.toggleMinimapButton
)
await expect(toggleButton).toBeVisible()
await toggleButton.click()
await expect(minimap).toBeVisible()
await expect(viewport).toBeVisible()
await expect
.poll(() =>
comfyPage.settings.getSetting<boolean>('Comfy.Minimap.Visible')
)
.toBe(true)
})
test(
'Panning canvas moves minimap viewport',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
const minimap = comfyPage.page.getByTestId(
TestIds.canvas.minimapContainer
)
const minimap = comfyPage.page.locator('.litegraph-minimap')
await expect(minimap).toBeVisible()
await expect(minimap).toHaveScreenshot('minimap-before-pan.png')
@@ -173,244 +105,14 @@ test.describe('Minimap', { tag: '@canvas' }, () => {
}
)
test('Minimap canvas is non-empty for a workflow with nodes', async ({
comfyPage
}) => {
const minimapCanvas = comfyPage.page.getByTestId(
TestIds.canvas.minimapCanvas
)
await expect(minimapCanvas).toBeVisible()
await expect.poll(() => hasCanvasContent(minimapCanvas)).toBe(true)
})
test('Minimap canvas is empty after all nodes are deleted', async ({
comfyPage
}) => {
const minimapCanvas = comfyPage.page.getByTestId(
TestIds.canvas.minimapCanvas
)
await expect(minimapCanvas).toBeVisible()
await comfyPage.keyboard.selectAll()
await comfyPage.vueNodes.deleteSelected()
await expect.poll(() => comfyPage.nodeOps.getGraphNodesCount()).toBe(0)
await expect.poll(() => hasCanvasContent(minimapCanvas)).toBe(false)
})
test('Clicking minimap corner pans the main canvas', async ({
comfyPage
}) => {
const minimap = comfyPage.page.getByTestId(TestIds.canvas.minimapContainer)
const viewport = minimap.getByTestId(TestIds.canvas.minimapViewport)
const overlay = comfyPage.page.getByTestId(
TestIds.canvas.minimapInteractionOverlay
)
await expect(minimap).toBeVisible()
const before = await comfyPage.page.evaluate(() => ({
x: window.app!.canvas.ds.offset[0],
y: window.app!.canvas.ds.offset[1]
}))
const transformBefore = await viewport.evaluate(
(el: HTMLElement) => el.style.transform
)
await clickMinimapAt(overlay, comfyPage.page, 0.15, 0.85)
await expect
.poll(() =>
comfyPage.page.evaluate(() => ({
x: window.app!.canvas.ds.offset[0],
y: window.app!.canvas.ds.offset[1]
}))
)
.not.toStrictEqual(before)
await expect
.poll(() => viewport.evaluate((el: HTMLElement) => el.style.transform))
.not.toBe(transformBefore)
})
test('Clicking minimap center after FitView causes minimal canvas movement', async ({
comfyPage
}) => {
const minimap = comfyPage.page.getByTestId(TestIds.canvas.minimapContainer)
const overlay = comfyPage.page.getByTestId(
TestIds.canvas.minimapInteractionOverlay
)
const viewport = minimap.getByTestId(TestIds.canvas.minimapViewport)
await expect(minimap).toBeVisible()
await comfyPage.page.evaluate(() => {
const canvas = window.app!.canvas
canvas.ds.offset[0] -= 1000
canvas.setDirty(true, true)
})
await comfyPage.nextFrame()
const transformBefore = await viewport.evaluate(
(el: HTMLElement) => el.style.transform
)
await comfyPage.page.evaluate(() => {
window.app!.canvas.fitViewToSelectionAnimated({ duration: 1 })
})
await expect
.poll(() => viewport.evaluate((el: HTMLElement) => el.style.transform), {
timeout: 2000
})
.not.toBe(transformBefore)
await comfyPage.nextFrame()
const before = await comfyPage.page.evaluate(() => ({
x: window.app!.canvas.ds.offset[0],
y: window.app!.canvas.ds.offset[1]
}))
await clickMinimapAt(overlay, comfyPage.page, 0.5, 0.5)
await comfyPage.nextFrame()
const after = await comfyPage.page.evaluate(() => ({
x: window.app!.canvas.ds.offset[0],
y: window.app!.canvas.ds.offset[1]
}))
// ~3px overlay error × ~15 canvas/minimap scale ≈ 45, rounded up
const TOLERANCE = 50
expect(
Math.abs(after.x - before.x),
`offset.x changed by more than ${TOLERANCE} after clicking minimap center post-FitView`
).toBeLessThan(TOLERANCE)
expect(
Math.abs(after.y - before.y),
`offset.y changed by more than ${TOLERANCE} after clicking minimap center post-FitView`
).toBeLessThan(TOLERANCE)
})
test('Dragging on minimap pans the main canvas progressively', async ({
comfyPage
}) => {
const overlay = comfyPage.page.getByTestId(
TestIds.canvas.minimapInteractionOverlay
)
const initial = await readMainCanvasOffset(comfyPage.page)
const start = await clientPointOnMinimapOverlay(overlay, 0.48, 0.5)
await overlay.dispatchEvent('pointerdown', {
...MINIMAP_POINTER_OPTS,
clientX: start.clientX,
clientY: start.clientY
})
await comfyPage.nextFrame()
const firstMove = await clientPointOnMinimapOverlay(overlay, 0.35, 0.52)
await overlay.dispatchEvent('pointermove', {
...MINIMAP_POINTER_OPTS,
clientX: firstMove.clientX,
clientY: firstMove.clientY
})
await expect
.poll(() => readMainCanvasOffset(comfyPage.page))
.not.toStrictEqual(initial)
const afterFirstMove = await readMainCanvasOffset(comfyPage.page)
const secondMove = await clientPointOnMinimapOverlay(overlay, 0.22, 0.55)
await overlay.dispatchEvent('pointermove', {
...MINIMAP_POINTER_OPTS,
clientX: secondMove.clientX,
clientY: secondMove.clientY
})
await expect
.poll(() => readMainCanvasOffset(comfyPage.page))
.not.toStrictEqual(afterFirstMove)
const upPoint = await clientPointOnMinimapOverlay(overlay, 0.18, 0.58)
await overlay.dispatchEvent('pointerup', {
...MINIMAP_POINTER_OPTS,
clientX: upPoint.clientX,
clientY: upPoint.clientY
})
await comfyPage.nextFrame()
const final = await readMainCanvasOffset(comfyPage.page)
const panDistance =
Math.abs(final.x - initial.x) + Math.abs(final.y - initial.y)
expect(
panDistance,
'Expected drag from center toward lower-left on minimap to move the canvas meaningfully'
).toBeGreaterThan(40)
})
test('Minimap viewport transform updates during overlay drag', async ({
comfyPage
}) => {
const minimap = comfyPage.page.getByTestId(TestIds.canvas.minimapContainer)
const viewport = minimap.getByTestId(TestIds.canvas.minimapViewport)
const overlay = comfyPage.page.getByTestId(
TestIds.canvas.minimapInteractionOverlay
)
const transformBefore = await viewport.evaluate(
(el: HTMLElement) => el.style.transform
)
const start = await clientPointOnMinimapOverlay(overlay, 0.48, 0.5)
await overlay.dispatchEvent('pointerdown', {
...MINIMAP_POINTER_OPTS,
clientX: start.clientX,
clientY: start.clientY
})
await comfyPage.nextFrame()
const mid = await clientPointOnMinimapOverlay(overlay, 0.28, 0.52)
await overlay.dispatchEvent('pointermove', {
...MINIMAP_POINTER_OPTS,
clientX: mid.clientX,
clientY: mid.clientY
})
await expect
.poll(() => viewport.evaluate((el: HTMLElement) => el.style.transform))
.not.toBe(transformBefore)
const transformMid = await viewport.evaluate(
(el: HTMLElement) => el.style.transform
)
const later = await clientPointOnMinimapOverlay(overlay, 0.18, 0.55)
await overlay.dispatchEvent('pointermove', {
...MINIMAP_POINTER_OPTS,
clientX: later.clientX,
clientY: later.clientY
})
await expect
.poll(() => viewport.evaluate((el: HTMLElement) => el.style.transform))
.not.toBe(transformMid)
const upPoint = await clientPointOnMinimapOverlay(overlay, 0.18, 0.55)
await overlay.dispatchEvent('pointerup', {
...MINIMAP_POINTER_OPTS,
clientX: upPoint.clientX,
clientY: upPoint.clientY
})
})
test(
'Viewport rectangle is visible and positioned within minimap',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
const minimap = comfyPage.page.getByTestId(
TestIds.canvas.minimapContainer
)
const minimap = comfyPage.page.locator('.litegraph-minimap')
await expect(minimap).toBeVisible()
const viewport = minimap.getByTestId(TestIds.canvas.minimapViewport)
const viewport = minimap.locator('.minimap-viewport')
await expect(viewport).toBeVisible()
await expect(async () => {
@@ -430,37 +132,3 @@ test.describe('Minimap', { tag: '@canvas' }, () => {
}
)
})
test.describe('Minimap mobile', { tag: ['@mobile', '@canvas'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', true)
await comfyPage.workflow.loadWorkflow('default')
await comfyPage.page.waitForFunction(() => window.app && window.app.canvas)
})
test('Minimap is hidden by default on small viewports', async ({
comfyPage
}) => {
const innerWidth = await comfyPage.page.evaluate(() => window.innerWidth)
expect(
innerWidth,
'Mobile project should use a viewport narrower than the lg breakpoint'
).toBeLessThan(1024)
await expect
.poll(() =>
comfyPage.settings.getSetting<boolean>('Comfy.Minimap.Visible')
)
.toBe(false)
await expect(
comfyPage.page.getByTestId(TestIds.canvas.minimapContainer)
).toHaveCount(0)
const toggle = comfyPage.page.getByTestId(
TestIds.canvas.toggleMinimapButton
)
await expect(toggle).toBeVisible()
})
})

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

@@ -29,7 +29,7 @@ async function openSelectionToolboxHelp(comfyPage: ComfyPage) {
const helpButton = comfyPage.selectionToolbox.getByTestId('info-button')
await expect(helpButton).toBeVisible()
await helpButton.click()
await helpButton.click({ force: true })
await comfyPage.nextFrame()
return comfyPage.page.getByTestId('properties-panel')
@@ -88,6 +88,7 @@ async function setLocaleAndWaitForWorkflowReload(
test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setup()
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.NodeLibrary.NewDesign', false)
})
@@ -190,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()
})
})
@@ -504,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

@@ -5,6 +5,8 @@ import {
test.describe('Node Library Essentials Tab', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
// Enable the essentials feature flag via the reactive serverFeatureFlags ref.
// In production, this flag comes via WebSocket or remoteConfig (cloud only).
// The localhost test server has neither, so we set it directly.

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,415 +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 '@e2e/fixtures/ComfyPage'
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
import { webSocketFixture } from '@e2e/fixtures/ws'
import { ExecutionHelper } from '@e2e/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.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

@@ -1,17 +1,10 @@
import type { UploadImageResponse } from '@comfyorg/ingest-types'
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
import {
drawStroke,
hasCanvasContent,
triggerSerialization
} from '@e2e/helpers/painter'
test.describe('Painter', { tag: ['@widget', '@vue-nodes'] }, () => {
test.describe('Painter', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => window.app?.graph?.clear())
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.workflow.loadWorkflow('widgets/painter_widget')
await comfyPage.vueNodes.waitForNodes()
})
@@ -27,15 +20,9 @@ test.describe('Painter', { tag: ['@widget', '@vue-nodes'] }, () => {
await expect(painterWidget).toBeVisible()
await expect(painterWidget.locator('canvas')).toBeVisible()
await expect(
painterWidget.getByRole('button', { name: 'Brush' })
).toBeVisible()
await expect(
painterWidget.getByRole('button', { name: 'Eraser' })
).toBeVisible()
await expect(
painterWidget.getByTestId('painter-clear-button')
).toBeVisible()
await expect(painterWidget.getByText('Brush')).toBeVisible()
await expect(painterWidget.getByText('Eraser')).toBeVisible()
await expect(painterWidget.getByText('Clear')).toBeVisible()
await expect(
painterWidget.locator('input[type="color"]').first()
).toBeVisible()
@@ -52,66 +39,22 @@ test.describe('Painter', { tag: ['@widget', '@vue-nodes'] }, () => {
const canvas = node.locator('.widget-expands canvas')
await expect(canvas).toBeVisible()
expect(await hasCanvasContent(canvas), 'canvas should start empty').toBe(
false
)
await drawStroke(comfyPage.page, canvas)
await expect
.poll(() => hasCanvasContent(canvas), {
message: 'canvas should have content after stroke'
})
.poll(async () =>
canvas.evaluate((el) => {
const ctx = (el as HTMLCanvasElement).getContext('2d')
if (!ctx) return true
const data = ctx.getImageData(
0,
0,
(el as HTMLCanvasElement).width,
(el as HTMLCanvasElement).height
)
return data.data.every((v, i) => (i % 4 === 3 ? v === 0 : true))
})
)
.toBe(true)
await expect(node).toHaveScreenshot('painter-after-stroke.png')
}
)
test.describe('Drawing', () => {
test(
'Eraser removes drawn content',
{ tag: '@smoke' },
async ({ comfyPage }) => {
const node = comfyPage.vueNodes.getNodeLocator('1')
const painterWidget = node.locator('.widget-expands')
const canvas = painterWidget.locator('canvas')
await drawStroke(comfyPage.page, canvas)
await expect
.poll(() => hasCanvasContent(canvas), {
message: 'canvas must have content before erasing'
})
.toBe(true)
await painterWidget.getByRole('button', { name: 'Eraser' }).click()
await drawStroke(comfyPage.page, canvas)
await expect
.poll(
() =>
canvas.evaluate((el: HTMLCanvasElement) => {
const ctx = el.getContext('2d')
if (!ctx) return false
const cx = Math.floor(el.width / 2)
const cy = Math.floor(el.height / 2)
const { data } = ctx.getImageData(cx - 5, cy - 5, 10, 10)
return data.every((v, i) => i % 4 !== 3 || v === 0)
}),
{ message: 'erased area should be transparent' }
)
.toBe(true)
}
)
test('Stroke ends cleanly when pointer up fires outside canvas', async ({
comfyPage
}) => {
const painterWidget = comfyPage.vueNodes
.getNodeLocator('1')
.locator('.widget-expands')
const canvas = painterWidget.locator('canvas')
const box = await canvas.boundingBox()
if (!box) throw new Error('Canvas bounding box not found')
@@ -125,250 +68,29 @@ test.describe('Painter', { tag: ['@widget', '@vue-nodes'] }, () => {
box.y + box.height * 0.5,
{ steps: 10 }
)
await comfyPage.page.mouse.move(box.x - 20, box.y + box.height * 0.5)
await comfyPage.page.mouse.up()
await comfyPage.nextFrame()
await expect
.poll(() => hasCanvasContent(canvas), {
message:
'canvas should have content after stroke with pointer up outside'
})
.poll(async () =>
canvas.evaluate((el) => {
const ctx = (el as HTMLCanvasElement).getContext('2d')
if (!ctx) return false
const data = ctx.getImageData(
0,
0,
(el as HTMLCanvasElement).width,
(el as HTMLCanvasElement).height
)
for (let i = 3; i < data.data.length; i += 4) {
if (data.data[i] > 0) return true
}
return false
})
)
.toBe(true)
})
})
test.describe('Tool selection', () => {
test('Tool switching toggles brush-only controls', async ({
comfyPage
}) => {
const painterWidget = comfyPage.vueNodes
.getNodeLocator('1')
.locator('.widget-expands')
await expect(painterWidget.getByTestId('painter-color-row')).toBeVisible()
await expect(
painterWidget.getByTestId('painter-hardness-row')
).toBeVisible()
await painterWidget.getByRole('button', { name: 'Eraser' }).click()
await expect(
painterWidget.getByTestId('painter-color-row'),
'color row should be hidden in eraser mode'
).toBeHidden()
await expect(
painterWidget.getByTestId('painter-hardness-row')
).toBeHidden()
await painterWidget.getByRole('button', { name: 'Brush' }).click()
await expect(painterWidget.getByTestId('painter-color-row')).toBeVisible()
await expect(
painterWidget.getByTestId('painter-hardness-row')
).toBeVisible()
})
})
test.describe('Brush settings', () => {
test('Size slider updates the displayed value', async ({ comfyPage }) => {
const painterWidget = comfyPage.vueNodes
.getNodeLocator('1')
.locator('.widget-expands')
const sizeRow = painterWidget.getByTestId('painter-size-row')
const sizeSlider = sizeRow.getByRole('slider')
const sizeDisplay = sizeRow.getByTestId('painter-size-value')
await expect(sizeDisplay).toHaveText('20')
await sizeSlider.focus()
for (let i = 0; i < 10; i++) {
await sizeSlider.press('ArrowRight')
}
await expect(sizeDisplay).toHaveText('30')
})
test('Opacity input clamps out-of-range values', async ({ comfyPage }) => {
const painterWidget = comfyPage.vueNodes
.getNodeLocator('1')
.locator('.widget-expands')
const opacityInput = painterWidget
.getByTestId('painter-color-row')
.locator('input[type="number"]')
await opacityInput.fill('150')
await opacityInput.press('Tab')
await expect(opacityInput).toHaveValue('100')
await opacityInput.fill('-10')
await opacityInput.press('Tab')
await expect(opacityInput).toHaveValue('0')
})
})
test.describe('Canvas size controls', () => {
test('Width and height sliders visible without connected input', async ({
comfyPage
}) => {
const painterWidget = comfyPage.vueNodes
.getNodeLocator('1')
.locator('.widget-expands')
await expect(painterWidget.getByTestId('painter-width-row')).toBeVisible()
await expect(
painterWidget.getByTestId('painter-height-row')
).toBeVisible()
await expect(
painterWidget.getByTestId('painter-dimension-text')
).toBeHidden()
})
test('Width slider resizes the canvas element', async ({ comfyPage }) => {
const painterWidget = comfyPage.vueNodes
.getNodeLocator('1')
.locator('.widget-expands')
const canvas = painterWidget.locator('canvas')
const widthSlider = painterWidget
.getByTestId('painter-width-row')
.getByRole('slider')
const initialWidth = await canvas.evaluate(
(el: HTMLCanvasElement) => el.width
)
expect(initialWidth, 'canvas should start at default width').toBe(512)
await widthSlider.focus()
await widthSlider.press('ArrowRight')
await expect
.poll(() => canvas.evaluate((el: HTMLCanvasElement) => el.width))
.toBe(576)
})
test(
'Resize preserves existing drawing',
{ tag: ['@smoke', '@screenshot'] },
async ({ comfyPage }) => {
const node = comfyPage.vueNodes.getNodeLocator('1')
const painterWidget = node.locator('.widget-expands')
const canvas = painterWidget.locator('canvas')
const widthSlider = painterWidget
.getByTestId('painter-width-row')
.getByRole('slider')
await drawStroke(comfyPage.page, canvas)
await expect
.poll(() => hasCanvasContent(canvas), {
message: 'canvas must have content before resize'
})
.toBe(true)
await widthSlider.focus()
await widthSlider.press('ArrowRight')
await expect
.poll(() => canvas.evaluate((el: HTMLCanvasElement) => el.width))
.toBe(576)
await expect.poll(() => hasCanvasContent(canvas)).toBe(true)
await expect(node).toHaveScreenshot('painter-after-resize.png')
}
)
})
test.describe('Clear', () => {
test(
'Clear removes all drawn content',
{ tag: '@smoke' },
async ({ comfyPage }) => {
const painterWidget = comfyPage.vueNodes
.getNodeLocator('1')
.locator('.widget-expands')
const canvas = painterWidget.locator('canvas')
await drawStroke(comfyPage.page, canvas)
await expect
.poll(() => hasCanvasContent(canvas), {
message: 'canvas must have content before clear'
})
.toBe(true)
const clearButton = painterWidget.getByTestId('painter-clear-button')
await clearButton.dispatchEvent('click')
await expect
.poll(() => hasCanvasContent(canvas), {
message: 'canvas should be clear after click'
})
.toBe(false)
}
)
})
test.describe('Serialization', () => {
test('Drawing triggers upload on serialization', async ({ comfyPage }) => {
const mockUploadResponse: UploadImageResponse = {
name: 'painter-test.png'
}
let uploadCount = 0
await comfyPage.page.route('**/upload/image', async (route) => {
uploadCount++
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockUploadResponse)
})
})
const canvas = comfyPage.vueNodes
.getNodeLocator('1')
.locator('.widget-expands canvas')
await drawStroke(comfyPage.page, canvas)
await triggerSerialization(comfyPage.page)
expect(uploadCount, 'should upload exactly once').toBe(1)
})
test('Empty canvas does not upload on serialization', async ({
comfyPage
}) => {
let uploadCount = 0
await comfyPage.page.route('**/upload/image', async (route) => {
uploadCount++
const mockResponse: UploadImageResponse = { name: 'painter-test.png' }
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockResponse)
})
})
await triggerSerialization(comfyPage.page)
expect(uploadCount, 'empty canvas should not upload').toBe(0)
})
test('Upload failure shows error toast', async ({ comfyPage }) => {
await comfyPage.page.route('**/upload/image', async (route) => {
await route.fulfill({ status: 500 })
})
const canvas = comfyPage.vueNodes
.getNodeLocator('1')
.locator('.widget-expands canvas')
await drawStroke(comfyPage.page, canvas)
await expect(triggerSerialization(comfyPage.page)).rejects.toThrow()
await expect(comfyPage.toast.visibleToasts.first()).toBeVisible()
})
})
await expect(node).toHaveScreenshot('painter-after-stroke.png')
}
)
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -2,53 +2,53 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
test.describe(
'Paste Image context menu option',
{ tag: ['@node', '@vue-nodes'] },
() => {
test('shows Paste Image in LoadImage node context menu', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow('widgets/load_image_widget')
test.describe('Paste Image context menu option', { tag: ['@node'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
})
const loadImageNode = (
await comfyPage.nodeOps.getNodeRefsByType('LoadImage')
)[0]
test('shows Paste Image in LoadImage node context menu', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow('widgets/load_image_widget')
const nodeEl = comfyPage.page.locator(
`[data-node-id="${loadImageNode.id}"]`
)
await nodeEl.click({ button: 'right' })
const menu = comfyPage.page.locator('.p-contextmenu')
await menu.waitFor({ state: 'visible' })
const menuLabels = await menu
.locator('[role="menuitem"] span.flex-1')
.allInnerTexts()
const loadImageNode = (
await comfyPage.nodeOps.getNodeRefsByType('LoadImage')
)[0]
expect(menuLabels).toContain('Paste Image')
})
const nodeEl = comfyPage.page.locator(
`[data-node-id="${loadImageNode.id}"]`
)
await nodeEl.click({ button: 'right' })
const menu = comfyPage.page.locator('.p-contextmenu')
await menu.waitFor({ state: 'visible' })
const menuLabels = await menu
.locator('[role="menuitem"] span.flex-1')
.allInnerTexts()
test('does not show Paste Image on output-only image nodes', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow('nodes/single_save_image_node')
expect(menuLabels).toContain('Paste Image')
})
const saveImageNode = (
await comfyPage.nodeOps.getNodeRefsByType('SaveImage')
)[0]
test('does not show Paste Image on output-only image nodes', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow('nodes/single_save_image_node')
const nodeEl = comfyPage.page.locator(
`[data-node-id="${saveImageNode.id}"]`
)
await nodeEl.click({ button: 'right' })
const menu = comfyPage.page.locator('.p-contextmenu')
await menu.waitFor({ state: 'visible' })
const menuLabels = await menu
.locator('[role="menuitem"] span.flex-1')
.allInnerTexts()
const saveImageNode = (
await comfyPage.nodeOps.getNodeRefsByType('SaveImage')
)[0]
expect(menuLabels).not.toContain('Paste Image')
expect(menuLabels).not.toContain('Open Image')
})
}
)
const nodeEl = comfyPage.page.locator(
`[data-node-id="${saveImageNode.id}"]`
)
await nodeEl.click({ button: 'right' })
const menu = comfyPage.page.locator('.p-contextmenu')
await menu.waitFor({ state: 'visible' })
const menuLabels = await menu
.locator('[role="menuitem"] span.flex-1')
.allInnerTexts()
expect(menuLabels).not.toContain('Paste Image')
expect(menuLabels).not.toContain('Open Image')
})
})

View File

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

View File

@@ -2,9 +2,8 @@ import { expect } from '@playwright/test'
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
import { TestIds } from '@e2e/fixtures/selectors'
import { PropertiesPanelHelper } from '@e2e/tests/propertiesPanel/PropertiesPanelHelper'
export async function loadWorkflowAndOpenErrorsTab(
export async function openErrorsTabViaSeeErrors(
comfyPage: ComfyPage,
workflow: string
) {
@@ -14,32 +13,5 @@ export async function loadWorkflowAndOpenErrorsTab(
await expect(errorOverlay).toBeVisible()
await errorOverlay.getByTestId(TestIds.dialogs.errorOverlaySeeErrors).click()
await expect(errorOverlay).toBeHidden()
}
export async function openErrorsTab(comfyPage: ComfyPage) {
const panel = new PropertiesPanelHelper(comfyPage.page)
await panel.open(comfyPage.actionbar.propertiesButton)
const errorsTab = comfyPage.page.getByTestId(
TestIds.propertiesPanel.errorsTab
)
await expect(errorsTab).toBeVisible()
await errorsTab.click()
}
/**
* Remove the fake model file from the backend so it is detected as missing.
* Fixture URLs (e.g. http://localhost:8188/...) are not actually downloaded
* during tests — they only serve as metadata for the missing model UI.
*/
export async function cleanupFakeModel(comfyPage: ComfyPage) {
await expect
.poll(() =>
comfyPage.page.evaluate(async (url: string) => {
const response = await fetch(`${url}/api/devtools/cleanup_fake_model`)
return response.ok
}, comfyPage.url)
)
.toBeTruthy()
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

@@ -6,6 +6,7 @@ import { PropertiesPanelHelper } from '@e2e/tests/propertiesPanel/PropertiesPane
test.describe('Errors tab - common', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting(
'Comfy.RightSidePanel.ShowErrorsTab',
true
@@ -32,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()
})
})
@@ -54,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

@@ -7,6 +7,7 @@ import { TestIds } from '@e2e/fixtures/selectors'
test.describe('Errors tab - Execution errors', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting(
'Comfy.RightSidePanel.ShowErrorsTab',
true
@@ -25,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

@@ -3,7 +3,7 @@ import { expect } from '@playwright/test'
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
import { TestIds } from '@e2e/fixtures/selectors'
import { loadWorkflowAndOpenErrorsTab } from '@e2e/tests/propertiesPanel/ErrorsTabHelper'
import { openErrorsTabViaSeeErrors } from '@e2e/tests/propertiesPanel/ErrorsTabHelper'
async function uploadFileViaDropzone(comfyPage: ComfyPage) {
const dropzone = comfyPage.page.getByTestId(
@@ -38,6 +38,7 @@ function getDropzone(comfyPage: ComfyPage) {
test.describe('Errors tab - Missing media', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting(
'Comfy.RightSidePanel.ShowErrorsTab',
true
@@ -46,10 +47,7 @@ test.describe('Errors tab - Missing media', { tag: '@ui' }, () => {
test.describe('Detection', () => {
test('Shows missing media group in errors tab', async ({ comfyPage }) => {
await loadWorkflowAndOpenErrorsTab(
comfyPage,
'missing/missing_media_single'
)
await openErrorsTabViaSeeErrors(comfyPage, 'missing/missing_media_single')
await expect(
comfyPage.page.getByTestId(TestIds.dialogs.missingMediaGroup)
@@ -59,7 +57,7 @@ test.describe('Errors tab - Missing media', { tag: '@ui' }, () => {
test('Shows correct number of missing media rows', async ({
comfyPage
}) => {
await loadWorkflowAndOpenErrorsTab(
await openErrorsTabViaSeeErrors(
comfyPage,
'missing/missing_media_multiple'
)
@@ -70,10 +68,7 @@ test.describe('Errors tab - Missing media', { tag: '@ui' }, () => {
test('Shows upload dropzone and library select for each missing item', async ({
comfyPage
}) => {
await loadWorkflowAndOpenErrorsTab(
comfyPage,
'missing/missing_media_single'
)
await openErrorsTabViaSeeErrors(comfyPage, 'missing/missing_media_single')
await expect(getDropzone(comfyPage)).toBeVisible()
await expect(
@@ -86,10 +81,7 @@ test.describe('Errors tab - Missing media', { tag: '@ui' }, () => {
test('Upload via file picker shows status card then allows confirm', async ({
comfyPage
}) => {
await loadWorkflowAndOpenErrorsTab(
comfyPage,
'missing/missing_media_single'
)
await openErrorsTabViaSeeErrors(comfyPage, 'missing/missing_media_single')
await uploadFileViaDropzone(comfyPage)
await expect(getStatusCard(comfyPage)).toBeVisible()
@@ -103,10 +95,7 @@ test.describe('Errors tab - Missing media', { tag: '@ui' }, () => {
test('Selecting from library shows status card then allows confirm', async ({
comfyPage
}) => {
await loadWorkflowAndOpenErrorsTab(
comfyPage,
'missing/missing_media_single'
)
await openErrorsTabViaSeeErrors(comfyPage, 'missing/missing_media_single')
const librarySelect = comfyPage.page.getByTestId(
TestIds.dialogs.missingMediaLibrarySelect
@@ -115,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
}
@@ -133,20 +121,17 @@ test.describe('Errors tab - Missing media', { tag: '@ui' }, () => {
test('Cancelling pending selection returns to upload/library UI', async ({
comfyPage
}) => {
await loadWorkflowAndOpenErrorsTab(
comfyPage,
'missing/missing_media_single'
)
await openErrorsTabViaSeeErrors(comfyPage, 'missing/missing_media_single')
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()
})
})
@@ -155,16 +140,13 @@ test.describe('Errors tab - Missing media', { tag: '@ui' }, () => {
test('Missing Inputs group disappears when all items are resolved', async ({
comfyPage
}) => {
await loadWorkflowAndOpenErrorsTab(
comfyPage,
'missing/missing_media_single'
)
await openErrorsTabViaSeeErrors(comfyPage, 'missing/missing_media_single')
await uploadFileViaDropzone(comfyPage)
await confirmPendingSelection(comfyPage)
await expect(
comfyPage.page.getByTestId(TestIds.dialogs.missingMediaGroup)
).toBeHidden()
).not.toBeVisible()
})
})
@@ -172,10 +154,7 @@ test.describe('Errors tab - Missing media', { tag: '@ui' }, () => {
test('Locate button navigates canvas to the missing media node', async ({
comfyPage
}) => {
await loadWorkflowAndOpenErrorsTab(
comfyPage,
'missing/missing_media_single'
)
await openErrorsTabViaSeeErrors(comfyPage, 'missing/missing_media_single')
const offsetBefore = await comfyPage.page.evaluate(() => {
const canvas = window['app']?.canvas

View File

@@ -6,24 +6,29 @@ import {
interceptClipboardWrite,
getClipboardText
} from '@e2e/helpers/clipboardSpy'
import {
cleanupFakeModel,
loadWorkflowAndOpenErrorsTab
} from '@e2e/tests/propertiesPanel/ErrorsTabHelper'
import { openErrorsTabViaSeeErrors } from '@e2e/tests/propertiesPanel/ErrorsTabHelper'
test.describe('Errors tab - Missing models', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting(
'Comfy.RightSidePanel.ShowErrorsTab',
true
)
await cleanupFakeModel(comfyPage)
await expect
.poll(async () => {
return await comfyPage.page.evaluate(async (url: string) => {
const response = await fetch(`${url}/api/devtools/cleanup_fake_model`)
return response.ok
}, comfyPage.url)
})
.toBeTruthy()
})
test('Should show missing models group in errors tab', async ({
comfyPage
}) => {
await loadWorkflowAndOpenErrorsTab(comfyPage, 'missing/missing_models')
await openErrorsTabViaSeeErrors(comfyPage, 'missing/missing_models')
await expect(
comfyPage.page.getByTestId(TestIds.dialogs.missingModelsGroup)
@@ -33,7 +38,7 @@ test.describe('Errors tab - Missing models', { tag: '@ui' }, () => {
test('Should display model name with referencing node count', async ({
comfyPage
}) => {
await loadWorkflowAndOpenErrorsTab(comfyPage, 'missing/missing_models')
await openErrorsTabViaSeeErrors(comfyPage, 'missing/missing_models')
const modelsGroup = comfyPage.page.getByTestId(
TestIds.dialogs.missingModelsGroup
@@ -44,7 +49,7 @@ test.describe('Errors tab - Missing models', { tag: '@ui' }, () => {
test('Should expand model row to show referencing nodes', async ({
comfyPage
}) => {
await loadWorkflowAndOpenErrorsTab(
await openErrorsTabViaSeeErrors(
comfyPage,
'missing/missing_models_with_nodes'
)
@@ -52,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
@@ -64,14 +69,14 @@ test.describe('Errors tab - Missing models', { tag: '@ui' }, () => {
})
test('Should copy model name to clipboard', async ({ comfyPage }) => {
await loadWorkflowAndOpenErrorsTab(comfyPage, 'missing/missing_models')
await openErrorsTabViaSeeErrors(comfyPage, 'missing/missing_models')
await interceptClipboardWrite(comfyPage.page)
const copyButton = comfyPage.page.getByTestId(
TestIds.dialogs.missingModelCopyName
)
await expect(copyButton.first()).toBeVisible()
await copyButton.first().dispatchEvent('click')
await copyButton.first().click()
const copiedText = await getClipboardText(comfyPage.page)
expect(copiedText).toContain('fake_model.safetensors')
@@ -81,7 +86,7 @@ test.describe('Errors tab - Missing models', { tag: '@ui' }, () => {
test('Should show Copy URL button for non-asset models', async ({
comfyPage
}) => {
await loadWorkflowAndOpenErrorsTab(comfyPage, 'missing/missing_models')
await openErrorsTabViaSeeErrors(comfyPage, 'missing/missing_models')
const copyUrlButton = comfyPage.page.getByTestId(
TestIds.dialogs.missingModelCopyUrl
@@ -92,7 +97,7 @@ test.describe('Errors tab - Missing models', { tag: '@ui' }, () => {
test('Should show Download button for downloadable models', async ({
comfyPage
}) => {
await loadWorkflowAndOpenErrorsTab(comfyPage, 'missing/missing_models')
await openErrorsTabViaSeeErrors(comfyPage, 'missing/missing_models')
const downloadButton = comfyPage.page.getByTestId(
TestIds.dialogs.missingModelDownload

View File

@@ -2,10 +2,11 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
import { TestIds } from '@e2e/fixtures/selectors'
import { loadWorkflowAndOpenErrorsTab } from '@e2e/tests/propertiesPanel/ErrorsTabHelper'
import { openErrorsTabViaSeeErrors } from '@e2e/tests/propertiesPanel/ErrorsTabHelper'
test.describe('Errors tab - Missing nodes', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting(
'Comfy.RightSidePanel.ShowErrorsTab',
true
@@ -13,7 +14,7 @@ test.describe('Errors tab - Missing nodes', { tag: '@ui' }, () => {
})
test('Should show MissingNodeCard in errors tab', async ({ comfyPage }) => {
await loadWorkflowAndOpenErrorsTab(comfyPage, 'missing/missing_nodes')
await openErrorsTabViaSeeErrors(comfyPage, 'missing/missing_nodes')
await expect(
comfyPage.page.getByTestId(TestIds.dialogs.missingNodeCard)
@@ -21,7 +22,7 @@ test.describe('Errors tab - Missing nodes', { tag: '@ui' }, () => {
})
test('Should show missing node packs group', async ({ comfyPage }) => {
await loadWorkflowAndOpenErrorsTab(comfyPage, 'missing/missing_nodes')
await openErrorsTabViaSeeErrors(comfyPage, 'missing/missing_nodes')
await expect(
comfyPage.page.getByTestId(TestIds.dialogs.missingNodePacksGroup)
@@ -31,7 +32,7 @@ test.describe('Errors tab - Missing nodes', { tag: '@ui' }, () => {
test('Should expand pack group to reveal node type names', async ({
comfyPage
}) => {
await loadWorkflowAndOpenErrorsTab(
await openErrorsTabViaSeeErrors(
comfyPage,
'missing/missing_nodes_in_subgraph'
)
@@ -51,7 +52,7 @@ test.describe('Errors tab - Missing nodes', { tag: '@ui' }, () => {
})
test('Should collapse expanded pack group', async ({ comfyPage }) => {
await loadWorkflowAndOpenErrorsTab(
await openErrorsTabViaSeeErrors(
comfyPage,
'missing/missing_nodes_in_subgraph'
)
@@ -73,13 +74,13 @@ 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 ({
comfyPage
}) => {
await loadWorkflowAndOpenErrorsTab(
await openErrorsTabViaSeeErrors(
comfyPage,
'missing/missing_nodes_in_subgraph'
)

View File

@@ -1,519 +0,0 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
import { TestIds } from '@e2e/fixtures/selectors'
import {
cleanupFakeModel,
openErrorsTab,
loadWorkflowAndOpenErrorsTab
} from '@e2e/tests/propertiesPanel/ErrorsTabHelper'
test.describe('Errors tab - Mode-aware errors', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting(
'Comfy.RightSidePanel.ShowErrorsTab',
true
)
})
test.describe('Missing nodes', () => {
test('Deleting a missing node removes its error from the errors tab', async ({
comfyPage
}) => {
await loadWorkflowAndOpenErrorsTab(comfyPage, 'missing/missing_nodes')
const missingNodeGroup = comfyPage.page.getByTestId(
TestIds.dialogs.missingNodePacksGroup
)
await expect(missingNodeGroup).toBeVisible()
const node = await comfyPage.nodeOps.getNodeRefById('1')
await node.delete()
await expect(missingNodeGroup).toBeHidden()
})
test('Undo after bypass restores error without showing overlay', async ({
comfyPage
}) => {
await loadWorkflowAndOpenErrorsTab(comfyPage, 'missing/missing_nodes')
const missingNodeGroup = comfyPage.page.getByTestId(
TestIds.dialogs.missingNodePacksGroup
)
const errorOverlay = comfyPage.page.getByTestId(
TestIds.dialogs.errorOverlay
)
await expect(missingNodeGroup).toBeVisible()
const node = await comfyPage.nodeOps.getNodeRefById('1')
await node.click('title')
await comfyPage.keyboard.bypass()
await expect.poll(() => node.isBypassed()).toBeTruthy()
await expect(missingNodeGroup).toBeHidden()
await comfyPage.keyboard.undo()
await expect.poll(() => node.isBypassed()).toBeFalsy()
await expect(errorOverlay).toBeHidden()
await openErrorsTab(comfyPage)
await expect(missingNodeGroup).toBeVisible()
await comfyPage.keyboard.redo()
await expect.poll(() => node.isBypassed()).toBeTruthy()
await expect(missingNodeGroup).toBeHidden()
})
})
test.describe('Missing models', () => {
test.beforeEach(async ({ comfyPage }) => {
await cleanupFakeModel(comfyPage)
})
test.afterEach(async ({ comfyPage }) => {
await cleanupFakeModel(comfyPage)
})
test('Loading a workflow with all nodes bypassed shows no errors', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow('missing/missing_models_bypassed')
const errorOverlay = comfyPage.page.getByTestId(
TestIds.dialogs.errorOverlay
)
await expect(errorOverlay).toBeHidden()
await comfyPage.actionbar.propertiesButton.click()
await expect(
comfyPage.page.getByTestId(TestIds.propertiesPanel.errorsTab)
).toBeHidden()
})
test('Bypassing a node hides its error, un-bypassing restores it', async ({
comfyPage
}) => {
await loadWorkflowAndOpenErrorsTab(comfyPage, 'missing/missing_models')
const missingModelGroup = comfyPage.page.getByTestId(
TestIds.dialogs.missingModelsGroup
)
await expect(missingModelGroup).toBeVisible()
const node = await comfyPage.nodeOps.getNodeRefById('1')
await node.click('title')
await comfyPage.keyboard.bypass()
await expect.poll(() => node.isBypassed()).toBeTruthy()
await expect(missingModelGroup).toBeHidden()
await node.click('title')
await comfyPage.keyboard.bypass()
await expect.poll(() => node.isBypassed()).toBeFalsy()
await openErrorsTab(comfyPage)
await expect(missingModelGroup).toBeVisible()
})
test('Pasting a node with missing model increases referencing node count', async ({
comfyPage
}) => {
await loadWorkflowAndOpenErrorsTab(comfyPage, 'missing/missing_models')
const missingModelGroup = comfyPage.page.getByTestId(
TestIds.dialogs.missingModelsGroup
)
await expect(missingModelGroup).toBeVisible()
await expect(missingModelGroup).toContainText(
/fake_model\.safetensors\s*\(1\)/
)
const node = await comfyPage.nodeOps.getNodeRefById('1')
await node.click('title')
await comfyPage.clipboard.copy()
await comfyPage.clipboard.paste()
await expect.poll(() => comfyPage.nodeOps.getNodeCount()).toBe(2)
await comfyPage.canvas.click()
await expect(missingModelGroup).toContainText(
/fake_model\.safetensors\s*\(2\)/
)
})
test('Pasting a bypassed node does not add a new error', async ({
comfyPage
}) => {
await loadWorkflowAndOpenErrorsTab(comfyPage, 'missing/missing_models')
const missingModelGroup = comfyPage.page.getByTestId(
TestIds.dialogs.missingModelsGroup
)
const node = await comfyPage.nodeOps.getNodeRefById('1')
await node.click('title')
await comfyPage.keyboard.bypass()
await expect.poll(() => node.isBypassed()).toBeTruthy()
await expect(missingModelGroup).toBeHidden()
await comfyPage.clipboard.copy()
await comfyPage.clipboard.paste()
await expect.poll(() => comfyPage.nodeOps.getNodeCount()).toBe(2)
await expect(missingModelGroup).toBeHidden()
})
test('Deleting a node with missing model removes its error', async ({
comfyPage
}) => {
await loadWorkflowAndOpenErrorsTab(comfyPage, 'missing/missing_models')
const missingModelGroup = comfyPage.page.getByTestId(
TestIds.dialogs.missingModelsGroup
)
await expect(missingModelGroup).toBeVisible()
const node = await comfyPage.nodeOps.getNodeRefById('1')
await node.delete()
await expect(missingModelGroup).toBeHidden()
})
test('Undo after bypass restores error without showing overlay', async ({
comfyPage
}) => {
await loadWorkflowAndOpenErrorsTab(comfyPage, 'missing/missing_models')
const missingModelGroup = comfyPage.page.getByTestId(
TestIds.dialogs.missingModelsGroup
)
const errorOverlay = comfyPage.page.getByTestId(
TestIds.dialogs.errorOverlay
)
await expect(missingModelGroup).toBeVisible()
const node = await comfyPage.nodeOps.getNodeRefById('1')
await node.click('title')
await comfyPage.keyboard.bypass()
await expect.poll(() => node.isBypassed()).toBeTruthy()
await expect(missingModelGroup).toBeHidden()
await comfyPage.keyboard.undo()
await expect.poll(() => node.isBypassed()).toBeFalsy()
await expect(errorOverlay).toBeHidden()
await openErrorsTab(comfyPage)
await expect(missingModelGroup).toBeVisible()
await comfyPage.keyboard.redo()
await expect.poll(() => node.isBypassed()).toBeTruthy()
await expect(missingModelGroup).toBeHidden()
})
test('Selecting a node filters errors tab to only that node', async ({
comfyPage
}) => {
await loadWorkflowAndOpenErrorsTab(
comfyPage,
'missing/missing_models_with_nodes'
)
const missingModelGroup = comfyPage.page.getByTestId(
TestIds.dialogs.missingModelsGroup
)
await expect(missingModelGroup).toContainText(/\(2\)/)
const node1 = await comfyPage.nodeOps.getNodeRefById('1')
await node1.click('title')
await expect(missingModelGroup).toContainText(/\(1\)/)
await comfyPage.canvas.click()
await expect(missingModelGroup).toContainText(/\(2\)/)
})
})
test.describe('Missing media', () => {
test('Loading a workflow with all nodes bypassed shows no errors', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow('missing/missing_media_bypassed')
const errorOverlay = comfyPage.page.getByTestId(
TestIds.dialogs.errorOverlay
)
await expect(errorOverlay).toBeHidden()
await comfyPage.actionbar.propertiesButton.click()
await expect(
comfyPage.page.getByTestId(TestIds.propertiesPanel.errorsTab)
).toBeHidden()
})
test('Bypassing a node hides its error, un-bypassing restores it', async ({
comfyPage
}) => {
await loadWorkflowAndOpenErrorsTab(
comfyPage,
'missing/missing_media_single'
)
const missingMediaGroup = comfyPage.page.getByTestId(
TestIds.dialogs.missingMediaGroup
)
await expect(missingMediaGroup).toBeVisible()
const node = await comfyPage.nodeOps.getNodeRefById('10')
await node.click('title')
await comfyPage.keyboard.bypass()
await expect.poll(() => node.isBypassed()).toBeTruthy()
await expect(missingMediaGroup).toBeHidden()
await node.click('title')
await comfyPage.keyboard.bypass()
await expect.poll(() => node.isBypassed()).toBeFalsy()
await openErrorsTab(comfyPage)
await expect(missingMediaGroup).toBeVisible()
})
test('Pasting a bypassed node does not add a new error', async ({
comfyPage
}) => {
await loadWorkflowAndOpenErrorsTab(
comfyPage,
'missing/missing_media_single'
)
const missingMediaGroup = comfyPage.page.getByTestId(
TestIds.dialogs.missingMediaGroup
)
const node = await comfyPage.nodeOps.getNodeRefById('10')
await node.click('title')
await comfyPage.keyboard.bypass()
await expect.poll(() => node.isBypassed()).toBeTruthy()
await expect(missingMediaGroup).toBeHidden()
await comfyPage.clipboard.copy()
await comfyPage.clipboard.paste()
await expect.poll(() => comfyPage.nodeOps.getNodeCount()).toBe(2)
await expect(missingMediaGroup).toBeHidden()
})
test('Selecting a node filters errors tab to only that node', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow('missing/missing_media_multiple')
const errorOverlay = comfyPage.page.getByTestId(
TestIds.dialogs.errorOverlay
)
await expect(errorOverlay).toBeVisible()
await errorOverlay
.getByTestId(TestIds.dialogs.errorOverlayDismiss)
.click()
const mediaRows = comfyPage.page.getByTestId(
TestIds.dialogs.missingMediaRow
)
await openErrorsTab(comfyPage)
await expect(mediaRows).toHaveCount(2)
const node = await comfyPage.nodeOps.getNodeRefById('10')
await node.click('title')
await expect(mediaRows).toHaveCount(1)
await comfyPage.canvas.click({ position: { x: 400, y: 600 } })
await expect(mediaRows).toHaveCount(2)
})
})
test.describe('Subgraph', () => {
test.beforeEach(async ({ comfyPage }) => {
await cleanupFakeModel(comfyPage)
})
test.afterEach(async ({ comfyPage }) => {
await cleanupFakeModel(comfyPage)
})
test('Bypassing a subgraph hides interior errors, un-bypassing restores them', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow(
'missing/missing_models_in_subgraph'
)
const errorOverlay = comfyPage.page.getByTestId(
TestIds.dialogs.errorOverlay
)
await expect(errorOverlay).toBeVisible()
await errorOverlay
.getByTestId(TestIds.dialogs.errorOverlayDismiss)
.click()
const missingModelGroup = comfyPage.page.getByTestId(
TestIds.dialogs.missingModelsGroup
)
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2')
const errorsTab = comfyPage.page.getByTestId(
TestIds.propertiesPanel.errorsTab
)
await comfyPage.keyboard.selectAll()
await comfyPage.keyboard.bypass()
await expect.poll(() => subgraphNode.isBypassed()).toBeTruthy()
await comfyPage.actionbar.propertiesButton.click()
await expect(errorsTab).toBeHidden()
await comfyPage.keyboard.selectAll()
await comfyPage.keyboard.bypass()
await expect.poll(() => subgraphNode.isBypassed()).toBeFalsy()
await openErrorsTab(comfyPage)
await expect(missingModelGroup).toBeVisible()
})
test('Deleting a node inside a subgraph removes its missing model error', async ({
comfyPage
}) => {
// Regression: before the execId fix, onNodeRemoved fell back to the
// interior node's local id (e.g. "1") when node.graph was already
// null, so the error keyed under "2:1" was never removed.
await comfyPage.workflow.loadWorkflow(
'missing/missing_models_in_subgraph'
)
const errorOverlay = comfyPage.page.getByTestId(
TestIds.dialogs.errorOverlay
)
await expect(errorOverlay).toBeVisible()
await errorOverlay
.getByTestId(TestIds.dialogs.errorOverlayDismiss)
.click()
const missingModelGroup = comfyPage.page.getByTestId(
TestIds.dialogs.missingModelsGroup
)
await openErrorsTab(comfyPage)
await expect(missingModelGroup).toBeVisible()
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2')
await subgraphNode.navigateIntoSubgraph()
// Select-all + Delete: interior node IDs may be reassigned during
// subgraph configure when they collide with root-graph IDs, so
// looking up by static id can fail.
await comfyPage.keyboard.selectAll()
await comfyPage.page.keyboard.press('Delete')
await expect(missingModelGroup).toBeHidden()
})
test('Deleting a node inside a subgraph removes its missing node-type error', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow('missing/missing_nodes_in_subgraph')
const errorOverlay = comfyPage.page.getByTestId(
TestIds.dialogs.errorOverlay
)
await expect(errorOverlay).toBeVisible()
await errorOverlay
.getByTestId(TestIds.dialogs.errorOverlayDismiss)
.click()
const missingNodeGroup = comfyPage.page.getByTestId(
TestIds.dialogs.missingNodePacksGroup
)
await openErrorsTab(comfyPage)
await expect(missingNodeGroup).toBeVisible()
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2')
await subgraphNode.navigateIntoSubgraph()
// Select-all + Delete: interior node IDs may be reassigned during
// subgraph configure when they collide with root-graph IDs, so
// looking up by static id can fail.
await comfyPage.keyboard.selectAll()
await comfyPage.page.keyboard.press('Delete')
await expect(missingNodeGroup).toBeHidden()
})
test('Bypassing a node inside a subgraph hides its error, un-bypassing restores it', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow(
'missing/missing_models_in_subgraph'
)
const errorOverlay = comfyPage.page.getByTestId(
TestIds.dialogs.errorOverlay
)
await expect(errorOverlay).toBeVisible()
await errorOverlay
.getByTestId(TestIds.dialogs.errorOverlayDismiss)
.click()
const missingModelGroup = comfyPage.page.getByTestId(
TestIds.dialogs.missingModelsGroup
)
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2')
await subgraphNode.navigateIntoSubgraph()
await comfyPage.keyboard.selectAll()
await comfyPage.keyboard.bypass()
const errorsTab = comfyPage.page.getByTestId(
TestIds.propertiesPanel.errorsTab
)
await comfyPage.actionbar.propertiesButton.click()
await expect(errorsTab).toBeHidden()
await comfyPage.keyboard.selectAll()
await comfyPage.keyboard.bypass()
await openErrorsTab(comfyPage)
await expect(missingModelGroup).toBeVisible()
})
})
test.describe('Workflow switching', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting(
'Comfy.Workflow.WorkflowTabsPosition',
'Sidebar'
)
await comfyPage.menu.workflowsTab.open()
})
test('Restores missing nodes in errors tab when switching back to workflow', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow('missing/missing_nodes')
const errorOverlay = comfyPage.page.getByTestId(
TestIds.dialogs.errorOverlay
)
await expect(errorOverlay).toBeVisible()
await errorOverlay
.getByTestId(TestIds.dialogs.errorOverlayDismiss)
.click()
const missingNodeGroup = comfyPage.page.getByTestId(
TestIds.dialogs.missingNodePacksGroup
)
await openErrorsTab(comfyPage)
await expect(missingNodeGroup).toBeVisible()
await comfyPage.menu.workflowsTab.open()
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
await expect(missingNodeGroup).toBeHidden()
await comfyPage.menu.workflowsTab.switchToWorkflow('missing_nodes')
await openErrorsTab(comfyPage)
await expect(missingNodeGroup).toBeVisible()
})
})
})

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

@@ -3,11 +3,13 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
import { PropertiesPanelHelper } from '@e2e/tests/propertiesPanel/PropertiesPanelHelper'
test.describe('Properties panel - Node settings', { tag: '@vue-nodes' }, () => {
test.describe('Properties panel - Node settings', () => {
let panel: PropertiesPanelHelper
test.beforeEach(async ({ comfyPage }) => {
panel = new PropertiesPanelHelper(comfyPage.page)
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()
await comfyPage.actionbar.propertiesButton.click()
await comfyPage.nodeOps.selectNodes(['KSampler'])
await panel.switchToTab('Settings')
@@ -40,8 +42,8 @@ test.describe('Properties panel - Node settings', { tag: '@vue-nodes' }, () => {
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()
})
})
@@ -112,7 +114,9 @@ test.describe('Properties panel - Node settings', { tag: '@vue-nodes' }, () => {
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()
})
})

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