Skip to content

Commit 86ee076

Browse files
huchenleigithub-actions
andauthored
Auto link node on creation (#110)
* Auto link node on creation * Handle corner case * Add some browser tests * Add auto link test * Force enable * Confirm setting before running test * Update test expectations [skip ci] --------- Co-authored-by: github-actions <[email protected]>
1 parent ffc4f0c commit 86ee076

File tree

9 files changed

+154
-43
lines changed

9 files changed

+154
-43
lines changed

browser_tests/ComfyPage.ts

Lines changed: 64 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { Page, Locator } from '@playwright/test';
2-
import { test as base } from '@playwright/test';
1+
import type { Page, Locator } from "@playwright/test";
2+
import { test as base } from "@playwright/test";
33
import dotenv from "dotenv";
44
dotenv.config();
55

@@ -8,6 +8,27 @@ interface Position {
88
y: number;
99
}
1010

11+
class ComfyNodeSearchBox {
12+
public readonly input: Locator;
13+
public readonly dropdown: Locator;
14+
15+
constructor(public readonly page: Page) {
16+
this.input = page.locator(
17+
'.comfy-vue-node-search-container input[type="text"]'
18+
);
19+
this.dropdown = page.locator(
20+
".comfy-vue-node-search-container .p-autocomplete-list"
21+
);
22+
}
23+
24+
async fillAndSelectFirstNode(nodeName: string) {
25+
await this.input.waitFor({ state: "visible" });
26+
await this.input.fill(nodeName);
27+
await this.dropdown.waitFor({ state: "visible" });
28+
await this.dropdown.locator("li").nth(0).click();
29+
}
30+
}
31+
1132
export class ComfyPage {
1233
public readonly url: string;
1334
// All canvas position operations are based on default view of canvas.
@@ -17,13 +38,15 @@ export class ComfyPage {
1738
// Buttons
1839
public readonly resetViewButton: Locator;
1940

20-
constructor(
21-
public readonly page: Page,
22-
) {
23-
this.url = process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188';
24-
this.canvas = page.locator('#graph-canvas');
25-
this.widgetTextBox = page.getByPlaceholder('text').nth(1);
26-
this.resetViewButton = page.getByRole('button', { name: 'Reset View' });
41+
// Search box
42+
public readonly searchBox: ComfyNodeSearchBox;
43+
44+
constructor(public readonly page: Page) {
45+
this.url = process.env.PLAYWRIGHT_TEST_URL || "http://localhost:8188";
46+
this.canvas = page.locator("#graph-canvas");
47+
this.widgetTextBox = page.getByPlaceholder("text").nth(1);
48+
this.resetViewButton = page.getByRole("button", { name: "Reset View" });
49+
this.searchBox = new ComfyNodeSearchBox(page);
2750
}
2851

2952
async goto() {
@@ -47,8 +70,8 @@ export class ComfyPage {
4770
await this.canvas.click({
4871
position: {
4972
x: 618,
50-
y: 191
51-
}
73+
y: 191,
74+
},
5275
});
5376
await this.nextFrame();
5477
}
@@ -57,8 +80,8 @@ export class ComfyPage {
5780
await this.canvas.click({
5881
position: {
5982
x: 622,
60-
y: 400
61-
}
83+
y: 400,
84+
},
6285
});
6386
await this.nextFrame();
6487
}
@@ -67,8 +90,8 @@ export class ComfyPage {
6790
await this.canvas.click({
6891
position: {
6992
x: 35,
70-
y: 31
71-
}
93+
y: 31,
94+
},
7295
});
7396
await this.nextFrame();
7497
}
@@ -82,10 +105,7 @@ export class ComfyPage {
82105
}
83106

84107
async dragNode2() {
85-
await this.dragAndDrop(
86-
{ x: 622, y: 400 },
87-
{ x: 622, y: 300 },
88-
);
108+
await this.dragAndDrop({ x: 622, y: 400 }, { x: 622, y: 300 });
89109
await this.nextFrame();
90110
}
91111

@@ -113,15 +133,15 @@ export class ComfyPage {
113133
async adjustWidgetValue() {
114134
// Adjust Empty Latent Image's width input.
115135
const page = this.page;
116-
await page.locator('#graph-canvas').click({
136+
await page.locator("#graph-canvas").click({
117137
position: {
118138
x: 724,
119-
y: 645
120-
}
139+
y: 645,
140+
},
121141
});
122142
await page.locator('input[type="text"]').click();
123-
await page.locator('input[type="text"]').fill('128');
124-
await page.locator('input[type="text"]').press('Enter');
143+
await page.locator('input[type="text"]').fill("128");
144+
await page.locator('input[type="text"]').press("Enter");
125145
await this.nextFrame();
126146
}
127147

@@ -140,7 +160,7 @@ export class ComfyPage {
140160
}
141161

142162
async rightClickCanvas() {
143-
await this.page.mouse.click(10, 10, { button: 'right' });
163+
await this.page.mouse.click(10, 10, { button: "right" });
144164
await this.nextFrame();
145165
}
146166

@@ -153,7 +173,7 @@ export class ComfyPage {
153173
await this.canvas.click({
154174
position: {
155175
x: 724,
156-
y: 625
176+
y: 625,
157177
},
158178
});
159179
this.page.mouse.move(10, 10);
@@ -164,34 +184,34 @@ export class ComfyPage {
164184
await this.canvas.click({
165185
position: {
166186
x: 724,
167-
y: 645
187+
y: 645,
168188
},
169-
button: 'right'
189+
button: "right",
170190
});
171191
this.page.mouse.move(10, 10);
172192
await this.nextFrame();
173193
}
174194

175195
async select2Nodes() {
176196
// Select 2 CLIP nodes.
177-
await this.page.keyboard.down('Control');
197+
await this.page.keyboard.down("Control");
178198
await this.clickTextEncodeNode1();
179199
await this.clickTextEncodeNode2();
180-
await this.page.keyboard.up('Control');
200+
await this.page.keyboard.up("Control");
181201
await this.nextFrame();
182202
}
183203

184204
async ctrlC() {
185-
await this.page.keyboard.down('Control');
186-
await this.page.keyboard.press('KeyC');
187-
await this.page.keyboard.up('Control');
205+
await this.page.keyboard.down("Control");
206+
await this.page.keyboard.press("KeyC");
207+
await this.page.keyboard.up("Control");
188208
await this.nextFrame();
189209
}
190210

191211
async ctrlV() {
192-
await this.page.keyboard.down('Control');
193-
await this.page.keyboard.press('KeyV');
194-
await this.page.keyboard.up('Control');
212+
await this.page.keyboard.down("Control");
213+
await this.page.keyboard.press("KeyV");
214+
await this.page.keyboard.up("Control");
195215
await this.nextFrame();
196216
}
197217
}
@@ -202,25 +222,27 @@ export const comfyPageFixture = base.extend<{ comfyPage: ComfyPage }>({
202222
await comfyPage.goto();
203223
// Unify font for consistent screenshots.
204224
await page.addStyleTag({
205-
url: "https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"
225+
url: "https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap",
206226
});
207227
await page.addStyleTag({
208-
url: "https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"
228+
url: "https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap",
209229
});
210230
await page.addStyleTag({
211231
content: `
212232
* {
213233
font-family: 'Roboto Mono', 'Noto Color Emoji';
214-
}`
234+
}`,
215235
});
216236

217237
await page.waitForFunction(() => document.fonts.ready);
218-
await page.waitForFunction(() => window['app'] != undefined);
219-
await page.evaluate(() => { window['app']['canvas'].show_info = false; });
238+
await page.waitForFunction(() => window["app"] != undefined);
239+
await page.evaluate(() => {
240+
window["app"]["canvas"].show_info = false;
241+
});
220242
await comfyPage.nextFrame();
221243
// Reset view to force re-rendering of canvas. So that info fields like fps
222244
// become hidden.
223245
await comfyPage.resetView();
224246
await use(comfyPage);
225247
},
226-
});
248+
});
962 Bytes
Loading
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { expect } from "@playwright/test";
2+
import { ComfyPage, comfyPageFixture } from "./ComfyPage";
3+
4+
export const test = comfyPageFixture.extend<{ comfyPage: ComfyPage }>({
5+
comfyPage: async ({ comfyPage }, use) => {
6+
await comfyPage.page.evaluate(async () => {
7+
await window["app"].ui.settings.setSettingValueAsync(
8+
"Comfy.NodeSearchBoxImpl",
9+
"default"
10+
);
11+
});
12+
await use(comfyPage);
13+
},
14+
});
15+
16+
test.describe("Node search box", () => {
17+
test("Can trigger on empty canvas double click", async ({ comfyPage }) => {
18+
await comfyPage.doubleClickCanvas();
19+
await expect(comfyPage.searchBox.input).toHaveCount(1);
20+
});
21+
22+
test("Can trigger on link release", async ({ comfyPage }) => {
23+
await comfyPage.page.keyboard.down("Shift");
24+
await comfyPage.disconnectEdge();
25+
await expect(comfyPage.searchBox.input).toHaveCount(1);
26+
});
27+
28+
test("Does not trigger on link release (no shift)", async ({ comfyPage }) => {
29+
await comfyPage.disconnectEdge();
30+
await expect(comfyPage.searchBox.input).toHaveCount(0);
31+
});
32+
33+
test("Can add node", async ({ comfyPage }) => {
34+
await comfyPage.doubleClickCanvas();
35+
await expect(comfyPage.searchBox.input).toHaveCount(1);
36+
await comfyPage.searchBox.fillAndSelectFirstNode("KSampler");
37+
await expect(comfyPage.canvas).toHaveScreenshot("added-node.png");
38+
});
39+
40+
test("Can auto link node", async ({ comfyPage }) => {
41+
await comfyPage.page.keyboard.down("Shift");
42+
await comfyPage.disconnectEdge();
43+
await comfyPage.page.keyboard.up("Shift");
44+
await comfyPage.searchBox.fillAndSelectFirstNode("CLIPTextEncode");
45+
await expect(comfyPage.canvas).toHaveScreenshot("auto-linked-node.png");
46+
});
47+
});
113 KB
Loading
106 KB
Loading
96.6 KB
Loading
90.8 KB
Loading

playwright.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export default defineConfig({
3636
{
3737
name: 'chromium',
3838
use: { ...devices['Desktop Chrome'] },
39+
timeout: 3000,
3940
},
4041

4142
// {

src/components/NodeSearchBoxPopover.vue

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@ import { app } from "@/scripts/app";
2323
import { inject, onMounted, onUnmounted, reactive, Ref, ref } from "vue";
2424
import NodeSearchBox from "./NodeSearchBox.vue";
2525
import Dialog from "primevue/dialog";
26-
import { LiteGraph, LiteGraphCanvasEvent } from "@comfyorg/litegraph";
26+
import {
27+
INodeSlot,
28+
LiteGraph,
29+
LiteGraphCanvasEvent,
30+
LGraphNode,
31+
LinkReleaseContext,
32+
} from "@comfyorg/litegraph";
2733
import {
2834
FilterAndValue,
2935
NodeSearchService,
@@ -63,12 +69,47 @@ const closeDialog = () => {
6369
clearFilters();
6470
visible.value = false;
6571
};
72+
const connectNodeOnLinkRelease = (
73+
node: LGraphNode,
74+
context: LinkReleaseContext
75+
) => {
76+
const destIsInput = context.node_from !== undefined;
77+
const srcNode = (
78+
destIsInput ? context.node_from : context.node_to
79+
) as LGraphNode;
80+
const srcSlotIndex: number = context.slot_from.slot_index;
81+
const linkDataType = destIsInput
82+
? context.type_filter_in
83+
: context.type_filter_out;
84+
const destSlots = destIsInput ? node.inputs : node.outputs;
85+
const destSlotIndex = destSlots.findIndex(
86+
(slot: INodeSlot) => slot.type === linkDataType
87+
);
88+
89+
if (destSlotIndex === -1) {
90+
console.warn(
91+
`Could not find slot with type ${linkDataType} on node ${node.title}`
92+
);
93+
return;
94+
}
95+
96+
if (destIsInput) {
97+
srcNode.connect(srcSlotIndex, node, destSlotIndex);
98+
} else {
99+
node.connect(destSlotIndex, srcNode, srcSlotIndex);
100+
}
101+
};
66102
const addNode = (nodeDef: ComfyNodeDef) => {
67103
closeDialog();
68104
const node = LiteGraph.createNode(nodeDef.name, nodeDef.display_name, {});
69105
if (node) {
70106
node.pos = getNewNodeLocation();
71107
app.graph.add(node);
108+
109+
const eventDetail = triggerEvent.value.detail;
110+
if (eventDetail.subType === "empty-release") {
111+
connectNodeOnLinkRelease(node, eventDetail.linkReleaseContext);
112+
}
72113
}
73114
};
74115
const nodeSearchService = (

0 commit comments

Comments
 (0)