mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
## Summary - Add e2e test covering node copy/paste (Ctrl+C/V), image paste onto a selected LoadImage node, and image paste on empty canvas creating a new LoadImage node - Extract `simulateImagePaste` into reusable `ClipboardHelper.pasteFile()` with auto-detected filename and MIME type - Add test workflow asset `load_image_with_ksampler.json` and `image32x32.webp` image asset ## Test plan - [ ] `pnpm typecheck:browser` passes - [ ] `pnpm exec eslint browser_tests/tests/copyPaste.spec.ts` passes - [ ] New test passes in CI: `Copy paste node, image paste onto LoadImage, image paste on empty canvas` - [ ] Existing copy/paste tests unaffected ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10233-test-add-copy-paste-node-and-image-paste-e2e-tests-3276d73d365081e78bb9f3f1bf34389f) by [Unito](https://www.unito.io)
162 lines
4.2 KiB
TypeScript
162 lines
4.2 KiB
TypeScript
import { readFileSync } from 'fs'
|
|
|
|
import type { Page } from '@playwright/test'
|
|
|
|
import type { Position } from '../types'
|
|
import { getMimeType } from './mimeTypeUtil'
|
|
|
|
export class DragDropHelper {
|
|
constructor(
|
|
private readonly page: Page,
|
|
private readonly assetPath: (fileName: string) => string
|
|
) {}
|
|
|
|
private async nextFrame(): Promise<void> {
|
|
await this.page.evaluate(() => {
|
|
return new Promise<void>((resolve) => {
|
|
requestAnimationFrame(() => resolve())
|
|
})
|
|
})
|
|
}
|
|
|
|
async dragAndDropExternalResource(
|
|
options: {
|
|
fileName?: string
|
|
url?: string
|
|
dropPosition?: Position
|
|
waitForUpload?: boolean
|
|
} = {}
|
|
): Promise<void> {
|
|
const {
|
|
dropPosition = { x: 100, y: 100 },
|
|
fileName,
|
|
url,
|
|
waitForUpload = false
|
|
} = options
|
|
|
|
if (!fileName && !url)
|
|
throw new Error('Must provide either fileName or url')
|
|
|
|
const evaluateParams: {
|
|
dropPosition: Position
|
|
fileName?: string
|
|
fileType?: string
|
|
buffer?: Uint8Array | number[]
|
|
url?: string
|
|
} = { dropPosition }
|
|
|
|
if (fileName) {
|
|
const filePath = this.assetPath(fileName)
|
|
const buffer = readFileSync(filePath)
|
|
|
|
evaluateParams.fileName = fileName
|
|
evaluateParams.fileType = getMimeType(fileName)
|
|
evaluateParams.buffer = [...new Uint8Array(buffer)]
|
|
}
|
|
|
|
if (url) evaluateParams.url = url
|
|
|
|
const uploadResponsePromise = waitForUpload
|
|
? this.page.waitForResponse(
|
|
(resp) => resp.url().includes('/upload/') && resp.status() === 200,
|
|
{ timeout: 10000 }
|
|
)
|
|
: null
|
|
|
|
await this.page.evaluate(async (params) => {
|
|
const dataTransfer = new DataTransfer()
|
|
|
|
if (params.buffer && params.fileName && params.fileType) {
|
|
const file = new File(
|
|
[new Uint8Array(params.buffer)],
|
|
params.fileName,
|
|
{
|
|
type: params.fileType
|
|
}
|
|
)
|
|
dataTransfer.items.add(file)
|
|
}
|
|
|
|
if (params.url) {
|
|
dataTransfer.setData('text/uri-list', params.url)
|
|
dataTransfer.setData('text/x-moz-url', params.url)
|
|
}
|
|
|
|
const targetElement = document.elementFromPoint(
|
|
params.dropPosition.x,
|
|
params.dropPosition.y
|
|
)
|
|
|
|
if (!targetElement) {
|
|
throw new Error(
|
|
`No element found at drop position: (${params.dropPosition.x}, ${params.dropPosition.y}). ` +
|
|
`document.elementFromPoint returned null. Ensure the target is visible and not obscured.`
|
|
)
|
|
}
|
|
|
|
const eventOptions = {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
dataTransfer,
|
|
clientX: params.dropPosition.x,
|
|
clientY: params.dropPosition.y
|
|
}
|
|
|
|
const dragOverEvent = new DragEvent('dragover', eventOptions)
|
|
const dropEvent = new DragEvent('drop', eventOptions)
|
|
|
|
const graphCanvasElement = document.querySelector('#graph-canvas')
|
|
|
|
// Keep Litegraph's drag-over node tracking in sync when the drop target is a
|
|
// Vue node DOM overlay outside of the graph canvas element.
|
|
if (graphCanvasElement && !graphCanvasElement.contains(targetElement)) {
|
|
graphCanvasElement.dispatchEvent(
|
|
new DragEvent('dragover', eventOptions)
|
|
)
|
|
}
|
|
|
|
Object.defineProperty(dropEvent, 'preventDefault', {
|
|
value: () => {},
|
|
writable: false
|
|
})
|
|
|
|
Object.defineProperty(dropEvent, 'stopPropagation', {
|
|
value: () => {},
|
|
writable: false
|
|
})
|
|
|
|
targetElement.dispatchEvent(dragOverEvent)
|
|
targetElement.dispatchEvent(dropEvent)
|
|
|
|
return {
|
|
success: true,
|
|
targetInfo: {
|
|
tagName: targetElement.tagName,
|
|
id: targetElement.id,
|
|
classList: Array.from(targetElement.classList)
|
|
}
|
|
}
|
|
}, evaluateParams)
|
|
|
|
if (uploadResponsePromise) {
|
|
await uploadResponsePromise
|
|
}
|
|
|
|
await this.nextFrame()
|
|
}
|
|
|
|
async dragAndDropFile(
|
|
fileName: string,
|
|
options: { dropPosition?: Position; waitForUpload?: boolean } = {}
|
|
): Promise<void> {
|
|
return this.dragAndDropExternalResource({ fileName, ...options })
|
|
}
|
|
|
|
async dragAndDropURL(
|
|
url: string,
|
|
options: { dropPosition?: Position } = {}
|
|
): Promise<void> {
|
|
return this.dragAndDropExternalResource({ url, ...options })
|
|
}
|
|
}
|