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 <github-actions@github.com>
This commit is contained in:
Chenlei Hu
2024-07-11 12:25:46 -04:00
committed by GitHub
parent ffc4f0c98e
commit 86ee0767c3
9 changed files with 154 additions and 43 deletions

View File

@@ -1,5 +1,5 @@
import type { Page, Locator } from '@playwright/test'; import type { Page, Locator } from "@playwright/test";
import { test as base } from '@playwright/test'; import { test as base } from "@playwright/test";
import dotenv from "dotenv"; import dotenv from "dotenv";
dotenv.config(); dotenv.config();
@@ -8,6 +8,27 @@ interface Position {
y: number; y: number;
} }
class ComfyNodeSearchBox {
public readonly input: Locator;
public readonly dropdown: Locator;
constructor(public readonly page: Page) {
this.input = page.locator(
'.comfy-vue-node-search-container input[type="text"]'
);
this.dropdown = page.locator(
".comfy-vue-node-search-container .p-autocomplete-list"
);
}
async fillAndSelectFirstNode(nodeName: string) {
await this.input.waitFor({ state: "visible" });
await this.input.fill(nodeName);
await this.dropdown.waitFor({ state: "visible" });
await this.dropdown.locator("li").nth(0).click();
}
}
export class ComfyPage { export class ComfyPage {
public readonly url: string; public readonly url: string;
// All canvas position operations are based on default view of canvas. // All canvas position operations are based on default view of canvas.
@@ -17,13 +38,15 @@ export class ComfyPage {
// Buttons // Buttons
public readonly resetViewButton: Locator; public readonly resetViewButton: Locator;
constructor( // Search box
public readonly page: Page, public readonly searchBox: ComfyNodeSearchBox;
) {
this.url = process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188'; constructor(public readonly page: Page) {
this.canvas = page.locator('#graph-canvas'); this.url = process.env.PLAYWRIGHT_TEST_URL || "http://localhost:8188";
this.widgetTextBox = page.getByPlaceholder('text').nth(1); this.canvas = page.locator("#graph-canvas");
this.resetViewButton = page.getByRole('button', { name: 'Reset View' }); this.widgetTextBox = page.getByPlaceholder("text").nth(1);
this.resetViewButton = page.getByRole("button", { name: "Reset View" });
this.searchBox = new ComfyNodeSearchBox(page);
} }
async goto() { async goto() {
@@ -47,8 +70,8 @@ export class ComfyPage {
await this.canvas.click({ await this.canvas.click({
position: { position: {
x: 618, x: 618,
y: 191 y: 191,
} },
}); });
await this.nextFrame(); await this.nextFrame();
} }
@@ -57,8 +80,8 @@ export class ComfyPage {
await this.canvas.click({ await this.canvas.click({
position: { position: {
x: 622, x: 622,
y: 400 y: 400,
} },
}); });
await this.nextFrame(); await this.nextFrame();
} }
@@ -67,8 +90,8 @@ export class ComfyPage {
await this.canvas.click({ await this.canvas.click({
position: { position: {
x: 35, x: 35,
y: 31 y: 31,
} },
}); });
await this.nextFrame(); await this.nextFrame();
} }
@@ -82,10 +105,7 @@ export class ComfyPage {
} }
async dragNode2() { async dragNode2() {
await this.dragAndDrop( await this.dragAndDrop({ x: 622, y: 400 }, { x: 622, y: 300 });
{ x: 622, y: 400 },
{ x: 622, y: 300 },
);
await this.nextFrame(); await this.nextFrame();
} }
@@ -113,15 +133,15 @@ export class ComfyPage {
async adjustWidgetValue() { async adjustWidgetValue() {
// Adjust Empty Latent Image's width input. // Adjust Empty Latent Image's width input.
const page = this.page; const page = this.page;
await page.locator('#graph-canvas').click({ await page.locator("#graph-canvas").click({
position: { position: {
x: 724, x: 724,
y: 645 y: 645,
} },
}); });
await page.locator('input[type="text"]').click(); await page.locator('input[type="text"]').click();
await page.locator('input[type="text"]').fill('128'); await page.locator('input[type="text"]').fill("128");
await page.locator('input[type="text"]').press('Enter'); await page.locator('input[type="text"]').press("Enter");
await this.nextFrame(); await this.nextFrame();
} }
@@ -140,7 +160,7 @@ export class ComfyPage {
} }
async rightClickCanvas() { async rightClickCanvas() {
await this.page.mouse.click(10, 10, { button: 'right' }); await this.page.mouse.click(10, 10, { button: "right" });
await this.nextFrame(); await this.nextFrame();
} }
@@ -153,7 +173,7 @@ export class ComfyPage {
await this.canvas.click({ await this.canvas.click({
position: { position: {
x: 724, x: 724,
y: 625 y: 625,
}, },
}); });
this.page.mouse.move(10, 10); this.page.mouse.move(10, 10);
@@ -164,9 +184,9 @@ export class ComfyPage {
await this.canvas.click({ await this.canvas.click({
position: { position: {
x: 724, x: 724,
y: 645 y: 645,
}, },
button: 'right' button: "right",
}); });
this.page.mouse.move(10, 10); this.page.mouse.move(10, 10);
await this.nextFrame(); await this.nextFrame();
@@ -174,24 +194,24 @@ export class ComfyPage {
async select2Nodes() { async select2Nodes() {
// Select 2 CLIP nodes. // Select 2 CLIP nodes.
await this.page.keyboard.down('Control'); await this.page.keyboard.down("Control");
await this.clickTextEncodeNode1(); await this.clickTextEncodeNode1();
await this.clickTextEncodeNode2(); await this.clickTextEncodeNode2();
await this.page.keyboard.up('Control'); await this.page.keyboard.up("Control");
await this.nextFrame(); await this.nextFrame();
} }
async ctrlC() { async ctrlC() {
await this.page.keyboard.down('Control'); await this.page.keyboard.down("Control");
await this.page.keyboard.press('KeyC'); await this.page.keyboard.press("KeyC");
await this.page.keyboard.up('Control'); await this.page.keyboard.up("Control");
await this.nextFrame(); await this.nextFrame();
} }
async ctrlV() { async ctrlV() {
await this.page.keyboard.down('Control'); await this.page.keyboard.down("Control");
await this.page.keyboard.press('KeyV'); await this.page.keyboard.press("KeyV");
await this.page.keyboard.up('Control'); await this.page.keyboard.up("Control");
await this.nextFrame(); await this.nextFrame();
} }
} }
@@ -202,25 +222,27 @@ export const comfyPageFixture = base.extend<{ comfyPage: ComfyPage }>({
await comfyPage.goto(); await comfyPage.goto();
// Unify font for consistent screenshots. // Unify font for consistent screenshots.
await page.addStyleTag({ await page.addStyleTag({
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" 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",
}); });
await page.addStyleTag({ await page.addStyleTag({
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" 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",
}); });
await page.addStyleTag({ await page.addStyleTag({
content: ` content: `
* { * {
font-family: 'Roboto Mono', 'Noto Color Emoji'; font-family: 'Roboto Mono', 'Noto Color Emoji';
}` }`,
}); });
await page.waitForFunction(() => document.fonts.ready); await page.waitForFunction(() => document.fonts.ready);
await page.waitForFunction(() => window['app'] != undefined); await page.waitForFunction(() => window["app"] != undefined);
await page.evaluate(() => { window['app']['canvas'].show_info = false; }); await page.evaluate(() => {
window["app"]["canvas"].show_info = false;
});
await comfyPage.nextFrame(); await comfyPage.nextFrame();
// Reset view to force re-rendering of canvas. So that info fields like fps // Reset view to force re-rendering of canvas. So that info fields like fps
// become hidden. // become hidden.
await comfyPage.resetView(); await comfyPage.resetView();
await use(comfyPage); await use(comfyPage);
}, },
}); });

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View File

@@ -0,0 +1,47 @@
import { expect } from "@playwright/test";
import { ComfyPage, comfyPageFixture } from "./ComfyPage";
export const test = comfyPageFixture.extend<{ comfyPage: ComfyPage }>({
comfyPage: async ({ comfyPage }, use) => {
await comfyPage.page.evaluate(async () => {
await window["app"].ui.settings.setSettingValueAsync(
"Comfy.NodeSearchBoxImpl",
"default"
);
});
await use(comfyPage);
},
});
test.describe("Node search box", () => {
test("Can trigger on empty canvas double click", async ({ comfyPage }) => {
await comfyPage.doubleClickCanvas();
await expect(comfyPage.searchBox.input).toHaveCount(1);
});
test("Can trigger on link release", async ({ comfyPage }) => {
await comfyPage.page.keyboard.down("Shift");
await comfyPage.disconnectEdge();
await expect(comfyPage.searchBox.input).toHaveCount(1);
});
test("Does not trigger on link release (no shift)", async ({ comfyPage }) => {
await comfyPage.disconnectEdge();
await expect(comfyPage.searchBox.input).toHaveCount(0);
});
test("Can add node", async ({ comfyPage }) => {
await comfyPage.doubleClickCanvas();
await expect(comfyPage.searchBox.input).toHaveCount(1);
await comfyPage.searchBox.fillAndSelectFirstNode("KSampler");
await expect(comfyPage.canvas).toHaveScreenshot("added-node.png");
});
test("Can auto link node", async ({ comfyPage }) => {
await comfyPage.page.keyboard.down("Shift");
await comfyPage.disconnectEdge();
await comfyPage.page.keyboard.up("Shift");
await comfyPage.searchBox.fillAndSelectFirstNode("CLIPTextEncode");
await expect(comfyPage.canvas).toHaveScreenshot("auto-linked-node.png");
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

View File

@@ -36,6 +36,7 @@ export default defineConfig({
{ {
name: 'chromium', name: 'chromium',
use: { ...devices['Desktop Chrome'] }, use: { ...devices['Desktop Chrome'] },
timeout: 3000,
}, },
// { // {

View File

@@ -23,7 +23,13 @@ import { app } from "@/scripts/app";
import { inject, onMounted, onUnmounted, reactive, Ref, ref } from "vue"; import { inject, onMounted, onUnmounted, reactive, Ref, ref } from "vue";
import NodeSearchBox from "./NodeSearchBox.vue"; import NodeSearchBox from "./NodeSearchBox.vue";
import Dialog from "primevue/dialog"; import Dialog from "primevue/dialog";
import { LiteGraph, LiteGraphCanvasEvent } from "@comfyorg/litegraph"; import {
INodeSlot,
LiteGraph,
LiteGraphCanvasEvent,
LGraphNode,
LinkReleaseContext,
} from "@comfyorg/litegraph";
import { import {
FilterAndValue, FilterAndValue,
NodeSearchService, NodeSearchService,
@@ -63,12 +69,47 @@ const closeDialog = () => {
clearFilters(); clearFilters();
visible.value = false; visible.value = false;
}; };
const connectNodeOnLinkRelease = (
node: LGraphNode,
context: LinkReleaseContext
) => {
const destIsInput = context.node_from !== undefined;
const srcNode = (
destIsInput ? context.node_from : context.node_to
) as LGraphNode;
const srcSlotIndex: number = context.slot_from.slot_index;
const linkDataType = destIsInput
? context.type_filter_in
: context.type_filter_out;
const destSlots = destIsInput ? node.inputs : node.outputs;
const destSlotIndex = destSlots.findIndex(
(slot: INodeSlot) => slot.type === linkDataType
);
if (destSlotIndex === -1) {
console.warn(
`Could not find slot with type ${linkDataType} on node ${node.title}`
);
return;
}
if (destIsInput) {
srcNode.connect(srcSlotIndex, node, destSlotIndex);
} else {
node.connect(destSlotIndex, srcNode, srcSlotIndex);
}
};
const addNode = (nodeDef: ComfyNodeDef) => { const addNode = (nodeDef: ComfyNodeDef) => {
closeDialog(); closeDialog();
const node = LiteGraph.createNode(nodeDef.name, nodeDef.display_name, {}); const node = LiteGraph.createNode(nodeDef.name, nodeDef.display_name, {});
if (node) { if (node) {
node.pos = getNewNodeLocation(); node.pos = getNewNodeLocation();
app.graph.add(node); app.graph.add(node);
const eventDetail = triggerEvent.value.detail;
if (eventDetail.subType === "empty-release") {
connectNodeOnLinkRelease(node, eventDetail.linkReleaseContext);
}
} }
}; };
const nodeSearchService = ( const nodeSearchService = (