mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-19 22:09:37 +00:00
Follow-up to #10856. Four correctness issues and their regression tests. ## Bugs fixed ### 1. ErrorOverlay model count reflected node selection `useErrorGroups` exposed `filteredMissingModelGroups` under the public name `missingModelGroups`. `ErrorOverlay.vue` read that alias to compute its model count label, so selecting a node shrank the overlay total. The overlay must always show the whole workflow's errors. Exposed both shapes explicitly: `missingModelGroups` / `missingMediaGroups` (unfiltered totals) and `filteredMissingModelGroups` / `filteredMissingMediaGroups` (selection-scoped). `TabErrors.vue` destructures the filtered variant with an alias. Before https://github.com/user-attachments/assets/eb848c5f-d092-4a4f-b86f-d22bb4408003 After https://github.com/user-attachments/assets/75e67819-c9f2-45ec-9241-74023eca6120 ### 2. Bypass → un-bypass dropped url/hash metadata Realtime `scanNodeModelCandidates` only reads widget values, so un-bypass produced a fresh candidate without the url that `enrichWithEmbeddedMetadata` had previously attached from `graphData.models`. `MissingModelRow`'s download/copy-url buttons disappeared after a bypass/un-bypass cycle. Added `enrichCandidateFromNodeProperties` that copies `url`/`hash`/`directory` from the node's own `properties.models` — which persists across mode toggles — into each scanned candidate. Applied to every call site of the per-node scan. A later fix in the same branch also enforces directory agreement to prevent a same-name / different-directory collision from stamping the wrong metadata. Before https://github.com/user-attachments/assets/39039d83-4d55-41a9-9d01-dec40843741b After https://github.com/user-attachments/assets/047a603b-fb52-4320-886d-dfeed457d833 ### 3. Initial full scan surfaced interior errors of a muted/bypassed subgraph container `scanAllModelCandidates`, `scanAllMediaCandidates`, and the JSON-based missing-node scan only check each node's own mode. Interior nodes whose parent container was bypassed passed the filter. Added `isAncestorPathActive(rootGraph, executionId)` to `graphTraversalUtil` and post-filter the three pipelines in `app.ts` after the live rootGraph is configured. The filter uses the execution-ID path (`"65:63"` → check node 65's mode) so it handles both live-scan-produced and JSON-enrichment-produced candidates. Before https://github.com/user-attachments/assets/3032d46b-81cd-420e-ab8e-f58392267602 After https://github.com/user-attachments/assets/02a01931-951d-4a48-986c-06424044fbf8 ### 4. Bypassed subgraph entry re-surfaced interior errors `useGraphNodeManager` replays `graph.onNodeAdded` for each existing interior node when the Vue node manager initializes on subgraph entry. That chain reached `scanSingleNodeErrors` via `installErrorClearingHooks`' `onNodeAdded` override. Each interior node's own mode was active, so the caller guards passed and the scan re-introduced the error that the initial pipeline had correctly suppressed. Added an ancestor-activity gate at the top of `scanSingleNodeErrors`, the single entry point shared by paste, un-bypass, subgraph entry, and subgraph container activation. A later commit also hardens this guard against detached nodes (null execution ID → skip) and applies the same ancestor check to `isCandidateStillActive` in the realtime verification callback. Before https://github.com/user-attachments/assets/fe44862d-f1d6-41ed-982d-614a7e83d441 After https://github.com/user-attachments/assets/497a76ce-3caa-479f-9024-4cd0f7bd20a4 ## Tests - 6 unit tests for `isAncestorPathActive` (root, active, immediate-bypass, deep-nested mute, unresolvable ancestor, null rootGraph) - 4 unit tests for `enrichCandidateFromNodeProperties` (enrichment, no-overwrite, name mismatch, directory mismatch) - 1 unit test for `scanSingleNodeErrors` ancestor guard (subgraph entry replaying onNodeAdded) - 2 unit tests for `useErrorGroups` dual export + ErrorOverlay contract - 4 E2E tests: - ErrorOverlay model count stays constant when a node is selected (new fixture `missing_models_distinct.json`) - Bypass/un-bypass cycle preserves Copy URL button (uses `missing_models_from_node_properties`) - Loading a workflow with bypassed subgraph suppresses interior missing model error (new fixture `missing_models_in_bypassed_subgraph.json`) - Entering a bypassed subgraph does not resurface interior missing model error (shares the above fixture) `pnpm typecheck`, `pnpm lint`, 206 related unit tests passing. ## Follow-up Several items raised by code review are deferred as pre-existing tech debt or scope-avoided refactors. Tracked via comments on #11215 and #11216. --- Follows up on #10856.
600 lines
20 KiB
TypeScript
600 lines
20 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/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('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('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()
|
|
})
|
|
})
|
|
})
|