import { expect } from '@playwright/test' import type { Locator } 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' const FAKE_MODEL_NAME = 'fake_model.safetensors' function getModelLabel(group: Locator, modelName: string = FAKE_MODEL_NAME) { return group.getByRole('button', { name: modelName, exact: true }) } async function expectReferenceBadge(group: Locator, count: number) { await expect( group.getByTestId(TestIds.dialogs.missingModelReferenceCount) ).toHaveText(String(count)) } 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.getByRole('button', { name: 'Copy URL' }) 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(getModelLabel(missingModelGroup)).toBeVisible() 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 expectReferenceBadge(missingModelGroup, 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 expectReferenceBadge(missingModelGroup, 2) const node1 = await comfyPage.nodeOps.getNodeRefById('1') await node1.click('title') await expect(getModelLabel(missingModelGroup)).toBeVisible() await expect( missingModelGroup.getByTestId(TestIds.dialogs.missingModelLocate) ).toHaveCount(1) await comfyPage.canvas.click() await expectReferenceBadge(missingModelGroup, 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(getModelLabel(missingModelGroup)).toBeVisible() 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(getModelLabel(missingModelGroup)).toBeVisible() 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() }) }) })