mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-15 19:54:32 +00:00
## Summary Fixes a Vue node subgraph case where the missing-model refresh flow clears the missing-model error, but the promoted combo widget remains in an invalid visual state because its hosted options snapshot is stale. ## Changes - **What**: After `reloadNodeDefs()` refreshes combo option lists and extension `refreshComboInNodes` hooks run, resync hosted options snapshots for promoted combo widgets so Vue-rendered subgraph nodes see the newly available model option. - **What**: Adds a focused E2E regression for the missing-model refresh path on a subgraph with a promoted `ckpt_name` widget. - **What**: Hardens the E2E by cleaning up its `/object_info` route override and asserting the widget's `aria-invalid` state rather than a Tailwind implementation class. - **Breaking**: None. - **Dependencies**: None. ## Review Focus This PR is intentionally a minimal patch for the missing-model refresh path, not a broader subgraph architecture change. Root cause: `reloadNodeDefs()` updates the live LiteGraph combo widget options, including widgets inside subgraphs. However, Vue nodes render promoted widgets from a hosted `WidgetState.options` snapshot. When a missing model is downloaded and the missing-model refresh button reloads node definitions, the source combo receives the updated model list, but the promoted host snapshot can still contain the old option list. The missing-model error and node-level state are cleared, while the Vue combo still computes itself as invalid from stale options. The fix keeps the existing host snapshot model intact. It simply resyncs promoted combo host options after the normal combo refresh pipeline finishes. This avoids changing promoted-widget ownership, `useProcessedWidgets` merge precedence, or broader subgraph internals while addressing the reported stale invalid state. Why the helper is in `app.ts`: this sync is currently a single-call-site post-step of `reloadNodeDefs()`, and the ordering is load-bearing. It must run after the core combo refresh loop and after extension `refreshComboInNodes` hooks so it captures both built-in and extension-provided option changes. Keeping the small private helper next to the refresh orchestration makes that sequence explicit and avoids adding a new public subgraph helper or introducing a more visible dependency cycle through `promotionUtils` for a narrow patch. This is stacked on `jaeone/fe-942-bug-error-indicators-persist-after-resolving-missing-model`, so it is opened as a draft until the base PR lands. ## Test Plan - `pnpm knip` - `pnpm exec oxfmt --check src/scripts/app.ts browser_tests/tests/propertiesPanel/errorsTabModeAware.spec.ts` - `pnpm exec eslint --cache --no-warn-ignored src/scripts/app.ts browser_tests/tests/propertiesPanel/errorsTabModeAware.spec.ts` - `pnpm typecheck` - `pnpm test:browser:local --grep "Refreshing a resolved promoted missing model clears the combo invalid state"` - First local run hit a `beforeEach` timeout while the dev server was still warming custom-node/conflict-detection output. - Re-running against the warmed dev server passed. ## Screenshots (if applicable) N/A. The behavioral E2E covers the visible invalid-state regression.
706 lines
23 KiB
TypeScript
706 lines
23 KiB
TypeScript
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/fixtures/helpers/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('Bypass/un-bypass cycle preserves Copy URL button on the restored row', async ({
|
|
comfyPage
|
|
}) => {
|
|
// Regression: on un-bypass, the realtime scan produced a fresh
|
|
// candidate without url/hash/directory — those fields were only
|
|
// attached by the full pipeline's enrichWithEmbeddedMetadata. The
|
|
// row's Copy URL button (v-if gated on representative.url) then
|
|
// disappeared. Per-node scan now enriches from node.properties.models
|
|
// which persists across mode toggles. Uses the `_from_node_properties`
|
|
// fixture because the enrichment source is per-node metadata, not
|
|
// the workflow-level `models[]` array (which the realtime scan
|
|
// path does not see).
|
|
await loadWorkflowAndOpenErrorsTab(
|
|
comfyPage,
|
|
'missing/missing_models_from_node_properties'
|
|
)
|
|
|
|
const copyUrlButton = comfyPage.page.getByTestId(
|
|
TestIds.dialogs.missingModelCopyUrl
|
|
)
|
|
await expect(copyUrlButton.first()).toBeVisible()
|
|
|
|
const node = await comfyPage.nodeOps.getNodeRefById('1')
|
|
await node.click('title')
|
|
await comfyPage.keyboard.bypass()
|
|
await expect.poll(() => node.isBypassed()).toBeTruthy()
|
|
|
|
await node.click('title')
|
|
await comfyPage.keyboard.bypass()
|
|
await expect.poll(() => node.isBypassed()).toBeFalsy()
|
|
await openErrorsTab(comfyPage)
|
|
await expect(copyUrlButton.first()).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(
|
|
'Resolving a promoted missing model widget through the legacy canvas path clears its error',
|
|
{ tag: ['@canvas', '@widget', '@subgraph'] },
|
|
async ({ comfyPage }) => {
|
|
const resolvedModelName = 'v1-5-pruned-emaonly-fp16.safetensors'
|
|
|
|
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', false)
|
|
await loadWorkflowAndOpenErrorsTab(
|
|
comfyPage,
|
|
'missing/missing_model_promoted_widget'
|
|
)
|
|
|
|
const missingModelGroup = comfyPage.page.getByTestId(
|
|
TestIds.dialogs.missingModelsGroup
|
|
)
|
|
await expect(missingModelGroup).toContainText(
|
|
/fake_model\.safetensors\s*\(1\)/
|
|
)
|
|
|
|
await comfyPage.page.evaluate((value) => {
|
|
const hostNode = window.app!.graph!.getNodeById(2)
|
|
if (!hostNode?.isSubgraphNode()) {
|
|
throw new Error('Expected subgraph host node')
|
|
}
|
|
|
|
const interiorNode = hostNode.subgraph.getNodeById(1)
|
|
const widget = interiorNode?.widgets?.find(
|
|
(entry) => entry.name === 'ckpt_name'
|
|
)
|
|
type SettableWidget = typeof widget & {
|
|
setValue?: (
|
|
value: string,
|
|
options: {
|
|
e: PointerEvent
|
|
node: unknown
|
|
canvas: unknown
|
|
}
|
|
) => void
|
|
}
|
|
const settableWidget = widget as SettableWidget | undefined
|
|
|
|
if (!settableWidget?.setValue) {
|
|
throw new Error('Expected concrete ckpt_name widget')
|
|
}
|
|
|
|
settableWidget.setValue(value, {
|
|
e: new PointerEvent('pointerup'),
|
|
node: hostNode,
|
|
canvas: window.app!.canvas
|
|
})
|
|
}, resolvedModelName)
|
|
|
|
await expect(missingModelGroup).toBeHidden()
|
|
}
|
|
)
|
|
|
|
test(
|
|
'Refreshing a resolved promoted missing model clears the combo invalid state',
|
|
{ tag: ['@widget', '@subgraph'] },
|
|
async ({ comfyPage }) => {
|
|
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
|
|
await loadWorkflowAndOpenErrorsTab(
|
|
comfyPage,
|
|
'missing/missing_model_promoted_widget'
|
|
)
|
|
await comfyPage.vueNodes.waitForNodes()
|
|
|
|
const missingModelGroup = comfyPage.page.getByTestId(
|
|
TestIds.dialogs.missingModelsGroup
|
|
)
|
|
await expect(missingModelGroup).toContainText(
|
|
/fake_model\.safetensors\s*\(1\)/
|
|
)
|
|
|
|
const promotedModelCombo = comfyPage.vueNodes
|
|
.getNodeByTitle('Subgraph with Promoted Missing Model')
|
|
.getByRole('combobox', { name: 'ckpt_name', exact: true })
|
|
await expect(promotedModelCombo).toHaveAttribute('aria-invalid', 'true')
|
|
|
|
const objectInfoRoute = /\/object_info$/
|
|
try {
|
|
await comfyPage.page.route(objectInfoRoute, async (route) => {
|
|
const response = await route.fetch()
|
|
const objectInfo = await response.json()
|
|
const ckptName =
|
|
objectInfo.CheckpointLoaderSimple.input.required.ckpt_name
|
|
ckptName[0] = [...ckptName[0], 'fake_model.safetensors']
|
|
await route.fulfill({ response, json: objectInfo })
|
|
})
|
|
|
|
await comfyPage.page
|
|
.getByTestId(TestIds.dialogs.missingModelRefresh)
|
|
.click()
|
|
|
|
await expect(missingModelGroup).toBeHidden()
|
|
await expect(promotedModelCombo).toBeVisible()
|
|
await expect(promotedModelCombo).not.toHaveAttribute(
|
|
'aria-invalid',
|
|
'true'
|
|
)
|
|
} finally {
|
|
await comfyPage.page.unroute(objectInfoRoute)
|
|
}
|
|
}
|
|
)
|
|
|
|
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('Loading a workflow with bypassed subgraph suppresses interior missing model error', async ({
|
|
comfyPage
|
|
}) => {
|
|
// Regression: the initial scan pipeline only checked each node's
|
|
// own mode, so interior nodes of a bypassed subgraph container
|
|
// surfaced errors even though the container was excluded from
|
|
// execution. The pipeline now post-filters candidates whose
|
|
// ancestor path is not fully active.
|
|
await comfyPage.workflow.loadWorkflow(
|
|
'missing/missing_models_in_bypassed_subgraph'
|
|
)
|
|
|
|
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('Entering a bypassed subgraph does not resurface interior missing model error', async ({
|
|
comfyPage
|
|
}) => {
|
|
// Regression: useGraphNodeManager replays graph.onNodeAdded for
|
|
// each interior node on subgraph entry; without an ancestor-aware
|
|
// guard in scanSingleNodeErrors, that re-scan reintroduced the
|
|
// error that the initial pipeline had correctly suppressed.
|
|
await comfyPage.workflow.loadWorkflow(
|
|
'missing/missing_models_in_bypassed_subgraph'
|
|
)
|
|
|
|
const errorsTab = comfyPage.page.getByTestId(
|
|
TestIds.propertiesPanel.errorsTab
|
|
)
|
|
await comfyPage.actionbar.propertiesButton.click()
|
|
await expect(errorsTab).toBeHidden()
|
|
|
|
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2')
|
|
await subgraphNode.navigateIntoSubgraph()
|
|
|
|
await expect(errorsTab).toBeHidden()
|
|
})
|
|
})
|
|
|
|
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()
|
|
})
|
|
})
|
|
})
|