mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-20 06:44:32 +00:00
[feat] Add node title buttons with icon-only rendering (#1186)
This commit is contained in:
276
test/LGraphNode.titleButtons.test.ts
Normal file
276
test/LGraphNode.titleButtons.test.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
|
||||
import { LGraphButton } from "@/LGraphButton"
|
||||
import { LGraphCanvas } from "@/LGraphCanvas"
|
||||
import { LGraphNode } from "@/LGraphNode"
|
||||
|
||||
describe("LGraphNode Title Buttons", () => {
|
||||
describe("addTitleButton", () => {
|
||||
it("should add a title button to the node", () => {
|
||||
const node = new LGraphNode("Test Node")
|
||||
|
||||
const button = node.addTitleButton({
|
||||
name: "test_button",
|
||||
text: "X",
|
||||
fgColor: "#FF0000",
|
||||
})
|
||||
|
||||
expect(button).toBeInstanceOf(LGraphButton)
|
||||
expect(button.name).toBe("test_button")
|
||||
expect(button.text).toBe("X")
|
||||
expect(button.fgColor).toBe("#FF0000")
|
||||
expect(node.title_buttons).toHaveLength(1)
|
||||
expect(node.title_buttons[0]).toBe(button)
|
||||
})
|
||||
|
||||
it("should add multiple title buttons", () => {
|
||||
const node = new LGraphNode("Test Node")
|
||||
|
||||
const button1 = node.addTitleButton({ name: "button1", text: "A" })
|
||||
const button2 = node.addTitleButton({ name: "button2", text: "B" })
|
||||
const button3 = node.addTitleButton({ name: "button3", text: "C" })
|
||||
|
||||
expect(node.title_buttons).toHaveLength(3)
|
||||
expect(node.title_buttons[0]).toBe(button1)
|
||||
expect(node.title_buttons[1]).toBe(button2)
|
||||
expect(node.title_buttons[2]).toBe(button3)
|
||||
})
|
||||
|
||||
it("should create buttons with default options", () => {
|
||||
const node = new LGraphNode("Test Node")
|
||||
|
||||
const button = node.addTitleButton({})
|
||||
|
||||
expect(button).toBeInstanceOf(LGraphButton)
|
||||
expect(button.name).toBeUndefined()
|
||||
expect(node.title_buttons).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("onMouseDown with title buttons", () => {
|
||||
it("should handle click on title button", () => {
|
||||
const node = new LGraphNode("Test Node")
|
||||
node.pos = [100, 200]
|
||||
node.size = [180, 60]
|
||||
|
||||
const button = node.addTitleButton({
|
||||
name: "close_button",
|
||||
text: "X",
|
||||
visible: true,
|
||||
})
|
||||
|
||||
// Mock button dimensions
|
||||
button.getWidth = vi.fn().mockReturnValue(20)
|
||||
button.height = 16
|
||||
|
||||
// Simulate button being drawn to populate _last_area
|
||||
// Button is drawn at node-relative coordinates
|
||||
// Button x: node.size[0] - 5 - button_width = 180 - 5 - 20 = 155
|
||||
// Button y: -LiteGraph.NODE_TITLE_HEIGHT = -30
|
||||
button._last_area[0] = 155
|
||||
button._last_area[1] = -30
|
||||
button._last_area[2] = 20
|
||||
button._last_area[3] = 16
|
||||
|
||||
const canvas = {
|
||||
ctx: {} as CanvasRenderingContext2D,
|
||||
dispatch: vi.fn(),
|
||||
} as unknown as LGraphCanvas
|
||||
|
||||
const event = {
|
||||
canvasX: 265, // node.pos[0] + node.size[0] - 5 - button_width = 100 + 180 - 5 - 20 = 255, click in middle = 265
|
||||
canvasY: 178, // node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT + 8 = 200 - 30 + 8 = 178
|
||||
} as any
|
||||
|
||||
// Calculate node-relative position for the click
|
||||
const clickPosRelativeToNode: [number, number] = [
|
||||
265 - node.pos[0], // 265 - 100 = 165
|
||||
178 - node.pos[1], // 178 - 200 = -22
|
||||
]
|
||||
|
||||
// Simulate the click - onMouseDown should detect button click
|
||||
const handled = node.onMouseDown(event, clickPosRelativeToNode, canvas)
|
||||
|
||||
expect(handled).toBe(true)
|
||||
expect(canvas.dispatch).toHaveBeenCalledWith("litegraph:node-title-button-clicked", {
|
||||
node: node,
|
||||
button: button,
|
||||
})
|
||||
})
|
||||
|
||||
it("should not handle click outside title buttons", () => {
|
||||
const node = new LGraphNode("Test Node")
|
||||
node.pos = [100, 200]
|
||||
node.size = [180, 60]
|
||||
|
||||
const button = node.addTitleButton({
|
||||
name: "test_button",
|
||||
text: "T",
|
||||
visible: true,
|
||||
})
|
||||
|
||||
button.getWidth = vi.fn().mockReturnValue(20)
|
||||
button.height = 16
|
||||
|
||||
// Simulate button being drawn at node-relative coordinates
|
||||
button._last_area[0] = 155 // 180 - 5 - 20
|
||||
button._last_area[1] = -30 // -NODE_TITLE_HEIGHT
|
||||
button._last_area[2] = 20
|
||||
button._last_area[3] = 16
|
||||
|
||||
const canvas = {
|
||||
ctx: {} as CanvasRenderingContext2D,
|
||||
dispatch: vi.fn(),
|
||||
} as unknown as LGraphCanvas
|
||||
|
||||
const event = {
|
||||
canvasX: 150, // Click in the middle of the node, not on button
|
||||
canvasY: 180,
|
||||
} as any
|
||||
|
||||
// Calculate node-relative position
|
||||
const clickPosRelativeToNode: [number, number] = [
|
||||
150 - node.pos[0], // 150 - 100 = 50
|
||||
180 - node.pos[1], // 180 - 200 = -20
|
||||
]
|
||||
|
||||
const handled = node.onMouseDown(event, clickPosRelativeToNode, canvas)
|
||||
|
||||
expect(handled).toBe(false)
|
||||
expect(canvas.dispatch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should handle multiple buttons correctly", () => {
|
||||
const node = new LGraphNode("Test Node")
|
||||
node.pos = [100, 200]
|
||||
node.size = [200, 60]
|
||||
|
||||
const button1 = node.addTitleButton({
|
||||
name: "button1",
|
||||
text: "A",
|
||||
visible: true,
|
||||
})
|
||||
|
||||
const button2 = node.addTitleButton({
|
||||
name: "button2",
|
||||
text: "B",
|
||||
visible: true,
|
||||
})
|
||||
|
||||
// Mock button dimensions
|
||||
button1.getWidth = vi.fn().mockReturnValue(20)
|
||||
button2.getWidth = vi.fn().mockReturnValue(20)
|
||||
button1.height = button2.height = 16
|
||||
|
||||
// Simulate buttons being drawn at node-relative coordinates
|
||||
// First button (rightmost): 200 - 5 - 20 = 175
|
||||
button1._last_area[0] = 175
|
||||
button1._last_area[1] = -30 // -NODE_TITLE_HEIGHT
|
||||
button1._last_area[2] = 20
|
||||
button1._last_area[3] = 16
|
||||
|
||||
// Second button: 175 - 5 - 20 = 150
|
||||
button2._last_area[0] = 150
|
||||
button2._last_area[1] = -30 // -NODE_TITLE_HEIGHT
|
||||
button2._last_area[2] = 20
|
||||
button2._last_area[3] = 16
|
||||
|
||||
const canvas = {
|
||||
ctx: {} as CanvasRenderingContext2D,
|
||||
dispatch: vi.fn(),
|
||||
} as unknown as LGraphCanvas
|
||||
|
||||
// Click on second button (leftmost, since they're right-aligned)
|
||||
const titleY = 170 + 8 // node.pos[1] - NODE_TITLE_HEIGHT + 8 = 200 - 30 + 8 = 178
|
||||
const event = {
|
||||
canvasX: 255, // First button at: 100 + 200 - 5 - 20 = 275, Second button at: 275 - 5 - 20 = 250, click in middle = 255
|
||||
canvasY: titleY,
|
||||
} as any
|
||||
|
||||
// Calculate node-relative position
|
||||
const clickPosRelativeToNode: [number, number] = [
|
||||
255 - node.pos[0], // 255 - 100 = 155
|
||||
titleY - node.pos[1], // 178 - 200 = -22
|
||||
]
|
||||
|
||||
const handled = node.onMouseDown(event, clickPosRelativeToNode, canvas)
|
||||
|
||||
expect(handled).toBe(true)
|
||||
expect(canvas.dispatch).toHaveBeenCalledWith("litegraph:node-title-button-clicked", {
|
||||
node: node,
|
||||
button: button2,
|
||||
})
|
||||
})
|
||||
|
||||
it("should skip invisible buttons", () => {
|
||||
const node = new LGraphNode("Test Node")
|
||||
node.pos = [100, 200]
|
||||
node.size = [180, 60]
|
||||
|
||||
const button1 = node.addTitleButton({
|
||||
name: "invisible_button",
|
||||
text: "", // Empty text makes it invisible
|
||||
})
|
||||
|
||||
const button2 = node.addTitleButton({
|
||||
name: "visible_button",
|
||||
text: "V",
|
||||
})
|
||||
|
||||
button1.getWidth = vi.fn().mockReturnValue(20)
|
||||
button2.getWidth = vi.fn().mockReturnValue(20)
|
||||
button1.height = button2.height = 16
|
||||
|
||||
// Simulate buttons being drawn at node-relative coordinates
|
||||
// Only visible button gets drawn area
|
||||
button2._last_area[0] = 155 // 180 - 5 - 20
|
||||
button2._last_area[1] = -30 // -NODE_TITLE_HEIGHT
|
||||
button2._last_area[2] = 20
|
||||
button2._last_area[3] = 16
|
||||
|
||||
const canvas = {
|
||||
ctx: {} as CanvasRenderingContext2D,
|
||||
dispatch: vi.fn(),
|
||||
} as unknown as LGraphCanvas
|
||||
|
||||
// Click where the visible button is (invisible button is skipped)
|
||||
const titleY = 178 // node.pos[1] - NODE_TITLE_HEIGHT + 8 = 200 - 30 + 8 = 178
|
||||
const event = {
|
||||
canvasX: 265, // Visible button at: 100 + 180 - 5 - 20 = 255, click in middle = 265
|
||||
canvasY: titleY,
|
||||
} as any
|
||||
|
||||
// Calculate node-relative position
|
||||
const clickPosRelativeToNode: [number, number] = [
|
||||
265 - node.pos[0], // 265 - 100 = 165
|
||||
titleY - node.pos[1], // 178 - 200 = -22
|
||||
]
|
||||
|
||||
const handled = node.onMouseDown(event, clickPosRelativeToNode, canvas)
|
||||
|
||||
expect(handled).toBe(true)
|
||||
expect(canvas.dispatch).toHaveBeenCalledWith("litegraph:node-title-button-clicked", {
|
||||
node: node,
|
||||
button: button2, // Should click visible button, not invisible
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("onTitleButtonClick", () => {
|
||||
it("should dispatch litegraph:node-title-button-clicked event", () => {
|
||||
const node = new LGraphNode("Test Node")
|
||||
const button = new LGraphButton({ name: "test_button" })
|
||||
|
||||
const canvas = {
|
||||
dispatch: vi.fn(),
|
||||
} as unknown as LGraphCanvas
|
||||
|
||||
node.onTitleButtonClick(button, canvas)
|
||||
|
||||
expect(canvas.dispatch).toHaveBeenCalledWith("litegraph:node-title-button-clicked", {
|
||||
node: node,
|
||||
button: button,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user