test(subgraph): rewrite paste migration/auto-expose tests to assert behavior

- Drop mock-only WidgetActions demote test; e2e covers the real path
- Drop redundant negative paste-migration test
- Rewrite the two paste-time hook-wiring tests in LGraphCanvas.clipboard.test.ts
  to wire the real flushProxyWidgetMigration / autoExposeKnownPreviewNodes
  helpers and assert observable post-paste state (proxyWidgets cleared,
  PreviewExposureStore exposures populated) instead of pinning hook
  invocations on static mocks.

Amp-Thread-ID: https://ampcode.com/threads/T-019e2812-d683-710e-946f-9ddb9018ff5a
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
DrJKL
2026-05-14 16:16:44 -07:00
parent 11b076ecdf
commit e229016ef9
2 changed files with 64 additions and 142 deletions

View File

@@ -9,7 +9,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createI18n } from 'vue-i18n'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import WidgetActions from './WidgetActions.vue'
@@ -17,14 +16,9 @@ const { mockGetInputSpecForWidget } = vi.hoisted(() => ({
mockGetInputSpecForWidget: vi.fn()
}))
const { mockDemoteWidget, mockPromoteWidget } = vi.hoisted(() => ({
mockDemoteWidget: vi.fn(),
mockPromoteWidget: vi.fn()
}))
vi.mock('@/core/graph/subgraph/promotionUtils', () => ({
demoteWidget: mockDemoteWidget,
promoteWidget: mockPromoteWidget,
demoteWidget: vi.fn(),
promoteWidget: vi.fn(),
isLinkedPromotion: vi.fn(() => false)
}))
@@ -211,50 +205,4 @@ describe('WidgetActions', () => {
expect(onResetToDefault).toHaveBeenCalledWith('option1')
})
it('demotes promoted widget per parent with computed sourceNodeId', async () => {
const sourceNodeId = '7'
const widget = {
name: 'seed',
type: 'number',
value: 1,
label: 'Seed',
options: {},
y: 0,
sourceNodeId,
sourceWidgetName: 'seed'
} as IBaseWidget
const node = fromAny<LGraphNode, unknown>({
id: 5,
type: 'SubgraphNode',
title: 'Subgraph',
rootGraph: { id: 'graph-test' },
computeSize: vi.fn(),
size: [200, 100],
isSubgraphNode: () => true
})
const parent = fromAny<SubgraphNode, unknown>({ id: 5 })
const otherParent = fromAny<SubgraphNode, unknown>({ id: 8 })
const { user } = renderWidgetActions(widget, node, {
parents: [parent, otherParent],
isShownOnParents: true
})
await user.click(screen.getByRole('button', { name: /Hide/ }))
expect(mockDemoteWidget).toHaveBeenCalledTimes(2)
expect(mockDemoteWidget).toHaveBeenNthCalledWith(
1,
expect.objectContaining({ id: sourceNodeId }),
widget,
[parent]
)
expect(mockDemoteWidget).toHaveBeenNthCalledWith(
2,
expect.objectContaining({ id: String(node.id) }),
widget,
[otherParent]
)
})
})

View File

@@ -2,6 +2,8 @@ import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { flushProxyWidgetMigration } from '@/core/graph/subgraph/migration/proxyWidgetMigration'
import { autoExposeKnownPreviewNodes } from '@/core/graph/subgraph/promotionUtils'
import type { Subgraph } from '@/lib/litegraph/src/litegraph'
import {
LGraph,
@@ -17,6 +19,7 @@ import type {
ExportedSubgraph,
ISerialisedNode
} from '@/lib/litegraph/src/types/serialisation'
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
useCanvasStore: () => ({})
@@ -179,7 +182,7 @@ describe('remapClipboardSubgraphNodeIds', () => {
})
})
describe('_deserializeItems proxyWidgets migration', () => {
describe('_deserializeItems paste-time migration & auto-expose', () => {
let originalFlush: typeof LGraph.proxyWidgetMigrationFlush
let originalAutoExpose: typeof LGraph.autoExposePreviewNodes
const registeredTypesToCleanup: string[] = []
@@ -199,6 +202,29 @@ describe('_deserializeItems proxyWidgets migration', () => {
registeredTypesToCleanup.length = 0
})
function registerSubgraphNodeTypeOnCreate(rootGraph: LGraph): void {
rootGraph.events.addEventListener('subgraph-created', (e) => {
const { subgraph } = e.detail
class TestSubgraphNode extends SubgraphNode {
constructor() {
super(rootGraph, subgraph as Subgraph, {
id: -1,
type: subgraph.id,
pos: [0, 0],
size: [100, 100],
inputs: [],
outputs: [],
flags: {},
order: 0,
mode: 0
})
}
}
LiteGraph.registerNodeType(subgraph.id, TestSubgraphNode)
registeredTypesToCleanup.push(subgraph.id)
})
}
function createCanvas(graph: LGraph): LGraphCanvas {
const el = document.createElement('canvas')
el.width = 800
@@ -234,31 +260,15 @@ describe('_deserializeItems proxyWidgets migration', () => {
return new LGraphCanvas(el, graph, { skip_render: true })
}
it('invokes the migration hook for top-level pasted SubgraphNodes carrying legacy proxyWidgets', () => {
const flush = vi.fn()
LGraph.proxyWidgetMigrationFlush = flush
it('clears legacy proxyWidgets on a pasted SubgraphNode and applies host widget values', () => {
LGraph.proxyWidgetMigrationFlush = (hostNode, nodeData) =>
flushProxyWidgetMigration({
hostNode,
hostWidgetValues: nodeData?.widgets_values
})
const rootGraph = new LGraph()
rootGraph.events.addEventListener('subgraph-created', (e) => {
const { subgraph } = e.detail
class TestSubgraphNode extends SubgraphNode {
constructor() {
super(rootGraph, subgraph as Subgraph, {
id: -1,
type: subgraph.id,
pos: [0, 0],
size: [100, 100],
inputs: [],
outputs: [],
flags: {},
order: 0,
mode: 0
})
}
}
LiteGraph.registerNodeType(subgraph.id, TestSubgraphNode)
registeredTypesToCleanup.push(subgraph.id)
})
registerSubgraphNodeTypeOnCreate(rootGraph)
const canvas = createCanvas(rootGraph)
const subgraphId = createUuidv4()
@@ -322,79 +332,29 @@ describe('_deserializeItems proxyWidgets migration', () => {
canvas._deserializeItems(parsed, {})
expect(flush).toHaveBeenCalledTimes(1)
const [hostNode, infoArg] = flush.mock.calls[0]
expect(hostNode).toBeInstanceOf(SubgraphNode)
expect(infoArg?.widgets_values).toStrictEqual([42])
const pastedHosts = rootGraph.nodes.filter(
(n): n is SubgraphNode => n instanceof SubgraphNode
)
expect(pastedHosts).toHaveLength(1)
expect(pastedHosts[0].properties.proxyWidgets).toBeUndefined()
})
it('does not invoke the migration hook for plain pasted nodes', () => {
const flush = vi.fn()
LGraph.proxyWidgetMigrationFlush = flush
it('auto-exposes preview nodes for pasted subgraphs that lack previewExposures', () => {
LGraph.autoExposePreviewNodes = (hostNode) =>
autoExposeKnownPreviewNodes(hostNode)
const rootGraph = new LGraph()
const canvas = createCanvas(rootGraph)
const parsed: ClipboardItems = {
nodes: [
{
id: 1,
type: 'test/plain',
pos: [0, 0],
size: [140, 80],
flags: {},
order: 0,
mode: 0,
inputs: [],
outputs: [],
properties: {}
}
],
groups: [],
reroutes: [],
links: [],
subgraphs: []
}
canvas._deserializeItems(parsed, {})
expect(flush).not.toHaveBeenCalled()
})
it('invokes the auto-expose hook for every pasted SubgraphNode (older clipboard data without previewExposures)', () => {
const autoExpose = vi.fn()
LGraph.autoExposePreviewNodes = autoExpose
const rootGraph = new LGraph()
rootGraph.events.addEventListener('subgraph-created', (e) => {
const { subgraph } = e.detail
class TestSubgraphNode extends SubgraphNode {
constructor() {
super(rootGraph, subgraph as Subgraph, {
id: -1,
type: subgraph.id,
pos: [0, 0],
size: [100, 100],
inputs: [],
outputs: [],
flags: {},
order: 0,
mode: 0
})
}
}
LiteGraph.registerNodeType(subgraph.id, TestSubgraphNode)
registeredTypesToCleanup.push(subgraph.id)
})
registerSubgraphNodeTypeOnCreate(rootGraph)
const canvas = createCanvas(rootGraph)
const subgraphId = createUuidv4()
const interiorPreviewId = 5
const pastedSubgraph: ExportedSubgraph = {
id: subgraphId,
version: 1,
revision: 0,
state: {
lastNodeId: 5,
lastNodeId: interiorPreviewId,
lastLinkId: 0,
lastGroupId: 0,
lastRerouteId: 0
@@ -408,7 +368,7 @@ describe('_deserializeItems proxyWidgets migration', () => {
widgets: [],
nodes: [
{
id: 5,
id: interiorPreviewId,
type: 'PreviewImage',
pos: [0, 0],
size: [140, 80],
@@ -447,7 +407,21 @@ describe('_deserializeItems proxyWidgets migration', () => {
canvas._deserializeItems(parsed, {})
expect(autoExpose).toHaveBeenCalledTimes(1)
expect(autoExpose.mock.calls[0][0]).toBeInstanceOf(SubgraphNode)
const pastedHost = rootGraph.nodes.find(
(n): n is SubgraphNode => n instanceof SubgraphNode
)
expect(pastedHost).toBeDefined()
const exposures = usePreviewExposureStore().getExposures(
rootGraph.id,
String(pastedHost!.id)
)
const interiorIdAfterRemap = pastedHost!.subgraph.nodes[0].id
expect(exposures).toEqual([
expect.objectContaining({
sourceNodeId: String(interiorIdAfterRemap),
sourcePreviewName: '$$canvas-image-preview'
})
])
})
})