mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
## Summary Extract repeated patterns from 12 subgraph Playwright spec files into shared test utilities, reducing duplication by ~142 lines. ## Changes - **What**: New shared helpers for common subgraph test operations: - `SubgraphHelper`: `getSlotCount()`, `getSlotLabel()`, `removeSlot()`, `findSubgraphNodeId()` - `NodeReference`: `delete()` - `subgraphTestUtils`: `serializeAndReload()`, `convertDefaultKSamplerToSubgraph()`, `expectWidgetBelowHeader()`, `collectConsoleWarnings()`, `packAllInteriorNodes()` - Replaced ~72 inline `page.evaluate` blocks and multi-line sequences with single helper calls across 12 spec files ## Review Focus - Behavioral equivalence: every replacement is a mechanical extraction with no test logic changes - API surface of new helpers: naming, parameter types, placement in existing utility classes - Whether any remaining inline patterns in the spec files would benefit from further extraction ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10629-test-extract-shared-subgraph-E2E-test-utilities-3306d73d365081b0b6b5db52ed0a4552) by [Unito](https://www.unito.io) --------- Co-authored-by: Amp <amp@ampcode.com>
324 lines
10 KiB
TypeScript
324 lines
10 KiB
TypeScript
import { expect } from '@playwright/test'
|
|
|
|
import type { ComfyPage } from '../fixtures/ComfyPage'
|
|
import type { PromotedWidgetEntry } from '../helpers/promotedWidgets'
|
|
|
|
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
|
import { TestIds } from '../fixtures/selectors'
|
|
import {
|
|
getPromotedWidgets,
|
|
getPseudoPreviewWidgets,
|
|
getNonPreviewPromotedWidgets
|
|
} from '../helpers/promotedWidgets'
|
|
|
|
const domPreviewSelector = '.image-preview'
|
|
|
|
const expectPromotedWidgetsToResolveToInteriorNodes = async (
|
|
comfyPage: ComfyPage,
|
|
hostSubgraphNodeId: string,
|
|
widgets: PromotedWidgetEntry[]
|
|
) => {
|
|
const interiorNodeIds = widgets.map(([id]) => id)
|
|
const results = await comfyPage.page.evaluate(
|
|
([hostId, ids]) => {
|
|
const graph = window.app!.graph!
|
|
const hostNode = graph.getNodeById(Number(hostId))
|
|
if (!hostNode?.isSubgraphNode()) return ids.map(() => false)
|
|
|
|
return ids.map((id) => {
|
|
const interiorNode = hostNode.subgraph.getNodeById(Number(id))
|
|
return interiorNode !== null && interiorNode !== undefined
|
|
})
|
|
},
|
|
[hostSubgraphNodeId, interiorNodeIds] as const
|
|
)
|
|
|
|
for (const exists of results) {
|
|
expect(exists).toBe(true)
|
|
}
|
|
}
|
|
|
|
test.describe(
|
|
'Subgraph Lifecycle Edge Behaviors',
|
|
{ tag: ['@subgraph'] },
|
|
() => {
|
|
test.describe('Deterministic Hydrate from Serialized proxyWidgets', () => {
|
|
test('proxyWidgets entries map to real interior node IDs after load', async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.workflow.loadWorkflow(
|
|
'subgraphs/subgraph-with-promoted-text-widget'
|
|
)
|
|
await comfyPage.nextFrame()
|
|
|
|
const widgets = await getPromotedWidgets(comfyPage, '11')
|
|
expect(widgets.length).toBeGreaterThan(0)
|
|
|
|
for (const [interiorNodeId] of widgets) {
|
|
expect(Number(interiorNodeId)).toBeGreaterThan(0)
|
|
}
|
|
|
|
await expectPromotedWidgetsToResolveToInteriorNodes(
|
|
comfyPage,
|
|
'11',
|
|
widgets
|
|
)
|
|
})
|
|
|
|
test('proxyWidgets entries survive double round-trip without drift', async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.workflow.loadWorkflow(
|
|
'subgraphs/subgraph-with-multiple-promoted-widgets'
|
|
)
|
|
await comfyPage.nextFrame()
|
|
|
|
const initialWidgets = await getPromotedWidgets(comfyPage, '11')
|
|
expect(initialWidgets.length).toBeGreaterThan(0)
|
|
await expectPromotedWidgetsToResolveToInteriorNodes(
|
|
comfyPage,
|
|
'11',
|
|
initialWidgets
|
|
)
|
|
|
|
await comfyPage.subgraph.serializeAndReload()
|
|
|
|
const afterFirst = await getPromotedWidgets(comfyPage, '11')
|
|
await expectPromotedWidgetsToResolveToInteriorNodes(
|
|
comfyPage,
|
|
'11',
|
|
afterFirst
|
|
)
|
|
|
|
await comfyPage.subgraph.serializeAndReload()
|
|
|
|
const afterSecond = await getPromotedWidgets(comfyPage, '11')
|
|
await expectPromotedWidgetsToResolveToInteriorNodes(
|
|
comfyPage,
|
|
'11',
|
|
afterSecond
|
|
)
|
|
|
|
expect(afterFirst).toEqual(initialWidgets)
|
|
expect(afterSecond).toEqual(initialWidgets)
|
|
})
|
|
|
|
test('Compressed target_slot (-1) entries are hydrated to real IDs', async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.workflow.loadWorkflow(
|
|
'subgraphs/subgraph-compressed-target-slot'
|
|
)
|
|
await comfyPage.nextFrame()
|
|
|
|
const widgets = await getPromotedWidgets(comfyPage, '2')
|
|
expect(widgets.length).toBeGreaterThan(0)
|
|
|
|
for (const [interiorNodeId] of widgets) {
|
|
expect(interiorNodeId).not.toBe('-1')
|
|
expect(Number(interiorNodeId)).toBeGreaterThan(0)
|
|
}
|
|
|
|
await expectPromotedWidgetsToResolveToInteriorNodes(
|
|
comfyPage,
|
|
'2',
|
|
widgets
|
|
)
|
|
})
|
|
})
|
|
|
|
test.describe('Cleanup Behavior After Promoted Source Removal', () => {
|
|
test.beforeEach(async ({ comfyPage }) => {
|
|
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
|
|
})
|
|
|
|
test('Removing promoted source node inside subgraph cleans up exterior proxyWidgets', async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.workflow.loadWorkflow(
|
|
'subgraphs/subgraph-with-promoted-text-widget'
|
|
)
|
|
await comfyPage.nextFrame()
|
|
|
|
const initialWidgets = await getPromotedWidgets(comfyPage, '11')
|
|
expect(initialWidgets.length).toBeGreaterThan(0)
|
|
|
|
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('11')
|
|
await subgraphNode.navigateIntoSubgraph()
|
|
|
|
const clipNode = await comfyPage.nodeOps.getNodeRefById('10')
|
|
await clipNode.delete()
|
|
|
|
await comfyPage.subgraph.exitViaBreadcrumb()
|
|
|
|
await expect
|
|
.poll(async () => {
|
|
return await comfyPage.page.evaluate(() => {
|
|
const hostNode = window.app!.canvas.graph!.getNodeById('11')
|
|
const proxyWidgets = hostNode?.properties?.proxyWidgets
|
|
return {
|
|
proxyWidgetCount: Array.isArray(proxyWidgets)
|
|
? proxyWidgets.length
|
|
: 0,
|
|
firstWidgetType: hostNode?.widgets?.[0]?.type
|
|
}
|
|
})
|
|
})
|
|
.toEqual({
|
|
proxyWidgetCount: 0,
|
|
firstWidgetType: undefined
|
|
})
|
|
})
|
|
|
|
test('Promoted widget disappears from DOM after interior node deletion', async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.workflow.loadWorkflow(
|
|
'subgraphs/subgraph-with-promoted-text-widget'
|
|
)
|
|
await comfyPage.nextFrame()
|
|
|
|
const textarea = comfyPage.page.getByTestId(
|
|
TestIds.widgets.domWidgetTextarea
|
|
)
|
|
await expect(textarea).toBeVisible()
|
|
|
|
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('11')
|
|
await subgraphNode.navigateIntoSubgraph()
|
|
|
|
const clipNode = await comfyPage.nodeOps.getNodeRefById('10')
|
|
await clipNode.delete()
|
|
|
|
await comfyPage.subgraph.exitViaBreadcrumb()
|
|
|
|
await expect(
|
|
comfyPage.page.getByTestId(TestIds.widgets.domWidgetTextarea)
|
|
).toHaveCount(0)
|
|
})
|
|
})
|
|
|
|
test.describe('Unpack/Remove Cleanup for Pseudo-Preview Targets', () => {
|
|
test('Pseudo-preview entries exist in proxyWidgets for preview subgraph', async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.workflow.loadWorkflow(
|
|
'subgraphs/subgraph-with-preview-node'
|
|
)
|
|
await comfyPage.nextFrame()
|
|
|
|
const pseudoWidgets = await getPseudoPreviewWidgets(comfyPage, '5')
|
|
expect(pseudoWidgets.length).toBeGreaterThan(0)
|
|
expect(
|
|
pseudoWidgets.some(([, name]) => name === '$$canvas-image-preview')
|
|
).toBe(true)
|
|
})
|
|
|
|
test('Non-preview widgets coexist with pseudo-preview entries', async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.workflow.loadWorkflow(
|
|
'subgraphs/subgraph-with-preview-node'
|
|
)
|
|
await comfyPage.nextFrame()
|
|
|
|
const pseudoWidgets = await getPseudoPreviewWidgets(comfyPage, '5')
|
|
const nonPreviewWidgets = await getNonPreviewPromotedWidgets(
|
|
comfyPage,
|
|
'5'
|
|
)
|
|
|
|
expect(pseudoWidgets.length).toBeGreaterThan(0)
|
|
expect(nonPreviewWidgets.length).toBeGreaterThan(0)
|
|
expect(
|
|
nonPreviewWidgets.some(([, name]) => name === 'filename_prefix')
|
|
).toBe(true)
|
|
})
|
|
|
|
test('Unpacking subgraph clears pseudo-preview entries from graph', async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.workflow.loadWorkflow(
|
|
'subgraphs/subgraph-with-preview-node'
|
|
)
|
|
await comfyPage.nextFrame()
|
|
|
|
const beforePseudo = await getPseudoPreviewWidgets(comfyPage, '5')
|
|
expect(beforePseudo.length).toBeGreaterThan(0)
|
|
|
|
await comfyPage.page.evaluate(() => {
|
|
const graph = window.app!.graph!
|
|
const subgraphNode = graph.nodes.find((n) => n.isSubgraphNode())
|
|
if (!subgraphNode || !subgraphNode.isSubgraphNode()) return
|
|
graph.unpackSubgraph(subgraphNode)
|
|
})
|
|
await comfyPage.nextFrame()
|
|
|
|
const subgraphNodeCount = await comfyPage.page.evaluate(() => {
|
|
const graph = window.app!.graph!
|
|
return graph.nodes.filter((n) => n.isSubgraphNode()).length
|
|
})
|
|
expect(subgraphNodeCount).toBe(0)
|
|
|
|
await expect
|
|
.poll(async () => comfyPage.subgraph.countGraphPseudoPreviewEntries())
|
|
.toBe(0)
|
|
})
|
|
|
|
test('Removing subgraph node clears pseudo-preview DOM elements', async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.workflow.loadWorkflow(
|
|
'subgraphs/subgraph-with-preview-node'
|
|
)
|
|
await comfyPage.nextFrame()
|
|
|
|
const beforePseudo = await getPseudoPreviewWidgets(comfyPage, '5')
|
|
expect(beforePseudo.length).toBeGreaterThan(0)
|
|
|
|
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('5')
|
|
expect(await subgraphNode.exists()).toBe(true)
|
|
|
|
await subgraphNode.delete()
|
|
|
|
expect(await subgraphNode.exists()).toBe(false)
|
|
|
|
await expect
|
|
.poll(async () => comfyPage.subgraph.countGraphPseudoPreviewEntries())
|
|
.toBe(0)
|
|
await expect(comfyPage.page.locator(domPreviewSelector)).toHaveCount(0)
|
|
})
|
|
|
|
test('Unpacking one subgraph does not clear sibling pseudo-preview entries', async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.workflow.loadWorkflow(
|
|
'subgraphs/subgraph-with-multiple-promoted-previews'
|
|
)
|
|
await comfyPage.nextFrame()
|
|
|
|
const firstNodeBefore = await getPseudoPreviewWidgets(comfyPage, '7')
|
|
const secondNodeBefore = await getPseudoPreviewWidgets(comfyPage, '8')
|
|
|
|
expect(firstNodeBefore.length).toBeGreaterThan(0)
|
|
expect(secondNodeBefore.length).toBeGreaterThan(0)
|
|
|
|
await comfyPage.page.evaluate(() => {
|
|
const graph = window.app!.graph!
|
|
const subgraphNode = graph.getNodeById('7')
|
|
if (!subgraphNode || !subgraphNode.isSubgraphNode()) return
|
|
graph.unpackSubgraph(subgraphNode)
|
|
})
|
|
await comfyPage.nextFrame()
|
|
|
|
const firstNodeExists = await comfyPage.page.evaluate(() => {
|
|
return !!window.app!.graph!.getNodeById('7')
|
|
})
|
|
expect(firstNodeExists).toBe(false)
|
|
|
|
const secondNodeAfter = await getPseudoPreviewWidgets(comfyPage, '8')
|
|
expect(secondNodeAfter).toEqual(secondNodeBefore)
|
|
})
|
|
})
|
|
}
|
|
)
|