|
1 | 1 | import { createTestingPinia } from '@pinia/testing' |
2 | 2 | import { setActivePinia } from 'pinia' |
3 | | -import { beforeEach, describe, expect, it } from 'vitest' |
| 3 | +import { beforeEach, describe, expect, it, vi } from 'vitest' |
4 | 4 |
|
5 | 5 | import { |
6 | 6 | SUBGRAPH_INPUT_ID, |
@@ -379,6 +379,194 @@ describe('LGraph.configure input slot realignment (#3348)', () => { |
379 | 379 | }) |
380 | 380 | }) |
381 | 381 |
|
| 382 | +const SHRUNK_DEFINITION_ORDER = ['in_a', 'in_b'] |
| 383 | + |
| 384 | +/** Drops any serialized input its definition no longer declares. */ |
| 385 | +class DroppedInputTargetNode extends LGraphNode { |
| 386 | + constructor(title?: string) { |
| 387 | + super(title ?? 'DroppedInputTarget') |
| 388 | + for (const name of SHRUNK_DEFINITION_ORDER) this.addInput(name, 'number') |
| 389 | + } |
| 390 | + |
| 391 | + override configure(data: ISerialisedNode): void { |
| 392 | + data.inputs = (data.inputs ?? []) |
| 393 | + .filter((input) => SHRUNK_DEFINITION_ORDER.includes(input.name)) |
| 394 | + .sort( |
| 395 | + (a, b) => |
| 396 | + SHRUNK_DEFINITION_ORDER.indexOf(a.name) - |
| 397 | + SHRUNK_DEFINITION_ORDER.indexOf(b.name) |
| 398 | + ) |
| 399 | + super.configure(data) |
| 400 | + } |
| 401 | +} |
| 402 | + |
| 403 | +const RENAMED_DEFINITION_ORDER = ['in_a', 'in_b', 'in_c_v2'] |
| 404 | + |
| 405 | +/** Renames a live input after configure, as a pack migrating a slot does. */ |
| 406 | +class RenamedInputTargetNode extends LGraphNode { |
| 407 | + constructor(title?: string) { |
| 408 | + super(title ?? 'RenamedInputTarget') |
| 409 | + for (const name of RENAMED_DEFINITION_ORDER) this.addInput(name, 'number') |
| 410 | + } |
| 411 | + |
| 412 | + override configure(data: ISerialisedNode): void { |
| 413 | + super.configure(data) |
| 414 | + for (const input of this.inputs) { |
| 415 | + if (input.name === 'in_c') input.name = 'in_c_v2' |
| 416 | + } |
| 417 | + this.inputs.sort( |
| 418 | + (a, b) => |
| 419 | + RENAMED_DEFINITION_ORDER.indexOf(a.name) - |
| 420 | + RENAMED_DEFINITION_ORDER.indexOf(b.name) |
| 421 | + ) |
| 422 | + } |
| 423 | +} |
| 424 | + |
| 425 | +/** |
| 426 | + * Serialized target carrying one input name the live node will not have. |
| 427 | + * `in_c` holds link 3 at slot 0, so the moves `1: 1 -> 0` and `2: 2 -> 1` |
| 428 | + * form a batch whose first member collides with link 3's stale placement. |
| 429 | + */ |
| 430 | +function unmatchedInputNameWorkflow(nodeType: string): SerialisableGraph { |
| 431 | + return { |
| 432 | + id: 'ab000000-0000-4000-8000-000000000004', |
| 433 | + version: 1, |
| 434 | + revision: 0, |
| 435 | + state: { lastNodeId: 2, lastLinkId: 3, lastGroupId: 0, lastRerouteId: 0 }, |
| 436 | + nodes: [ |
| 437 | + { |
| 438 | + id: 1, |
| 439 | + type: 'test/RealignSource', |
| 440 | + pos: [0, 0], |
| 441 | + size: [140, 60], |
| 442 | + flags: {}, |
| 443 | + order: 0, |
| 444 | + mode: 0, |
| 445 | + inputs: [], |
| 446 | + outputs: [{ name: 'out', type: 'number', links: [1, 2, 3] }], |
| 447 | + properties: {} |
| 448 | + }, |
| 449 | + { |
| 450 | + id: 2, |
| 451 | + type: nodeType, |
| 452 | + pos: [300, 0], |
| 453 | + size: [140, 80], |
| 454 | + flags: {}, |
| 455 | + order: 1, |
| 456 | + mode: 0, |
| 457 | + inputs: [ |
| 458 | + { name: 'in_c', type: 'number', link: 3 }, |
| 459 | + { name: 'in_a', type: 'number', link: 1 }, |
| 460 | + { name: 'in_b', type: 'number', link: 2 } |
| 461 | + ], |
| 462 | + outputs: [], |
| 463 | + properties: {} |
| 464 | + } |
| 465 | + ], |
| 466 | + links: [ |
| 467 | + { |
| 468 | + id: 3, |
| 469 | + origin_id: 1, |
| 470 | + origin_slot: 0, |
| 471 | + target_id: 2, |
| 472 | + target_slot: 0, |
| 473 | + type: 'number' |
| 474 | + }, |
| 475 | + { |
| 476 | + id: 1, |
| 477 | + origin_id: 1, |
| 478 | + origin_slot: 0, |
| 479 | + target_id: 2, |
| 480 | + target_slot: 1, |
| 481 | + type: 'number' |
| 482 | + }, |
| 483 | + { |
| 484 | + id: 2, |
| 485 | + origin_id: 1, |
| 486 | + origin_slot: 0, |
| 487 | + target_id: 2, |
| 488 | + target_slot: 2, |
| 489 | + type: 'number' |
| 490 | + } |
| 491 | + ] |
| 492 | + } |
| 493 | +} |
| 494 | + |
| 495 | +describe('LGraph.configure realignment with an unmatched input name (#15581)', () => { |
| 496 | + beforeEach(() => { |
| 497 | + setActivePinia(createTestingPinia({ stubActions: false })) |
| 498 | + LiteGraph.registerNodeType('test/RealignSource', SourceNode) |
| 499 | + LiteGraph.registerNodeType( |
| 500 | + 'test/DroppedInputTarget', |
| 501 | + DroppedInputTargetNode |
| 502 | + ) |
| 503 | + LiteGraph.registerNodeType( |
| 504 | + 'test/RenamedInputTarget', |
| 505 | + RenamedInputTargetNode |
| 506 | + ) |
| 507 | + }) |
| 508 | + |
| 509 | + it.fails('realigns siblings when configure drops an input', () => { |
| 510 | + const graph = new LGraph() |
| 511 | + graph.configure(unmatchedInputNameWorkflow('test/DroppedInputTarget')) |
| 512 | + |
| 513 | + const target = graph.getNodeById(toNodeId(2))! |
| 514 | + expect(target.getInputLink(0)?.id).toBe(toLinkId(1)) |
| 515 | + expect(target.getInputLink(1)?.id).toBe(toLinkId(2)) |
| 516 | + }) |
| 517 | + |
| 518 | + it.fails('realigns siblings when configure renames an input', () => { |
| 519 | + const graph = new LGraph() |
| 520 | + graph.configure(unmatchedInputNameWorkflow('test/RenamedInputTarget')) |
| 521 | + |
| 522 | + const target = graph.getNodeById(toNodeId(2))! |
| 523 | + expect(target.getInputLink(0)?.id).toBe(toLinkId(1)) |
| 524 | + expect(target.getInputLink(1)?.id).toBe(toLinkId(2)) |
| 525 | + }) |
| 526 | + |
| 527 | + it.fails('reports no error while realigning around an unmatched name', () => { |
| 528 | + const error = vi.spyOn(console, 'error').mockImplementation(() => {}) |
| 529 | + |
| 530 | + const graph = new LGraph() |
| 531 | + graph.configure(unmatchedInputNameWorkflow('test/DroppedInputTarget')) |
| 532 | + |
| 533 | + expect(error).not.toHaveBeenCalled() |
| 534 | + }) |
| 535 | +}) |
| 536 | + |
| 537 | +describe('realignInputLinkSlots with a rejected batch (#15581)', () => { |
| 538 | + beforeEach(() => { |
| 539 | + setActivePinia(createTestingPinia({ stubActions: false })) |
| 540 | + }) |
| 541 | + |
| 542 | + it.fails('lands the non-conflicting moves when one move is blocked', () => { |
| 543 | + const graph = new LGraph() |
| 544 | + const source = new LGraphNode('Source') |
| 545 | + source.addOutput('out', 'number') |
| 546 | + const target = new LGraphNode('Target') |
| 547 | + for (const name of ['p', 'q', 'r']) target.addInput(name, 'number') |
| 548 | + graph.add(source) |
| 549 | + graph.add(target) |
| 550 | + |
| 551 | + const squatter = source.connect(0, target, 0)! |
| 552 | + const blocked = source.connect(0, target, 1)! |
| 553 | + const free = source.connect(0, target, 2)! |
| 554 | + |
| 555 | + const nodeData = target.serialize() |
| 556 | + nodeData.inputs = [ |
| 557 | + { name: 'no_such_input', type: 'number', link: squatter.id }, |
| 558 | + { name: 'p', type: 'number', link: blocked.id }, |
| 559 | + { name: 'q', type: 'number', link: free.id } |
| 560 | + ] |
| 561 | + |
| 562 | + realignInputLinkSlots(graph, [nodeData]) |
| 563 | + |
| 564 | + expect( |
| 565 | + useLinkStore().getInputSlotLink(graphScopeOf(graph), target.id, 1)?.id |
| 566 | + ).toBe(free.id) |
| 567 | + }) |
| 568 | +}) |
| 569 | + |
382 | 570 | describe('realignInputLinkSlots', () => { |
383 | 571 | beforeEach(() => { |
384 | 572 | setActivePinia(createTestingPinia({ stubActions: false })) |
|
0 commit comments