mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 02:32:18 +00:00
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:
@@ -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 |
47
browser_tests/nodeSearchBox.spec.ts
Normal file
47
browser_tests/nodeSearchBox.spec.ts
Normal 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 |
@@ -36,6 +36,7 @@ export default defineConfig({
|
|||||||
{
|
{
|
||||||
name: 'chromium',
|
name: 'chromium',
|
||||||
use: { ...devices['Desktop Chrome'] },
|
use: { ...devices['Desktop Chrome'] },
|
||||||
|
timeout: 3000,
|
||||||
},
|
},
|
||||||
|
|
||||||
// {
|
// {
|
||||||
|
|||||||
@@ -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 = (
|
||||||
|
|||||||
Reference in New Issue
Block a user