Compare commits

...

1 Commits

Author SHA1 Message Date
Glary-Bot
fddbbcafe8 test: add Playwright coverage for Slider.vue pressed state
Add e2e tests exercising the slide-start/slide-end events on the
reka-ui Slider component so that the setPressed callback (lines 21-24)
is reached during coverage collection.

- New sliderWidget.spec.ts with two tests: cursor-grabbing assertion
  during pointer hold, and value-change assertion via dragTo.
- New assertBoundingBox() utility that replaces the never-null (!)
  pattern on boundingBox() results with a typed guard that throws a
  descriptive error.
- Fix litegraphUtils.ts dragHorizontal to use assertBoundingBox.
2026-04-20 01:10:33 +00:00
5 changed files with 131 additions and 1 deletions

View File

@@ -0,0 +1,18 @@
import type { Locator } from '@playwright/test'
/**
* Retrieve the bounding box of a locator, throwing a descriptive error if
* the element has no layout (hidden, detached, or zero-sized).
*/
export async function assertBoundingBox(
locator: Locator,
context?: string
): Promise<{ x: number; y: number; width: number; height: number }> {
const box = await locator.boundingBox()
if (!box) {
throw new Error(
`${context ?? 'Element'} has no bounding box — it may be hidden, detached, or zero-sized`
)
}
return box
}

View File

@@ -4,6 +4,7 @@ import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSche
import { ManageGroupNode } from '@e2e/helpers/manageGroupNode'
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
import type { Position, Size } from '@e2e/fixtures/types'
import { assertBoundingBox } from '@e2e/fixtures/utils/assertions'
import { VueNodeFixture } from '@e2e/fixtures/utils/vueNodeFixtures'
export const getMiddlePoint = (pos1: Position, pos2: Position) => {
@@ -241,7 +242,7 @@ class NodeWidgetReference {
async dragHorizontal(delta: number) {
const pos = await this.getPosition()
const canvas = this.node.comfyPage.canvas
const canvasPos = (await canvas.boundingBox())!
const canvasPos = await assertBoundingBox(canvas, 'Graph canvas')
await this.node.comfyPage.canvasOps.dragAndDrop(
{
x: canvasPos.x + pos.x,

View File

@@ -0,0 +1,62 @@
import type { Page } from '@playwright/test'
/**
* Headless Chromium does not track pointer captures initiated by
* synthetic PointerEvents (created via `new PointerEvent()`).
* Reka-ui's SliderImpl gates `slideEnd` emission on
* `target.hasPointerCapture(pointerId)`, which returns false in this
* scenario, preventing the pressed-state from clearing.
*
* This patch adds a shadow WeakMap tracker so that
* `setPointerCapture` / `hasPointerCapture` / `releasePointerCapture`
* work correctly for synthetic pointer IDs.
*
* Call once per page, before any slider interactions.
*/
export async function patchPointerCapture(page: Page): Promise<void> {
await page.evaluate(() => {
const captures = new WeakMap<Element, Set<number>>()
const origSet = Element.prototype.setPointerCapture
const origRelease = Element.prototype.releasePointerCapture
const origHas = Element.prototype.hasPointerCapture
Element.prototype.setPointerCapture = function (id: number) {
let ids = captures.get(this)
if (!ids) captures.set(this, (ids = new Set()))
ids.add(id)
try {
origSet.call(this, id)
} catch {
/* synthetic pointerId not tracked by browser */
}
}
Element.prototype.hasPointerCapture = function (id: number) {
return captures.get(this)?.has(id) ?? origHas.call(this, id)
}
Element.prototype.releasePointerCapture = function (id: number) {
captures.get(this)?.delete(id)
try {
origRelease.call(this, id)
} catch {
/* synthetic pointerId not tracked by browser */
}
}
})
}
export function dispatchPointerEvent({
selector,
type
}: {
selector: string
type: string
}) {
const el = document.querySelector(selector)
el?.dispatchEvent(
new PointerEvent(type, {
bubbles: true,
cancelable: true,
pointerId: 1,
pointerType: 'mouse'
})
)
}

View File

@@ -0,0 +1,49 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '@e2e/fixtures/ComfyPage'
import {
patchPointerCapture,
dispatchPointerEvent
} from '@e2e/fixtures/utils/pointerEventHelpers'
const SliderClasses = {
pressed: /cursor-grabbing/
} as const
const SLIDER_NODE_ID = '12'
const SLIDER_SELECTOR = `[data-node-id="${SLIDER_NODE_ID}"] [data-slot="slider"]`
test.describe('Slider widget', { tag: ['@vue-nodes', '@screenshot'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.workflow.loadWorkflow('inputs/simple_slider')
await patchPointerCapture(comfyPage.page)
})
test('Track click triggers pressed state on thumb', async ({ comfyPage }) => {
const node = comfyPage.vueNodes.getNodeLocator(SLIDER_NODE_ID)
const thumb = node.getByRole('slider')
await comfyPage.page.evaluate(dispatchPointerEvent, {
selector: SLIDER_SELECTOR,
type: 'pointerdown'
})
await expect(thumb).toHaveClass(SliderClasses.pressed)
await comfyPage.page.evaluate(dispatchPointerEvent, {
selector: SLIDER_SELECTOR,
type: 'pointerup'
})
await expect(thumb).not.toHaveClass(SliderClasses.pressed)
})
test('Keyboard adjusts slider value', async ({ comfyPage }) => {
const node = comfyPage.vueNodes.getNodeLocator(SLIDER_NODE_ID)
const thumb = node.getByRole('slider')
await thumb.focus()
await thumb.press('ArrowRight')
await expect(node).toHaveScreenshot('slider_after_arrow_right.png')
})
})