mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-24 14:45:36 +00:00
## Summary
Fix part of the
#https://github.com/Comfy-Org/ComfyUI_frontend/issues/11092
A total of 4 `// @ts-expect-error` directives were removed across 3
files — all caused by PrimeVue's legacy `$el` access pattern (`const
inputElement = inputRef.value.$el`) — by replacing PrimeVue with
Reka-based UI components. 1 corresponding unit test file was added.
## Changes
### `src/components/ui/input/Input.vue`
- **Exposed APIs**: Extended `defineExpose` to include `blur()` and
`setSelectionRange()`. This allows parent components to programmatically
control input behavior without direct DOM manipulation.
### `src/components/ui/textarea/Textarea.vue`
- **Exposed APIs**: Added `focus()` via `defineExpose`.
- **Cleanup**: Removed redundant attribute spreading (`...restAttrs`) to
lean on Vue’s default `$attrs` inheritance, making the component more
predictable.
---
## Refactored Feature Components
### `WidgetMarkdown.vue` (Note/Markdown Widgets)
- **Dependency Swap**: Replaced `primevue/textarea` with local
`Textarea.vue`.
- **Logic Simplification**: Simplified focus logic from
`textareaRef.value?.$el?.focus()` to a typed
`textareaRef.value?.focus()`.
- **Code Style**: Converted arrow functions to function declarations and
removed redundant section comments.
### `PromptDialogContent.vue` (Generic Prompt Dialogs)
- **Component Update**: Replaced PrimeVue `FloatLabel` and `InputText`
with a native `<label>` and local `Input.vue`.
- **Vue 3.5 Adoption**: Implemented **Reactive Destructuring** for
props.
- **Conflict Resolution**: Renamed internal `onConfirm` handler to
`handleConfirm` to prevent collision with destructured props.
### `EditableText.vue` (Node Titles & Sidebar Items)
- **Style Modernization**: Removed `<style scoped>` block in favor of
**Tailwind CSS** utility classes (e.g., `inline`, `w-full`).
- **Clean Implementation**: Replaced PrimeVue PassThrough (`:pt`) logic
with standard `@blur` and `v-bind` attributes.
---
## Testing & Quality Assurance
### Updated Tests
- **Redundancy Removal**: Cleaned up `EditableText.test.ts` and
`WidgetMarkdown.test.ts` by removing unused PrimeVue global
registrations. All 34 existing behavioral tests remain passing.
### New Coverage
- **`PromptDialogContent.test.ts`**: Added 3 new tests to verify:
1. Correct initialization with `defaultValue`.
2. Value persistence when clicking the Confirm button.
3. Form submission via the `Enter` key.
---
## Manual Test Screenshot
All functions have passed testing.
<img width="594" height="530" alt="test5"
src="https://github.com/user-attachments/assets/46a6b3b2-1855-414e-ac78-65668052ce50"
/>
<img width="1190" height="1074" alt="test4"
src="https://github.com/user-attachments/assets/89aa61ab-9401-44c2-9eae-9ca8761df675"
/>
<img width="1154" height="1028" alt="test3"
src="https://github.com/user-attachments/assets/3f63cfdf-8fbd-4dd3-9e42-dbebe4d8d421"
/>
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Moderate risk because it swaps underlying input/textarea components
and ref handling (focus/blur/selection) in interactive UI paths
(editable labels, prompt dialogs, markdown editor), which could subtly
change keyboard/blur behavior.
>
> **Overview**
> Refactors several Vue components to stop using PrimeVue
`InputText`/`Textarea` (and `$el` access) in favor of the project’s
`Input`/`Textarea` components, updating bindings/events and Tailwind
classes accordingly.
>
> Extends the shared `Input` to expose `blur`, `setSelectionRange`, and
`selectAll`, and updates `Textarea` to expose `focus`, enabling callers
to manage focus/selection without DOM internals.
>
> Adds a new unit test suite for `PromptDialogContent` and simplifies
existing tests by removing PrimeVue plugin/component setup; the groups
e2e test replaces a screenshot assertion with a functional visibility
check for the new title input.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
9c97314d59. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11324-refactor-replace-PrimeVue-InputText-Textarea-with-project-UI-components-3456d73d36508109a18bc97a7d0487a7)
by [Unito](https://www.unito.io)
221 lines
6.7 KiB
TypeScript
221 lines
6.7 KiB
TypeScript
import {
|
|
comfyExpect as expect,
|
|
comfyPageFixture as test
|
|
} from '@e2e/fixtures/ComfyPage'
|
|
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
|
|
|
const CREATE_GROUP_HOTKEY = 'Control+g'
|
|
|
|
type NodeGroupCenteringError = {
|
|
horizontal: number
|
|
vertical: number
|
|
}
|
|
|
|
type NodeGroupCenteringErrors = {
|
|
innerGroup: NodeGroupCenteringError
|
|
outerGroup: NodeGroupCenteringError
|
|
}
|
|
|
|
const LEGACY_VUE_CENTERING_BASELINE: NodeGroupCenteringErrors = {
|
|
innerGroup: {
|
|
horizontal: 16.308832840862777,
|
|
vertical: 17.390899314547084
|
|
},
|
|
outerGroup: {
|
|
horizontal: 20.30164329441476,
|
|
vertical: 42.196324096481476
|
|
}
|
|
} as const
|
|
|
|
const CENTERING_TOLERANCE = {
|
|
innerGroup: 6,
|
|
outerGroup: 12
|
|
} as const
|
|
|
|
function expectWithinBaseline(
|
|
actual: number,
|
|
baseline: number,
|
|
tolerance: number
|
|
) {
|
|
expect(Math.abs(actual - baseline)).toBeLessThan(tolerance)
|
|
}
|
|
|
|
async function getNodeGroupCenteringErrors(
|
|
comfyPage: ComfyPage
|
|
): Promise<NodeGroupCenteringErrors> {
|
|
return comfyPage.page.evaluate(() => {
|
|
type GraphNode = {
|
|
id: number | string
|
|
pos: ReadonlyArray<number>
|
|
}
|
|
type GraphGroup = {
|
|
title: string
|
|
pos: ReadonlyArray<number>
|
|
size: ReadonlyArray<number>
|
|
}
|
|
|
|
const app = window.app!
|
|
const node = app.graph.nodes[0] as GraphNode | undefined
|
|
|
|
if (!node) {
|
|
throw new Error('Expected a node in the loaded workflow')
|
|
}
|
|
|
|
const nodeElement = document.querySelector<HTMLElement>(
|
|
`[data-node-id="${node.id}"]`
|
|
)
|
|
|
|
if (!nodeElement) {
|
|
throw new Error(`Vue node element not found for node ${node.id}`)
|
|
}
|
|
|
|
const groups = app.graph.groups as GraphGroup[]
|
|
const innerGroup = groups.find((group) => group.title === 'Inner Group')
|
|
const outerGroup = groups.find((group) => group.title === 'Outer Group')
|
|
|
|
if (!innerGroup || !outerGroup) {
|
|
throw new Error('Expected both Inner Group and Outer Group in graph')
|
|
}
|
|
|
|
const nodeRect = nodeElement.getBoundingClientRect()
|
|
|
|
const getCenteringError = (group: GraphGroup): NodeGroupCenteringError => {
|
|
const [groupStartX, groupStartY] = app.canvasPosToClientPos([
|
|
group.pos[0],
|
|
group.pos[1]
|
|
])
|
|
const [groupEndX, groupEndY] = app.canvasPosToClientPos([
|
|
group.pos[0] + group.size[0],
|
|
group.pos[1] + group.size[1]
|
|
])
|
|
|
|
const groupLeft = Math.min(groupStartX, groupEndX)
|
|
const groupRight = Math.max(groupStartX, groupEndX)
|
|
const groupTop = Math.min(groupStartY, groupEndY)
|
|
const groupBottom = Math.max(groupStartY, groupEndY)
|
|
|
|
const leftGap = nodeRect.left - groupLeft
|
|
const rightGap = groupRight - nodeRect.right
|
|
const topGap = nodeRect.top - groupTop
|
|
const bottomGap = groupBottom - nodeRect.bottom
|
|
|
|
return {
|
|
horizontal: Math.abs(leftGap - rightGap),
|
|
vertical: Math.abs(topGap - bottomGap)
|
|
}
|
|
}
|
|
|
|
return {
|
|
innerGroup: getCenteringError(innerGroup),
|
|
outerGroup: getCenteringError(outerGroup)
|
|
}
|
|
})
|
|
}
|
|
|
|
test.describe('Vue Node Groups', { tag: ['@screenshot', '@vue-nodes'] }, () => {
|
|
test.beforeEach(async ({ comfyPage }) => {
|
|
await comfyPage.settings.setSetting('Comfy.Minimap.ShowGroups', true)
|
|
})
|
|
|
|
test('should allow creating groups with hotkey', async ({ comfyPage }) => {
|
|
await comfyPage.page.getByText('Load Checkpoint').click()
|
|
await comfyPage.page.getByText('KSampler').click({ modifiers: ['Control'] })
|
|
await comfyPage.page.keyboard.press(CREATE_GROUP_HOTKEY)
|
|
await expect(comfyPage.page.getByTestId('node-title-input')).toBeVisible()
|
|
})
|
|
|
|
test('should allow fitting group to contents', async ({ comfyPage }) => {
|
|
await comfyPage.workflow.loadWorkflow('groups/oversized_group')
|
|
await comfyPage.keyboard.selectAll()
|
|
await comfyPage.command.executeCommand('Comfy.Graph.FitGroupToContents')
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'vue-groups-fit-to-contents.png'
|
|
)
|
|
})
|
|
|
|
test('should move nested groups together when dragging outer group', async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.workflow.loadWorkflow('groups/nested-groups-1-inner-node')
|
|
|
|
// Get initial positions with null guards
|
|
const outerInitial =
|
|
await comfyPage.canvasOps.getGroupPosition('Outer Group')
|
|
const innerInitial =
|
|
await comfyPage.canvasOps.getGroupPosition('Inner Group')
|
|
|
|
const initialOffsetX = innerInitial.x - outerInitial.x
|
|
const initialOffsetY = innerInitial.y - outerInitial.y
|
|
|
|
// Drag the outer group
|
|
const dragDelta = { x: 100, y: 80 }
|
|
await comfyPage.canvasOps.dragGroup({
|
|
name: 'Outer Group',
|
|
deltaX: dragDelta.x,
|
|
deltaY: dragDelta.y
|
|
})
|
|
|
|
// Use retrying assertion to wait for positions to update
|
|
await expect(async () => {
|
|
const outerFinal =
|
|
await comfyPage.canvasOps.getGroupPosition('Outer Group')
|
|
const innerFinal =
|
|
await comfyPage.canvasOps.getGroupPosition('Inner Group')
|
|
|
|
const finalOffsetX = innerFinal.x - outerFinal.x
|
|
const finalOffsetY = innerFinal.y - outerFinal.y
|
|
|
|
// Both groups should have moved
|
|
expect(outerFinal.x).not.toBe(outerInitial.x)
|
|
expect(innerFinal.x).not.toBe(innerInitial.x)
|
|
|
|
// The relative offset should be maintained (inner group moved with outer)
|
|
expect(finalOffsetX).toBeCloseTo(initialOffsetX, 0)
|
|
expect(finalOffsetY).toBeCloseTo(initialOffsetY, 0)
|
|
}).toPass({ timeout: 5000 })
|
|
})
|
|
|
|
test('should keep groups aligned after loading legacy Vue workflows', async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.workflow.loadWorkflow('groups/nested-groups-1-inner-node')
|
|
await comfyPage.vueNodes.waitForNodes(1)
|
|
|
|
await expect
|
|
.poll(() =>
|
|
comfyPage.page.evaluate(() => {
|
|
const extra = window.app!.graph.extra as
|
|
| { workflowRendererVersion?: string }
|
|
| undefined
|
|
return extra?.workflowRendererVersion
|
|
})
|
|
)
|
|
.toMatch(/^Vue/)
|
|
|
|
await expect(async () => {
|
|
const centeringErrors = await getNodeGroupCenteringErrors(comfyPage)
|
|
|
|
expectWithinBaseline(
|
|
centeringErrors.innerGroup.horizontal,
|
|
LEGACY_VUE_CENTERING_BASELINE.innerGroup.horizontal,
|
|
CENTERING_TOLERANCE.innerGroup
|
|
)
|
|
expectWithinBaseline(
|
|
centeringErrors.innerGroup.vertical,
|
|
LEGACY_VUE_CENTERING_BASELINE.innerGroup.vertical,
|
|
CENTERING_TOLERANCE.innerGroup
|
|
)
|
|
expectWithinBaseline(
|
|
centeringErrors.outerGroup.horizontal,
|
|
LEGACY_VUE_CENTERING_BASELINE.outerGroup.horizontal,
|
|
CENTERING_TOLERANCE.outerGroup
|
|
)
|
|
expectWithinBaseline(
|
|
centeringErrors.outerGroup.vertical,
|
|
LEGACY_VUE_CENTERING_BASELINE.outerGroup.vertical,
|
|
CENTERING_TOLERANCE.outerGroup
|
|
)
|
|
}).toPass({ timeout: 5000 })
|
|
})
|
|
})
|