Files
ComfyUI_frontend/src/components/sidebar/tabs/nodeLibrary/NodeTreeLeaf.vue
Alexander Brown f2d5bfab73 test(browser): refactor browser tests for reliability and maintainability (#8510)
## Summary

Major refactoring of browser tests to improve reliability,
maintainability, and type safety.

## Changes

### Test Infrastructure Decomposition
- Decomposed `ComfyPage.ts` (~1000 lines) into focused helpers:
- `CanvasHelper`, `DebugHelper`, `SubgraphHelper`,
`NodeOperationsHelper`
- `SettingsHelper`, `WorkflowHelper`, `ClipboardHelper`,
`KeyboardHelper`
- Created `ContextMenu` page object, `BaseDialog` base class, and
`BottomPanel` page object
- Extracted `DefaultGraphPositions` constants

### Locator Stability
- Added `data-testid` attributes to Vue components (sidebar, dialogs,
node library)
- Created centralized `selectors.ts` with test ID constants
- Replaced fragile CSS selectors (`.nth()`, `:nth-child()`) with
`getByTestId`/`getByRole`

### Performance & Reliability
- Removed `setTimeout` anti-patterns (replaced with `waitForFunction`)
- Replaced `waitForTimeout` with retrying assertions
- Replaced hardcoded coordinates with computed `NodeReference` positions
- Enforced LF line endings for all text files

### Type Safety
- Enabled `no-explicit-any` lint rule for browser_tests via oxlint
- Purged `as any` casts from browser_tests
- Added Window type augmentation for standardized window access
- Added proper type annotations throughout

### Bug Fixes
- Restored `ExtensionManager` API contract
- Removed test-only settings from production schema
- Fixed flaky selectors and missing test setup

## Testing
- All browser tests pass
- Typecheck passes


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Tests**
* Overhauled browser E2E test infrastructure with many new
helpers/fixtures, updated test APIs, and CI test container image bumped
for consistency.

* **Chores**
* Standardized line endings and applied stricter lint rules for browser
tests; workspace dependency version updated.

* **Documentation**
* Updated Playwright and TypeScript testing guidance and test-run
commands.

* **UI**
* Added stable data-testids to multiple components to improve
testability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-02-03 12:29:40 -08:00

217 lines
6.3 KiB
Vue

<template>
<div
ref="container"
class="node-lib-node-container"
data-testid="node-tree-leaf"
:data-node-name="nodeDef.display_name"
>
<TreeExplorerTreeNode :node="node" @contextmenu="handleContextMenu">
<template #before-label>
<Tag
v-if="nodeDef.experimental"
:value="$t('g.experimental')"
severity="primary"
/>
<Tag
v-if="nodeDef.deprecated"
:value="$t('g.deprecated')"
severity="danger"
/>
</template>
<template v-if="isUserBlueprint" #actions>
<Button
variant="destructive"
size="icon-sm"
:aria-label="$t('g.delete')"
@click.stop="deleteBlueprint"
>
<i class="icon-[lucide--trash-2] size-3.5" />
</Button>
<Button
variant="muted-textonly"
size="icon-sm"
:aria-label="$t('g.edit')"
@click.stop="editBlueprint"
>
<i class="icon-[lucide--square-pen] size-3.5" />
</Button>
</template>
<template v-else #actions>
<Button
class="bookmark-button"
variant="muted-textonly"
size="icon-sm"
:aria-label="$t('icon.bookmark')"
@click.stop="toggleBookmark"
>
<i
:class="
cn(
isBookmarked ? 'pi pi-bookmark-fill' : 'pi pi-bookmark',
'size-3.5'
)
"
/>
</Button>
<Button
v-tooltip.bottom="$t('g.learnMore')"
class="help-button"
variant="muted-textonly"
size="icon-sm"
:aria-label="$t('g.learnMore')"
@click.stop="onHelpClick"
>
<i class="pi pi-question size-3.5" />
</Button>
</template>
</TreeExplorerTreeNode>
<teleport v-if="isHovered" to="#node-library-node-preview-container">
<div class="node-lib-node-preview" :style="nodePreviewStyle">
<NodePreview :node-def="nodeDef" />
</div>
</teleport>
</div>
<ContextMenu ref="menu" :model="menuItems" />
</template>
<script setup lang="ts">
import ContextMenu from 'primevue/contextmenu'
import type { MenuItem } from 'primevue/menuitem'
import Tag from 'primevue/tag'
import type { CSSProperties } from 'vue'
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import TreeExplorerTreeNode from '@/components/common/TreeExplorerTreeNode.vue'
import NodePreview from '@/components/node/NodePreview.vue'
import Button from '@/components/ui/button/Button.vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
import { useSubgraphStore } from '@/stores/subgraphStore'
import type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
import { cn } from '@/utils/tailwindUtil'
const { t } = useI18n()
const props = defineProps<{
node: RenderedTreeExplorerNode<ComfyNodeDefImpl>
openNodeHelp: (nodeDef: ComfyNodeDefImpl) => void
}>()
// Note: node.data should be present for leaf nodes.
const nodeDef = computed(() => props.node.data!)
const nodeBookmarkStore = useNodeBookmarkStore()
const isBookmarked = computed(() =>
nodeBookmarkStore.isBookmarked(nodeDef.value)
)
const settingStore = useSettingStore()
const sidebarLocation = computed<'left' | 'right'>(() =>
settingStore.get('Comfy.Sidebar.Location')
)
const toggleBookmark = async () => {
await nodeBookmarkStore.toggleBookmark(nodeDef.value)
}
const onHelpClick = () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'node_library_help_button'
})
props.openNodeHelp(nodeDef.value)
}
const editBlueprint = async () => {
if (!props.node.data)
throw new Error(
'Failed to edit subgraph blueprint lacking backing node data'
)
await useSubgraphStore().editBlueprint(props.node.data.name)
}
const menu = ref<InstanceType<typeof ContextMenu> | null>(null)
const subgraphStore = useSubgraphStore()
const isUserBlueprint = computed(() => {
const name = nodeDef.value.name
if (!name.startsWith(subgraphStore.typePrefix)) return false
return !subgraphStore.isGlobalBlueprint(
name.slice(subgraphStore.typePrefix.length)
)
})
const menuItems = computed<MenuItem[]>(() => {
if (!isUserBlueprint.value) return []
return [
{
label: t('g.delete'),
icon: 'pi pi-trash',
severity: 'error',
command: deleteBlueprint
}
]
})
function handleContextMenu(event: Event) {
if (!isUserBlueprint.value) return
menu.value?.show(event)
}
function deleteBlueprint() {
if (!props.node.data) return
void subgraphStore.deleteBlueprint(props.node.data.name)
}
const nodePreviewStyle = ref<CSSProperties>({
position: 'fixed',
top: '0px',
left: '0px',
pointerEvents: 'none',
zIndex: 1001
})
const handleNodeHover = async () => {
const hoverTarget = nodeContentElement.value
if (!hoverTarget) return
const targetRect = hoverTarget.getBoundingClientRect()
const margin = 40
nodePreviewStyle.value.top = `${targetRect.top}px`
nodePreviewStyle.value.left =
sidebarLocation.value === 'left'
? `${targetRect.right + margin}px`
: `${targetRect.left - margin}px`
nodePreviewStyle.value.transform =
sidebarLocation.value === 'right' ? 'translateX(-100%)' : undefined
}
const container = ref<HTMLElement | null>(null)
const nodeContentElement = ref<HTMLElement | null>(null)
const isHovered = ref(false)
const handleMouseEnter = async () => {
isHovered.value = true
await nextTick()
await handleNodeHover()
}
const handleMouseLeave = () => {
isHovered.value = false
}
onMounted(() => {
nodeContentElement.value =
container.value?.closest('.p-tree-node-content') ?? null
nodeContentElement.value?.addEventListener('mouseenter', handleMouseEnter)
nodeContentElement.value?.addEventListener('mouseleave', handleMouseLeave)
})
onUnmounted(() => {
nodeContentElement.value?.removeEventListener('mouseenter', handleMouseEnter)
nodeContentElement.value?.removeEventListener('mouseleave', handleMouseLeave)
})
</script>
<style scoped>
@reference '../../../../assets/css/style.css';
.node-lib-node-container {
@apply h-full w-full;
}
</style>