Files
ComfyUI_frontend/browser_tests/tests/nodeHelp.spec.ts
Johnpaul Chiwetelu ac107b45ea Floating Selection Toolbox Improvements (#5218)
* WIP

* WIP: UI design for right click menu

* feat: add composable for node customization and information handling

* fix: correct v-show directive in MaskEditorButton and enhance MoreOptions functionality

* feat: add selection and subgraph operations composables for enhanced graph management

* fix: update computed properties to use 'void' for non-reactive calls and add MenuOptionItem component

* feat: add composables for More Options menu and submenu positioning logic

* feat: refactor MoreOptions component to use MenuOptionItem for menu rendering and streamline submenu handling

* feat: implement SubmenuPopover component for enhanced submenu functionality and selection handling

* feat: add 'More Options' label and enhance shape options in localization file

* refactor: simplify shape name handling by removing Pascal case conversion and using localized names

* refactor: enhance submenu handling by dynamically setting refs and improving key assignment

* feat: implement useNodeArrangement composable for node alignment and distribution functionality

* feat: enhance useMoreOptionsMenu with image node operations and alignment options

* feat: localize context menu options and enhance submenu handling

* refactor: improve type safety for title assignment in selection operations and enhance color option retrieval in node customization

* fix: adjust component order in SelectionToolbox for improved layout

* feat: update FrameNodes button visibility and tooltip, and add localization for frameNodes

* feat: enhance button visibility logic in SelectionToolbox based on selection types

* refactor: reorganize properties panel option in More Options menu for single nodes

* remove excessive logging and alerts

* fix component tests

* ad browser tests

* feat: enhance popover behavior in MoreOptions component to manage visibility state during selection overlay changes

* refactor: update visibility logic for buttons in SelectionToolbox and ExecuteButton components

* refactor: remove duplicate shape option and clean up shapeOptions array

* refactor: update help toggle logic in InfoButton and useMoreOptionsMenu to manage sidebar and help state

* refactor: streamline node info handling and integrate output node filtering in useNodeInfo and useMoreOptionsMenu

* Added useSelectionState composable consolidating all selection-derived state and the node help toggle

* Updated toolbox buttons (InfoButton, BookmarkButton, BypassButton, MaskEditorButton, ConvertToSubgraphButton, PinButton, DeleteButton, ColorPickerButton, ExecuteButton, FrameNodes, Load3DViewerButton) to remove duplicated selection logic and use useSelectionState

* Introduced HideReason ('manual' | 'drag') to differentiate drag-induced hides from manual/outside hides in MoreOptions

* refactor: enhance popover visibility handling during drag events using canvas state

* fix: update shape option name from 'default' to 'box' and add localization for 'box'

* refactor: streamline BypassButton logic and enhance MoreOptions menu with state bumping

* refactor: remove toast notifications from subgraph operations for cleaner logic

* refactor: ensure menu options re-compute when selection flags change

* feat: Enhance MoreOptions behavior with drag-and-drop support

* fix: Update mask icon class for consistent styling in MaskEditorButton

* refactor: Standardize icon sizes and classes across selection toolbox buttons

* refactor: Update layout and styling in SelectionToolbox and MoreOptions components

* refactor: Improve selection toolbox behavior with more options state management

* Refactor: Remove unused imports and conditionally add subgraph option in menu

* Enhance popover behavior: add show/hide event handlers and improve positioning logic

* Cleanup: Remove debug comments from popover functions for clarity

* Refactor: Clean up FrameNodes component and add MenuOptionBadge for better option display

* Cleanup: Remove debug comments from useSelectionToolboxPosition for clarity

* Add useFrameNodes composable for grouping selected nodes

* Refactor: Update shape options in useNodeCustomization and localize frame nodes label

* fix tests

* Cleanup: Remove packageManager entry from package.json

* Refactor: Replace ILucide icons with named imports from lucide-vue-next

* Refactor: Update shape selection and improve color picker behavior in selection toolbox

* Update test expectations [skip ci]

* feat: Enhance More Options Menu for group node management and update localization strings

* refactor: Comment out PublishButton

* refactor: Comment out test for bookmark button visibility in SelectionToolbox

* refactor: Update class names for dark theme compatibility in ExecuteButton and MenuOptionItem components

* refactor: Modularize menu options by creating dedicated composables for group, image, node, and selection operations

* refactor: Update selectors in tests to match design changes

* refactor: Update help button selector in Node Help tests

* refactor: Update getGroupColorOptions to accept groupContext and bump parameters

* Update test expectations [skip ci]

* refactor: Center KSampler node before interaction in More Options submenu tests

* refactor: Adjust KSampler node positioning and simplify button click in More Options submenu tests

* refactor: Rename comfyPageFixture import for clarity

* refactor: use gap-1 instead of the explicit gap-[4px]

* refactor: Replace app.canvas with canvasStore.getCanvas for  state management

* refactor: Simplify prop access by removing 'props.' prefix in MenuOptionItem component

* refactor: Remove explicit type annotation for item in buildSelectionSignature function

* refactor: Replace Lucide icons with string-based icon references in menu options

* refactor: Remove export from interface declarations for improved clarity

* refactor: Simplify class binding in BypassButton component for improved readability

* refactor: Update button class for consistent sizing in ExecuteButton component

* refactor: Update help button locator class for consistency in Node Help tests

* fix node help test

* refactor: Remove unused imports and simplify visibility conditions in selection toolbox components

* feat: Add 3D node selection logic and cleanup on unmount for selection toolbox

* refactor: Update help button locator to use consistent data-testid in Node Help tests

* fix: Correct help button locator syntax in Node Help tests

* refactor: Change resetMoreOptionsState to an internal function in useSelectionToolboxPosition

* test: Add Load3D node visibility logic for ColorPickerButton and remove redundant test case

* fix: Increase tooltip show delay for ColorPickerButton

* fix: Update selectedOutputNodes computation to filter by isLGraphNode

* fix: Remove unused nodeDef reference from InfoButton and submenu trigger from MenuOptionItem

* fix: Update showInfoButton logic to depend on nodeDef value

* refactor: Remove deprecated getBasicNodeOptions function for cleaner code

* refactor: Replace useNodeInfo with useSelectedNodeActions

* refactor: Integrate useNodeDefStore for improved node definition handling in SelectionToolbox and InfoButton tests

* refactor: Introduce useCanvasRefresh composable for consistent canvas refresh logic across node operations

* refactor: Remove irrelevant append-to attribute from Popover

* refactor: Use storeToRefs for selectedItems in useSelectionState and add tests for selection logic

* refactor: Update ExecuteButton to use hasOutputNodesSelected for visibility and remove unnecessary computed property

* refactor: move display of execution button tests to selectionToolbox

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-09-13 22:52:30 -07:00

554 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
comfyExpect as expect,
comfyPageFixture as test
} from '../fixtures/ComfyPage'
// TODO: there might be a better solution for this
// Helper function to pan canvas and select node
async function selectNodeWithPan(comfyPage: any, nodeRef: any) {
const nodePos = await nodeRef.getPosition()
await comfyPage.page.evaluate((pos) => {
const app = window['app']
const canvas = app.canvas
canvas.ds.offset[0] = -pos.x + canvas.canvas.width / 2
canvas.ds.offset[1] = -pos.y + canvas.canvas.height / 2 + 100
canvas.setDirty(true, true)
}, nodePos)
await comfyPage.nextFrame()
await nodeRef.click('title')
}
test.describe('Node Help', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setup()
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
})
test.describe('Selection Toolbox', () => {
test('Should open help menu for selected node', async ({ comfyPage }) => {
// Load a workflow with a node
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
await comfyPage.loadWorkflow('default')
// Select a single node (KSampler) using node references
const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler')
if (ksamplerNodes.length === 0) {
throw new Error('No KSampler nodes found in the workflow')
}
// Select the node with panning to ensure toolbox is visible
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
// Wait for selection toolbox to appear
await expect(comfyPage.selectionToolbox).toBeVisible()
// Click the help button in the selection toolbox
const helpButton = comfyPage.selectionToolbox.locator(
'button[data-testid="info-button"]'
)
await expect(helpButton).toBeVisible()
await helpButton.click()
// Verify that the node library sidebar is opened
await expect(
comfyPage.menu.nodeLibraryTab.selectedTabButton
).toBeVisible()
// Verify that the help page is shown for the correct node
const helpPage = comfyPage.page.locator('.sidebar-content-container')
await expect(helpPage).toContainText('KSampler')
await expect(helpPage.locator('.node-help-content')).toBeVisible()
})
})
test.describe('Node Library Sidebar', () => {
test('Should open help menu from node library', async ({ comfyPage }) => {
// Open the node library sidebar
await comfyPage.menu.nodeLibraryTab.open()
// Wait for node library to load
await expect(comfyPage.menu.nodeLibraryTab.nodeLibraryTree).toBeVisible()
// Search for KSampler to make it easier to find
await comfyPage.menu.nodeLibraryTab.nodeLibrarySearchBoxInput.fill(
'KSampler'
)
// Find the KSampler node in search results
const ksamplerNode = comfyPage.page
.locator('.tree-explorer-node-label')
.filter({ hasText: 'KSampler' })
.first()
await expect(ksamplerNode).toBeVisible()
// Hover over the node to show action buttons
await ksamplerNode.hover()
// Click the help button
const helpButton = ksamplerNode.locator('button:has(.pi-question)')
await expect(helpButton).toBeVisible()
await helpButton.click()
// Verify that the help page is shown
const helpPage = comfyPage.page.locator('.sidebar-content-container')
await expect(helpPage).toContainText('KSampler')
await expect(helpPage.locator('.node-help-content')).toBeVisible()
})
test('Should show node library tab when clicking back from help page', async ({
comfyPage
}) => {
// Open the node library sidebar
await comfyPage.menu.nodeLibraryTab.open()
// Wait for node library to load
await expect(comfyPage.menu.nodeLibraryTab.nodeLibraryTree).toBeVisible()
// Search for KSampler
await comfyPage.menu.nodeLibraryTab.nodeLibrarySearchBoxInput.fill(
'KSampler'
)
// Find and interact with the node
const ksamplerNode = comfyPage.page
.locator('.tree-explorer-node-label')
.filter({ hasText: 'KSampler' })
.first()
await ksamplerNode.hover()
const helpButton = ksamplerNode.locator('button:has(.pi-question)')
await helpButton.click()
// Verify help page is shown
const helpPage = comfyPage.page.locator('.sidebar-content-container')
await expect(helpPage).toContainText('KSampler')
// Click the back button - use a more specific selector
const backButton = comfyPage.page.locator('button:has(.pi-arrow-left)')
await expect(backButton).toBeVisible()
await backButton.click()
// Verify that we're back to the node library view
await expect(comfyPage.menu.nodeLibraryTab.nodeLibraryTree).toBeVisible()
await expect(
comfyPage.menu.nodeLibraryTab.nodeLibrarySearchBoxInput
).toBeVisible()
// Verify help page is no longer visible
await expect(helpPage.locator('.node-help-content')).not.toBeVisible()
})
})
test.describe('Help Content', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
})
test('Should display loading state while fetching help', async ({
comfyPage
}) => {
// Mock slow network response
await comfyPage.page.route('**/docs/**/*.md', async (route) => {
await new Promise((resolve) => setTimeout(resolve, 1000))
await route.fulfill({
status: 200,
body: '# Test Help Content\nThis is test help content.'
})
})
// Load workflow and select a node
await comfyPage.loadWorkflow('default')
const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler')
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
// Click help button
const helpButton = comfyPage.page.locator(
'.selection-toolbox button[data-testid="info-button"]'
)
await helpButton.click()
// Verify loading spinner is shown
const helpPage = comfyPage.page.locator('.sidebar-content-container')
await expect(helpPage.locator('.p-progressspinner')).toBeVisible()
// Wait for content to load
await expect(helpPage).toContainText('Test Help Content')
})
test('Should display fallback content when help file not found', async ({
comfyPage
}) => {
// Mock 404 response for help files
await comfyPage.page.route('**/docs/**/*.md', async (route) => {
await route.fulfill({
status: 404,
body: 'Not Found'
})
})
// Load workflow and select a node
await comfyPage.loadWorkflow('default')
const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler')
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
// Click help button
const helpButton = comfyPage.page.locator(
'.selection-toolbox button[data-testid="info-button"]'
)
await helpButton.click()
// Verify fallback content is shown (description, inputs, outputs)
const helpPage = comfyPage.page.locator('.sidebar-content-container')
await expect(helpPage).toContainText('Description')
await expect(helpPage).toContainText('Inputs')
await expect(helpPage).toContainText('Outputs')
})
test('Should render markdown with images correctly', async ({
comfyPage
}) => {
// Mock response with markdown containing images
await comfyPage.page.route('**/docs/KSampler/en.md', async (route) => {
await route.fulfill({
status: 200,
body: `# KSampler Documentation
![Example Image](example.jpg)
![External Image](https://example.com/image.png)
## Parameters
- **steps**: Number of steps
`
})
})
await comfyPage.loadWorkflow('default')
const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler')
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
const helpButton = comfyPage.page.locator(
'.selection-toolbox button[data-testid="info-button"]'
)
await helpButton.click()
const helpPage = comfyPage.page.locator('.sidebar-content-container')
await expect(helpPage).toContainText('KSampler Documentation')
// Check that relative image paths are prefixed correctly
const relativeImage = helpPage.locator('img[alt="Example Image"]')
await expect(relativeImage).toBeVisible()
await expect(relativeImage).toHaveAttribute(
'src',
/.*\/docs\/KSampler\/example\.jpg/
)
// Check that absolute URLs are not modified
const externalImage = helpPage.locator('img[alt="External Image"]')
await expect(externalImage).toHaveAttribute(
'src',
'https://example.com/image.png'
)
})
test('Should render video elements with source tags in markdown', async ({
comfyPage
}) => {
// Mock response with video elements
await comfyPage.page.route('**/docs/KSampler/en.md', async (route) => {
await route.fulfill({
status: 200,
body: `# KSampler Demo
<video src="demo.mp4" controls autoplay></video>
<video src="/absolute/video.mp4" controls></video>
<video controls>
<source src="video.mp4" type="video/mp4">
<source src="https://example.com/video.webm" type="video/webm">
</video>
`
})
})
await comfyPage.loadWorkflow('default')
const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler')
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
const helpButton = comfyPage.page.locator(
'.selection-toolbox button[data-testid="info-button"]'
)
await helpButton.click()
const helpPage = comfyPage.page.locator('.sidebar-content-container')
// Check relative video paths are prefixed
const relativeVideo = helpPage.locator('video[src*="demo.mp4"]')
await expect(relativeVideo).toBeVisible()
await expect(relativeVideo).toHaveAttribute(
'src',
/.*\/docs\/KSampler\/demo\.mp4/
)
await expect(relativeVideo).toHaveAttribute('controls', '')
await expect(relativeVideo).toHaveAttribute('autoplay', '')
// Check absolute paths are not modified
const absoluteVideo = helpPage.locator('video[src="/absolute/video.mp4"]')
await expect(absoluteVideo).toHaveAttribute('src', '/absolute/video.mp4')
// Check video source elements
const relativeVideoSource = helpPage.locator('source[src*="video.mp4"]')
await expect(relativeVideoSource).toHaveAttribute(
'src',
/.*\/docs\/KSampler\/video\.mp4/
)
const externalVideoSource = helpPage.locator(
'source[src="https://example.com/video.webm"]'
)
await expect(externalVideoSource).toHaveAttribute(
'src',
'https://example.com/video.webm'
)
})
test('Should handle custom node documentation paths', async ({
comfyPage
}) => {
// First load workflow with custom node
await comfyPage.loadWorkflow('groupnodes/group_node_v1.3.3')
// Mock custom node documentation with fallback
await comfyPage.page.route(
'**/extensions/*/docs/*/en.md',
async (route) => {
await route.fulfill({ status: 404 })
}
)
await comfyPage.page.route('**/extensions/*/docs/*.md', async (route) => {
await route.fulfill({
status: 200,
body: `# Custom Node Documentation
This is documentation for a custom node.
![Custom Image](assets/custom.png)
`
})
})
// Find and select a custom/group node
const nodeRefs = await comfyPage.page.evaluate(() => {
return window['app'].graph.nodes.map((n: any) => n.id)
})
if (nodeRefs.length > 0) {
const firstNode = await comfyPage.getNodeRefById(nodeRefs[0])
await selectNodeWithPan(comfyPage, firstNode)
}
const helpButton = comfyPage.page.locator(
'.selection-toolbox button[data-testid="info-button"]'
)
if (await helpButton.isVisible()) {
await helpButton.click()
const helpPage = comfyPage.page.locator('.sidebar-content-container')
await expect(helpPage).toContainText('Custom Node Documentation')
// Check image path for custom nodes
const image = helpPage.locator('img[alt="Custom Image"]')
await expect(image).toHaveAttribute(
'src',
/.*\/extensions\/.*\/docs\/assets\/custom\.png/
)
}
})
test('Should sanitize dangerous HTML content', async ({ comfyPage }) => {
// Mock response with potentially dangerous content
await comfyPage.page.route('**/docs/KSampler/en.md', async (route) => {
await route.fulfill({
status: 200,
body: `# Safe Content
<script>alert('XSS')</script>
<img src="x" onerror="alert('XSS')">
<a href="javascript:alert('XSS')">Dangerous Link</a>
<iframe src="evil.com"></iframe>
<!-- Safe content -->
<video src="safe.mp4" controls></video>
<img src="safe.jpg" alt="Safe Image">
`
})
})
await comfyPage.loadWorkflow('default')
const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler')
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
const helpButton = comfyPage.page.locator(
'.selection-toolbox button[data-testid="info-button"]'
)
await helpButton.click()
const helpPage = comfyPage.page.locator('.sidebar-content-container')
// Dangerous elements should be removed
await expect(helpPage.locator('script')).toHaveCount(0)
await expect(helpPage.locator('iframe')).toHaveCount(0)
// Check that onerror attribute is removed
const images = helpPage.locator('img')
const imageCount = await images.count()
for (let i = 0; i < imageCount; i++) {
const img = images.nth(i)
const onError = await img.getAttribute('onerror')
expect(onError).toBeNull()
}
// Check that javascript: links are sanitized
const links = helpPage.locator('a')
const linkCount = await links.count()
for (let i = 0; i < linkCount; i++) {
const link = links.nth(i)
const href = await link.getAttribute('href')
if (href !== null) {
expect(href).not.toContain('javascript:')
}
}
// Safe content should remain
await expect(helpPage.locator('video[src*="safe.mp4"]')).toBeVisible()
await expect(helpPage.locator('img[alt="Safe Image"]')).toBeVisible()
})
test('Should handle locale-specific documentation', async ({
comfyPage
}) => {
// Mock different responses for different locales
await comfyPage.page.route('**/docs/KSampler/ja.md', async (route) => {
await route.fulfill({
status: 200,
body: `# KSamplerード
これは日本語のドキュメントです。
`
})
})
await comfyPage.page.route('**/docs/KSampler/en.md', async (route) => {
await route.fulfill({
status: 200,
body: `# KSampler Node
This is English documentation.
`
})
})
// Set locale to Japanese
await comfyPage.setSetting('Comfy.Locale', 'ja')
await comfyPage.loadWorkflow('default')
const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler')
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
const helpButton = comfyPage.page.locator(
'.selection-toolbox button[data-testid="info-button"]'
)
await helpButton.click()
const helpPage = comfyPage.page.locator('.sidebar-content-container')
await expect(helpPage).toContainText('KSamplerード')
await expect(helpPage).toContainText('これは日本語のドキュメントです')
// Reset locale
await comfyPage.setSetting('Comfy.Locale', 'en')
})
test('Should handle network errors gracefully', async ({ comfyPage }) => {
// Mock network error
await comfyPage.page.route('**/docs/**/*.md', async (route) => {
await route.abort('failed')
})
await comfyPage.loadWorkflow('default')
const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler')
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
const helpButton = comfyPage.page.locator(
'.selection-toolbox button[data-testid="info-button"]'
)
await helpButton.click()
const helpPage = comfyPage.page.locator('.sidebar-content-container')
// Should show fallback content (node description)
await expect(helpPage).toBeVisible()
await expect(helpPage.locator('.p-progressspinner')).not.toBeVisible()
// Should show some content even on error
const content = await helpPage.textContent()
expect(content).toBeTruthy()
})
test('Should update help content when switching between nodes', async ({
comfyPage
}) => {
// Mock different help content for different nodes
await comfyPage.page.route('**/docs/KSampler/en.md', async (route) => {
await route.fulfill({
status: 200,
body: '# KSampler Help\n\nThis is KSampler documentation.'
})
})
await comfyPage.page.route(
'**/docs/CheckpointLoaderSimple/en.md',
async (route) => {
await route.fulfill({
status: 200,
body: '# Checkpoint Loader Help\n\nThis is Checkpoint Loader documentation.'
})
}
)
await comfyPage.loadWorkflow('default')
// Select KSampler first
const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler')
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
const helpButton = comfyPage.page.locator(
'.selection-toolbox button[data-testid="info-button"]'
)
await helpButton.click()
const helpPage = comfyPage.page.locator('.sidebar-content-container')
await expect(helpPage).toContainText('KSampler Help')
await expect(helpPage).toContainText('This is KSampler documentation')
// Now select Checkpoint Loader
const checkpointNodes = await comfyPage.getNodeRefsByType(
'CheckpointLoaderSimple'
)
await selectNodeWithPan(comfyPage, checkpointNodes[0])
// Click help button again
const helpButton2 = comfyPage.page.locator(
'.selection-toolbox button[data-testid="info-button"]'
)
await helpButton2.click()
// Content should update
await expect(helpPage).toContainText('Checkpoint Loader Help')
await expect(helpPage).toContainText(
'This is Checkpoint Loader documentation'
)
await expect(helpPage).not.toContainText('KSampler documentation')
})
})
})