mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-21 21:09:00 +00:00
Compare commits
4 Commits
glary/oxli
...
litegraph/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07a5fbe16d | ||
|
|
dfeeb7b49a | ||
|
|
fcb7e01259 | ||
|
|
dad1eaddd5 |
@@ -41,10 +41,6 @@ ALGOLIA_API_KEY=684d998c36b67a9a9fce8fc2d8860579
|
||||
# Enable PostHog debug logging in the browser console.
|
||||
# VITE_POSTHOG_DEBUG=true
|
||||
|
||||
# Override staging comfy-api / comfy-platform base URLs.
|
||||
# VITE_STAGING_API_BASE_URL=https://stagingapi.comfy.org
|
||||
# VITE_STAGING_PLATFORM_BASE_URL=https://stagingplatform.comfy.org
|
||||
|
||||
# Sentry ENV vars replace with real ones for debugging
|
||||
# SENTRY_AUTH_TOKEN=private-token # get from sentry
|
||||
# SENTRY_ORG=comfy-org
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
],
|
||||
"rules": {
|
||||
"no-async-promise-executor": "off",
|
||||
"func-style": ["error", "declaration"],
|
||||
"no-console": [
|
||||
"error",
|
||||
{
|
||||
@@ -125,12 +124,6 @@
|
||||
"no-console": "allow"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["src/lib/litegraph/**"],
|
||||
"rules": {
|
||||
"func-style": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["browser_tests/**/*.ts"],
|
||||
"jsPlugins": ["eslint-plugin-playwright"],
|
||||
|
||||
@@ -47,7 +47,7 @@ setup((app) => {
|
||||
})
|
||||
|
||||
// Theme and dialog decorator
|
||||
export function withTheme(Story: StoryFn, context: StoryContext) {
|
||||
export const withTheme = (Story: StoryFn, context: StoryContext) => {
|
||||
const theme = context.globals.theme || 'light'
|
||||
|
||||
// Apply theme class to document root
|
||||
|
||||
@@ -40,7 +40,7 @@ setup((app) => {
|
||||
app.use(ToastService)
|
||||
})
|
||||
|
||||
export function withTheme(Story: StoryFn, context: StoryContext) {
|
||||
export const withTheme = (Story: StoryFn, context: StoryContext) => {
|
||||
const theme = context.globals.theme || 'light'
|
||||
if (theme === 'dark') {
|
||||
document.documentElement.classList.add('dark-theme')
|
||||
|
||||
@@ -58,7 +58,7 @@ const tooltipText = computed(() => {
|
||||
: t('serverStart.copyAllTooltip')
|
||||
})
|
||||
|
||||
async function handleCopy() {
|
||||
const handleCopy = async () => {
|
||||
const existingSelection = terminal.getSelection()
|
||||
const shouldSelectAll = !existingSelection
|
||||
if (shouldSelectAll) terminal.selectAll()
|
||||
@@ -76,7 +76,7 @@ async function handleCopy() {
|
||||
}
|
||||
}
|
||||
|
||||
function showContextMenu(event: MouseEvent) {
|
||||
const showContextMenu = (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
electronAPI()?.showContextMenu({ type: 'text' })
|
||||
}
|
||||
|
||||
@@ -44,9 +44,8 @@ const emit = defineEmits<{
|
||||
|
||||
const validationState = ref<ValidationState>(ValidationState.IDLE)
|
||||
|
||||
function cleanInput(value: string): string {
|
||||
return value ? value.replace(/\s+/g, '') : ''
|
||||
}
|
||||
const cleanInput = (value: string): string =>
|
||||
value ? value.replace(/\s+/g, '') : ''
|
||||
|
||||
// Add internal value state
|
||||
const internalValue = ref(cleanInput(props.modelValue))
|
||||
@@ -69,14 +68,14 @@ onMounted(async () => {
|
||||
await validateUrl(props.modelValue)
|
||||
})
|
||||
|
||||
function handleInput(value: string | undefined) {
|
||||
const handleInput = (value: string | undefined) => {
|
||||
// Update internal value without emitting
|
||||
internalValue.value = cleanInput(value ?? '')
|
||||
// Reset validation state when user types
|
||||
validationState.value = ValidationState.IDLE
|
||||
}
|
||||
|
||||
async function handleBlur() {
|
||||
const handleBlur = async () => {
|
||||
const input = cleanInput(internalValue.value)
|
||||
|
||||
let normalizedUrl = input
|
||||
@@ -92,7 +91,7 @@ async function handleBlur() {
|
||||
}
|
||||
|
||||
// Default validation implementation
|
||||
async function defaultValidateUrl(url: string): Promise<boolean> {
|
||||
const defaultValidateUrl = async (url: string): Promise<boolean> => {
|
||||
if (!isValidUrl(url)) return false
|
||||
try {
|
||||
return await checkUrlReachable(url)
|
||||
@@ -101,7 +100,7 @@ async function defaultValidateUrl(url: string): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
async function validateUrl(value: string) {
|
||||
const validateUrl = async (value: string) => {
|
||||
if (validationState.value === ValidationState.LOADING) return
|
||||
|
||||
const url = cleanInput(value)
|
||||
|
||||
@@ -127,7 +127,7 @@ const showDialog = ref(false)
|
||||
const autoUpdate = defineModel<boolean>('autoUpdate', { required: true })
|
||||
const allowMetrics = defineModel<boolean>('allowMetrics', { required: true })
|
||||
|
||||
function showMetricsInfo() {
|
||||
const showMetricsInfo = () => {
|
||||
showDialog.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -182,12 +182,10 @@ function getTorchMirrorItem(device: TorchDeviceType): UVMirror {
|
||||
}
|
||||
|
||||
const userIsInChina = ref(false)
|
||||
function useFallbackMirror(mirror: UVMirror) {
|
||||
return {
|
||||
...mirror,
|
||||
mirror: mirror.fallbackMirror
|
||||
}
|
||||
}
|
||||
const useFallbackMirror = (mirror: UVMirror) => ({
|
||||
...mirror,
|
||||
mirror: mirror.fallbackMirror
|
||||
})
|
||||
|
||||
const mirrors = computed<[UVMirror, ModelRef<string>][]>(() =>
|
||||
(
|
||||
@@ -214,7 +212,7 @@ onMounted(async () => {
|
||||
userIsInChina.value = await isInChina()
|
||||
})
|
||||
|
||||
async function validatePath(path: string | undefined) {
|
||||
const validatePath = async (path: string | undefined) => {
|
||||
try {
|
||||
pathError.value = ''
|
||||
pathExists.value = false
|
||||
@@ -248,7 +246,7 @@ async function validatePath(path: string | undefined) {
|
||||
}
|
||||
}
|
||||
|
||||
async function browsePath() {
|
||||
const browsePath = async () => {
|
||||
try {
|
||||
const result = await electron.showDirectoryPicker()
|
||||
if (result) {
|
||||
@@ -260,7 +258,7 @@ async function browsePath() {
|
||||
}
|
||||
}
|
||||
|
||||
async function onFocus() {
|
||||
const onFocus = async () => {
|
||||
if (!inputTouched.value) {
|
||||
inputTouched.value = true
|
||||
return
|
||||
|
||||
@@ -92,7 +92,7 @@ const isValidSource = computed(
|
||||
() => sourcePath.value !== '' && pathError.value === ''
|
||||
)
|
||||
|
||||
async function validateSource(sourcePath: string | undefined) {
|
||||
const validateSource = async (sourcePath: string | undefined) => {
|
||||
if (!sourcePath) {
|
||||
pathError.value = ''
|
||||
return
|
||||
@@ -109,7 +109,7 @@ async function validateSource(sourcePath: string | undefined) {
|
||||
}
|
||||
}
|
||||
|
||||
async function browsePath() {
|
||||
const browsePath = async () => {
|
||||
try {
|
||||
const result = await electron.showDirectoryPicker()
|
||||
if (result) {
|
||||
|
||||
@@ -82,7 +82,7 @@ const isExecuting = useMinLoadingDurationRef(reactiveExecuting, 250)
|
||||
// Popover
|
||||
const infoPopover = ref<InstanceType<typeof Popover> | null>(null)
|
||||
|
||||
function toggle(event: Event) {
|
||||
const toggle = (event: Event) => {
|
||||
infoPopover.value?.toggle(event)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -67,7 +67,7 @@ defineProps<{
|
||||
filter: MaintenanceFilter
|
||||
}>()
|
||||
|
||||
async function executeTask(task: MaintenanceTask) {
|
||||
const executeTask = async (task: MaintenanceTask) => {
|
||||
let message: string | undefined
|
||||
|
||||
try {
|
||||
@@ -87,7 +87,7 @@ async function executeTask(task: MaintenanceTask) {
|
||||
}
|
||||
|
||||
// Commands
|
||||
async function confirmButton(event: MouseEvent, task: MaintenanceTask) {
|
||||
const confirmButton = async (event: MouseEvent, task: MaintenanceTask) => {
|
||||
if (!task.requireConfirm) {
|
||||
await executeTask(task)
|
||||
return
|
||||
|
||||
@@ -34,10 +34,10 @@ const buffer = useTerminalBuffer()
|
||||
let xterm: Terminal | null = null
|
||||
|
||||
// Created and destroyed with the Drawer - contents copied from hidden buffer
|
||||
function terminalCreated(
|
||||
const terminalCreated = (
|
||||
{ terminal, useAutoSize }: ReturnType<typeof useTerminal>,
|
||||
root: Ref<HTMLElement | undefined>
|
||||
) {
|
||||
) => {
|
||||
xterm = terminal
|
||||
useAutoSize({ root, autoRows: true, autoCols: true })
|
||||
terminal.write(props.defaultMessage)
|
||||
@@ -49,7 +49,7 @@ function terminalCreated(
|
||||
terminal.options.disableStdin = true
|
||||
}
|
||||
|
||||
function terminalUnmounted() {
|
||||
const terminalUnmounted = () => {
|
||||
xterm = null
|
||||
}
|
||||
|
||||
|
||||
@@ -55,14 +55,14 @@ export function useTerminal(element: Ref<HTMLElement | undefined>) {
|
||||
minRows?: number
|
||||
onResize?: () => void
|
||||
}) {
|
||||
function ensureValidRows(rows: number | undefined): number {
|
||||
const ensureValidRows = (rows: number | undefined): number => {
|
||||
if (rows == null || isNaN(rows)) {
|
||||
return (root.value?.clientHeight ?? 80) / 20
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
function ensureValidCols(cols: number | undefined): number {
|
||||
const ensureValidCols = (cols: number | undefined): number => {
|
||||
if (cols == null || isNaN(cols)) {
|
||||
// Sometimes this is NaN if so, estimate.
|
||||
return (root.value?.clientWidth ?? 80) / 8
|
||||
@@ -70,7 +70,7 @@ export function useTerminal(element: Ref<HTMLElement | undefined>) {
|
||||
return cols
|
||||
}
|
||||
|
||||
function resize() {
|
||||
const resize = () => {
|
||||
const dims = fitAddon.proposeDimensions()
|
||||
// Sometimes propose returns NaN, so we may need to estimate.
|
||||
terminal.resize(
|
||||
|
||||
@@ -6,17 +6,13 @@ export function useTerminalBuffer() {
|
||||
const serializeAddon = new SerializeAddon()
|
||||
const terminal = markRaw(new Terminal({ convertEol: true }))
|
||||
|
||||
function copyTo(destinationTerminal: Terminal) {
|
||||
const copyTo = (destinationTerminal: Terminal) => {
|
||||
destinationTerminal.write(serializeAddon.serialize())
|
||||
}
|
||||
|
||||
function write(message: string) {
|
||||
return terminal.write(message)
|
||||
}
|
||||
const write = (message: string) => terminal.write(message)
|
||||
|
||||
function serialize() {
|
||||
return serializeAddon.serialize()
|
||||
}
|
||||
const serialize = () => serializeAddon.serialize()
|
||||
|
||||
onMounted(() => {
|
||||
terminal.loadAddon(serializeAddon)
|
||||
|
||||
@@ -5,7 +5,7 @@ import { electronAPI } from '@/utils/envUtil'
|
||||
|
||||
const electron = electronAPI()
|
||||
|
||||
function openUrl(url: string) {
|
||||
const openUrl = (url: string) => {
|
||||
window.open(url, '_blank')
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -124,15 +124,13 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
|
||||
* @param task Task to get the matching state object for
|
||||
* @returns The state object for this task
|
||||
*/
|
||||
function getRunner(task: MaintenanceTask) {
|
||||
return taskRunners.value.get(task.id)!
|
||||
}
|
||||
const getRunner = (task: MaintenanceTask) => taskRunners.value.get(task.id)!
|
||||
|
||||
/**
|
||||
* Updates the task list with the latest validation state.
|
||||
* @param validationUpdate Update details passed in by electron
|
||||
*/
|
||||
function processUpdate(validationUpdate: InstallValidation) {
|
||||
const processUpdate = (validationUpdate: InstallValidation) => {
|
||||
lastUpdate.value = validationUpdate
|
||||
const update = validationUpdate as IndexedUpdate
|
||||
isRefreshing.value = true
|
||||
@@ -153,19 +151,19 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
|
||||
}
|
||||
|
||||
/** Clears the resolved status of tasks (when changing filters) */
|
||||
function clearResolved() {
|
||||
const clearResolved = () => {
|
||||
for (const task of tasks.value) {
|
||||
getRunner(task).resolved &&= false
|
||||
}
|
||||
}
|
||||
|
||||
/** @todo Refreshes Electron tasks only. */
|
||||
async function refreshDesktopTasks() {
|
||||
const refreshDesktopTasks = async () => {
|
||||
isRefreshing.value = true
|
||||
await electron.Validation.validateInstallation(processUpdate)
|
||||
}
|
||||
|
||||
async function execute(task: MaintenanceTask) {
|
||||
const execute = async (task: MaintenanceTask) => {
|
||||
const success = await getRunner(task).execute(task)
|
||||
if (success && task.isInstallationFix) {
|
||||
await refreshDesktopTasks()
|
||||
|
||||
@@ -7,7 +7,7 @@ import { electronAPI } from './envUtil'
|
||||
* @param mirror - The mirror to check.
|
||||
* @returns True if the mirror is reachable, false otherwise.
|
||||
*/
|
||||
export async function checkMirrorReachable(mirror: string) {
|
||||
export const checkMirrorReachable = async (mirror: string) => {
|
||||
return (
|
||||
isValidUrl(mirror) && (await electronAPI().NetWork.canAccessUrl(mirror))
|
||||
)
|
||||
|
||||
@@ -36,7 +36,7 @@ import { electronAPI } from '@/utils/envUtil'
|
||||
const route = useRoute()
|
||||
const { id, title, message, buttons } = getDialog(route.params.dialogId)
|
||||
|
||||
async function handleButtonClick(button: DialogAction) {
|
||||
const handleButtonClick = async (button: DialogAction) => {
|
||||
await electronAPI().Dialog.clickButton(button.returnValue)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -52,7 +52,7 @@ const electron = electronAPI()
|
||||
|
||||
const terminalVisible = ref(false)
|
||||
|
||||
function toggleConsoleDrawer() {
|
||||
const toggleConsoleDrawer = () => {
|
||||
terminalVisible.value = !terminalVisible.value
|
||||
}
|
||||
|
||||
|
||||
@@ -47,11 +47,11 @@ import { useRouter } from 'vue-router'
|
||||
|
||||
import BaseViewTemplate from '@/views/templates/BaseViewTemplate.vue'
|
||||
|
||||
function openGitDownloads() {
|
||||
const openGitDownloads = () => {
|
||||
window.open('https://git-scm.com/downloads/', '_blank')
|
||||
}
|
||||
|
||||
async function skipGit() {
|
||||
const skipGit = async () => {
|
||||
console.warn('pushing')
|
||||
const router = useRouter()
|
||||
await router.push('install')
|
||||
|
||||
@@ -8,8 +8,8 @@ import { createMemoryHistory, createRouter } from 'vue-router'
|
||||
import InstallView from './InstallView.vue'
|
||||
|
||||
// Create a mock router for stories
|
||||
function createMockRouter() {
|
||||
return createRouter({
|
||||
const createMockRouter = () =>
|
||||
createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/', component: { template: '<div>Home</div>' } },
|
||||
@@ -23,7 +23,6 @@ function createMockRouter() {
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
const meta: Meta<typeof InstallView> = {
|
||||
title: 'Desktop/Views/InstallView',
|
||||
|
||||
@@ -90,7 +90,7 @@ const currentStep = ref('1')
|
||||
/** Forces each install step to be visited at least once. */
|
||||
const highestStep = ref(0)
|
||||
|
||||
function handleStepChange(value: string | number) {
|
||||
const handleStepChange = (value: string | number) => {
|
||||
setHighestStep(value)
|
||||
|
||||
electronAPI().Events.trackEvent('install_stepper_change', {
|
||||
@@ -98,7 +98,7 @@ function handleStepChange(value: string | number) {
|
||||
})
|
||||
}
|
||||
|
||||
function setHighestStep(value: string | number) {
|
||||
const setHighestStep = (value: string | number) => {
|
||||
const int = typeof value === 'number' ? value : parseInt(value, 10)
|
||||
if (!isNaN(int) && int > highestStep.value) highestStep.value = int
|
||||
}
|
||||
@@ -123,7 +123,7 @@ const canProceed = computed(() => {
|
||||
})
|
||||
|
||||
// Navigation methods
|
||||
function goToNextStep() {
|
||||
const goToNextStep = () => {
|
||||
const nextStep = (parseInt(currentStep.value) + 1).toString()
|
||||
currentStep.value = nextStep
|
||||
setHighestStep(nextStep)
|
||||
@@ -132,7 +132,7 @@ function goToNextStep() {
|
||||
})
|
||||
}
|
||||
|
||||
function goToPreviousStep() {
|
||||
const goToPreviousStep = () => {
|
||||
const prevStep = (parseInt(currentStep.value) - 1).toString()
|
||||
currentStep.value = prevStep
|
||||
electronAPI().Events.trackEvent('install_stepper_change', {
|
||||
@@ -142,7 +142,7 @@ function goToPreviousStep() {
|
||||
|
||||
const electron = electronAPI()
|
||||
const router = useRouter()
|
||||
async function install() {
|
||||
const install = async () => {
|
||||
if (!device.value) return
|
||||
|
||||
const options: InstallOptions = {
|
||||
|
||||
@@ -35,14 +35,12 @@ const validationState: ValidationState = {
|
||||
upgradePackages: 'OK'
|
||||
}
|
||||
|
||||
function createMockElectronAPI() {
|
||||
const createMockElectronAPI = () => {
|
||||
const logListeners: Array<(message: string) => void> = []
|
||||
|
||||
function getValidationUpdate() {
|
||||
return {
|
||||
...validationState
|
||||
}
|
||||
}
|
||||
const getValidationUpdate = () => ({
|
||||
...validationState
|
||||
})
|
||||
|
||||
return {
|
||||
getPlatform: () => 'darwin',
|
||||
@@ -78,7 +76,7 @@ function createMockElectronAPI() {
|
||||
}
|
||||
}
|
||||
|
||||
function ensureElectronAPI() {
|
||||
const ensureElectronAPI = () => {
|
||||
const globalWindow = window as { electronAPI?: unknown }
|
||||
if (!globalWindow.electronAPI) {
|
||||
globalWindow.electronAPI = createMockElectronAPI()
|
||||
|
||||
@@ -183,7 +183,7 @@ const unsafeReasonText = computed(() => {
|
||||
})
|
||||
|
||||
/** If valid, leave the validation window. */
|
||||
async function completeValidation() {
|
||||
const completeValidation = async () => {
|
||||
const isValid = await electron.Validation.complete()
|
||||
if (!isValid) {
|
||||
toast.add({
|
||||
@@ -194,7 +194,7 @@ async function completeValidation() {
|
||||
}
|
||||
}
|
||||
|
||||
function toggleConsoleDrawer() {
|
||||
const toggleConsoleDrawer = () => {
|
||||
terminalVisible.value = !terminalVisible.value
|
||||
}
|
||||
|
||||
|
||||
@@ -67,9 +67,7 @@ const electron = electronAPI()
|
||||
const basePath = ref<string | null>(null)
|
||||
const sep = ref<'\\' | '/'>('/')
|
||||
|
||||
function restartApp(message?: string) {
|
||||
return electron.restartApp(message)
|
||||
}
|
||||
const restartApp = (message?: string) => electron.restartApp(message)
|
||||
|
||||
onMounted(async () => {
|
||||
basePath.value = await electron.getBasePath()
|
||||
|
||||
@@ -64,7 +64,7 @@ const allowMetrics = ref(true)
|
||||
const router = useRouter()
|
||||
const isUpdating = ref(false)
|
||||
|
||||
async function updateConsent() {
|
||||
const updateConsent = async () => {
|
||||
isUpdating.value = true
|
||||
try {
|
||||
await electronAPI().setMetricsConsent(allowMetrics.value)
|
||||
|
||||
@@ -61,19 +61,19 @@ import { useRouter } from 'vue-router'
|
||||
|
||||
import BaseViewTemplate from '@/views/templates/BaseViewTemplate.vue'
|
||||
|
||||
function openDocs() {
|
||||
const openDocs = () => {
|
||||
window.open(
|
||||
'https://github.com/Comfy-Org/desktop#currently-supported-platforms',
|
||||
'_blank'
|
||||
)
|
||||
}
|
||||
|
||||
function reportIssue() {
|
||||
const reportIssue = () => {
|
||||
window.open('https://forum.comfy.org/c/v1-feedback/', '_blank')
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
async function continueToInstall() {
|
||||
const continueToInstall = async () => {
|
||||
await router.push('/install')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -118,7 +118,7 @@ let xterm: Terminal | undefined
|
||||
/**
|
||||
* Handles installation stage updates from the desktop
|
||||
*/
|
||||
function updateInstallStage(stageInfo: InstallStageInfo) {
|
||||
const updateInstallStage = (stageInfo: InstallStageInfo) => {
|
||||
console.warn('[InstallStage.onUpdate] Received:', {
|
||||
stage: stageInfo.stage,
|
||||
progress: stageInfo.progress,
|
||||
@@ -183,17 +183,17 @@ const displayStatusText = computed(() => {
|
||||
return currentStatusLabel.value
|
||||
})
|
||||
|
||||
function updateProgress({ status: newStatus }: { status: ProgressStatus }) {
|
||||
const updateProgress = ({ status: newStatus }: { status: ProgressStatus }) => {
|
||||
status.value = newStatus
|
||||
|
||||
// Make critical error screen more obvious.
|
||||
if (newStatus === ProgressStatus.ERROR) terminalVisible.value = false
|
||||
}
|
||||
|
||||
function terminalCreated(
|
||||
const terminalCreated = (
|
||||
{ terminal, useAutoSize }: ReturnType<typeof useTerminal>,
|
||||
root: Ref<HTMLElement | undefined>
|
||||
) {
|
||||
) => {
|
||||
xterm = terminal
|
||||
|
||||
useAutoSize({ root, autoRows: true, autoCols: true })
|
||||
@@ -206,15 +206,11 @@ function terminalCreated(
|
||||
terminal.options.cursorInactiveStyle = 'block'
|
||||
}
|
||||
|
||||
function troubleshoot() {
|
||||
return electron.startTroubleshooting()
|
||||
}
|
||||
function reportIssue() {
|
||||
const troubleshoot = () => electron.startTroubleshooting()
|
||||
const reportIssue = () => {
|
||||
window.open('https://forum.comfy.org/c/v1-feedback/', '_blank')
|
||||
}
|
||||
function openLogs() {
|
||||
return electron.openLogsFolder()
|
||||
}
|
||||
const openLogs = () => electron.openLogsFolder()
|
||||
|
||||
let cleanupInstallStageListener: (() => void) | undefined
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ import { useRouter } from 'vue-router'
|
||||
import BaseViewTemplate from '@/views/templates/BaseViewTemplate.vue'
|
||||
|
||||
const router = useRouter()
|
||||
async function navigateTo(path: string) {
|
||||
const navigateTo = async (path: string) => {
|
||||
await router.push(path)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -3,9 +3,8 @@ import { expect, test } from '@playwright/test'
|
||||
import { demos, getNextDemo } from '../src/config/demos'
|
||||
import { t } from '../src/i18n/translations'
|
||||
|
||||
function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}
|
||||
const escapeRegExp = (value: string): string =>
|
||||
value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
|
||||
test.describe('Demo pages @smoke', () => {
|
||||
for (const demo of demos) {
|
||||
|
||||
@@ -111,7 +111,7 @@ async function measureMarqueeLoopGeometry(
|
||||
`Animation on ${sel} has unusable duration: ${String(duration)}`
|
||||
)
|
||||
}
|
||||
function setAllTimes(time: number) {
|
||||
const setAllTimes = (time: number) => {
|
||||
for (const track of tracks) {
|
||||
for (const anim of track.getAnimations()) {
|
||||
anim.currentTime = time
|
||||
@@ -119,9 +119,7 @@ async function measureMarqueeLoopGeometry(
|
||||
}
|
||||
void document.body.offsetWidth
|
||||
}
|
||||
function readX() {
|
||||
return tracks.map((track) => track.getBoundingClientRect().x)
|
||||
}
|
||||
const readX = () => tracks.map((track) => track.getBoundingClientRect().x)
|
||||
setAllTimes(0)
|
||||
const startPositions = readX()
|
||||
const copyWidths = tracks.map(
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
.bg { fill: #000000; }
|
||||
.fg { fill: #F2FF59; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.bg { fill: #F2FF59; }
|
||||
.fg { fill: #000000; }
|
||||
}
|
||||
</style>
|
||||
<circle class="bg" cx="24" cy="24" r="24"/>
|
||||
<g transform="translate(7.8 6.72) scale(0.72)">
|
||||
<path class="fg" d="M35.6487 36.021C35.733 35.7387 35.7791 35.4411 35.7791 35.1283C35.7791 33.3963 34.3675 31.9924 32.6262 31.9924H18.4956C17.7361 32 17.1147 31.3896 17.1147 30.6342C17.1147 30.4969 17.1377 30.3672 17.1684 30.2451L20.9734 17.0606C21.1345 16.4807 21.6715 16.0534 22.3005 16.0534L36.4848 16.0382C39.4766 16.0382 42.0005 14.0315 42.76 11.2923L44.8926 3.94468C44.9616 3.68526 45 3.40296 45 3.12065C45 1.39628 43.5961 0 41.8624 0L24.7017 0C21.7252 0 19.209 1.99142 18.4342 4.70005L16.992 9.71292C16.8232 10.2852 16.2939 10.7048 15.6648 10.7048H11.5453C8.59189 10.7048 6.0987 12.6581 5.30089 15.3362L0.11507 33.3505C0.0383566 33.6175 0 33.9075 0 34.1974C0 35.9294 1.41152 37.3333 3.15292 37.3333H7.20338C7.96284 37.3333 8.58421 37.9437 8.58421 38.7067C8.58421 38.8364 8.56887 38.9661 8.53051 39.0882L7.09598 44.0553C7.02694 44.3224 6.98091 44.597 6.98091 44.8794C6.98091 46.6037 8.38476 48 10.1185 48L27.2869 47.9847C30.2711 47.9847 32.7873 45.9857 33.5544 43.2618L35.641 36.0286L35.6487 36.021Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -36,9 +36,7 @@ let pendingFrame = 0
|
||||
const HEADER_OFFSET = -144
|
||||
const ACTIVATION_OFFSET = 300
|
||||
|
||||
function deptElementId(key: string) {
|
||||
return `careers-dept-${key}`
|
||||
}
|
||||
const deptElementId = (key: string) => `careers-dept-${key}`
|
||||
|
||||
function pickActiveSection() {
|
||||
pendingFrame = 0
|
||||
|
||||
@@ -58,7 +58,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
raw.sort((a, b) => {
|
||||
function norm(v: number) {
|
||||
const norm = (v: number) => {
|
||||
const r = v + Math.PI / 2
|
||||
return r < 0 ? r + 2 * Math.PI : r
|
||||
}
|
||||
@@ -117,7 +117,7 @@ onMounted(() => {
|
||||
applyToPanel(panels[1], elapsed2)
|
||||
applyToPanel(panels[2], elapsed3)
|
||||
|
||||
function wOf(elapsed: number) {
|
||||
const wOf = (elapsed: number) => {
|
||||
const progress = elapsed < PANEL_DURATION ? elapsed / PANEL_DURATION : 1
|
||||
return lerp(S.w, E.w, ease(progress))
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export function useParallax(
|
||||
|
||||
const triggerEl = options.trigger?.value
|
||||
|
||||
function createAnimations() {
|
||||
const createAnimations = () => {
|
||||
const els = elements
|
||||
.map((r) => r.value)
|
||||
.filter((el): el is HTMLElement => !!el && el.offsetParent !== null)
|
||||
|
||||
@@ -29,7 +29,7 @@ function interpolateY(
|
||||
contentH: number,
|
||||
vpH: number
|
||||
) {
|
||||
function clampedTarget(i: number) {
|
||||
const clampedTarget = (i: number) => {
|
||||
const center = buttonCenters[i] ?? 0
|
||||
return Math.max(-(contentH - vpH), Math.min(0, vpH / 2 - center))
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ const websiteJsonLd = {
|
||||
{noindex && <meta name="robots" content="noindex, nofollow" />}
|
||||
<title>{title}</title>
|
||||
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="icon" href="/icons/logomark.svg" type="image/svg+xml" />
|
||||
<link rel="canonical" href={canonicalURL.href} />
|
||||
<link rel="preconnect" href="https://www.googletagmanager.com" />
|
||||
<link rel="dns-prefetch" href="https://www.googletagmanager.com" />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
import type { GetStaticPaths } from 'astro'
|
||||
|
||||
import type { Pack } from '../../../data/cloudNodes'
|
||||
|
||||
@@ -8,7 +9,7 @@ import { t } from '../../../i18n/translations'
|
||||
import { loadPacksForBuild } from '../../../utils/cloudNodes.build'
|
||||
import { escapeJsonLd } from '../../../utils/escapeJsonLd'
|
||||
|
||||
export async function getStaticPaths() {
|
||||
export const getStaticPaths: GetStaticPaths = async () => {
|
||||
const packs = await loadPacksForBuild()
|
||||
return packs.map((pack) => ({
|
||||
params: { pack: pack.id },
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
import type { GetStaticPaths } from 'astro'
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro'
|
||||
import DetailHeroSection from '../../components/customers/DetailHeroSection.vue'
|
||||
import ContentSection from '../../components/common/ContentSection.vue'
|
||||
@@ -6,7 +7,7 @@ import WhatsNextSection from '../../components/customers/WhatsNextSection.vue'
|
||||
import { customerStories, getNextStory, getStoryBySlug } from '../../config/customerStories'
|
||||
import { t } from '../../i18n/translations'
|
||||
|
||||
export function getStaticPaths() {
|
||||
export const getStaticPaths: GetStaticPaths = () => {
|
||||
return customerStories.map((story) => ({
|
||||
params: { slug: story.slug }
|
||||
}))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
import type { GetStaticPaths } from 'astro'
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro'
|
||||
import DemoHeroSection from '../../components/demos/DemoHeroSection.vue'
|
||||
import ArcadeEmbed from '../../components/demos/ArcadeEmbed.vue'
|
||||
@@ -7,7 +8,7 @@ import DemoNavSection from '../../components/demos/DemoNavSection.vue'
|
||||
import { demos, getDemoBySlug, getNextDemo } from '../../config/demos'
|
||||
import { t } from '../../i18n/translations'
|
||||
|
||||
export function getStaticPaths() {
|
||||
export const getStaticPaths: GetStaticPaths = () => {
|
||||
return demos.map((demo) => ({
|
||||
params: { slug: demo.slug }
|
||||
}))
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
---
|
||||
import type { GetStaticPaths } from 'astro'
|
||||
import BaseLayout from '../../../layouts/BaseLayout.astro'
|
||||
import ModelHeroSection from '../../../components/models/ModelHeroSection.vue'
|
||||
import { models, getModelBySlug } from '../../../config/models'
|
||||
import { t } from '../../../i18n/translations'
|
||||
|
||||
export function getStaticPaths() {
|
||||
export const getStaticPaths: GetStaticPaths = () => {
|
||||
return models.map((model) => ({
|
||||
params: { slug: model.slug }
|
||||
}))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
import type { GetStaticPaths } from 'astro'
|
||||
|
||||
import type { Pack } from '../../../../data/cloudNodes'
|
||||
|
||||
@@ -8,7 +9,7 @@ import { t } from '../../../../i18n/translations'
|
||||
import { loadPacksForBuild } from '../../../../utils/cloudNodes.build'
|
||||
import { escapeJsonLd } from '../../../../utils/escapeJsonLd'
|
||||
|
||||
export async function getStaticPaths() {
|
||||
export const getStaticPaths: GetStaticPaths = async () => {
|
||||
const packs = await loadPacksForBuild()
|
||||
return packs.map((pack) => ({
|
||||
params: { pack: pack.id },
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
import type { GetStaticPaths } from 'astro'
|
||||
import BaseLayout from '../../../layouts/BaseLayout.astro'
|
||||
import DetailHeroSection from '../../../components/customers/DetailHeroSection.vue'
|
||||
import ContentSection from '../../../components/common/ContentSection.vue'
|
||||
@@ -6,7 +7,7 @@ import WhatsNextSection from '../../../components/customers/WhatsNextSection.vue
|
||||
import { customerStories, getNextStory, getStoryBySlug } from '../../../config/customerStories'
|
||||
import { t } from '../../../i18n/translations'
|
||||
|
||||
export function getStaticPaths() {
|
||||
export const getStaticPaths: GetStaticPaths = () => {
|
||||
return customerStories.map((story) => ({
|
||||
params: { slug: story.slug }
|
||||
}))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
import type { GetStaticPaths } from 'astro'
|
||||
import BaseLayout from '../../../layouts/BaseLayout.astro'
|
||||
import DemoHeroSection from '../../../components/demos/DemoHeroSection.vue'
|
||||
import ArcadeEmbed from '../../../components/demos/ArcadeEmbed.vue'
|
||||
@@ -7,7 +8,7 @@ import DemoNavSection from '../../../components/demos/DemoNavSection.vue'
|
||||
import { demos, getDemoBySlug, getNextDemo } from '../../../config/demos'
|
||||
import { t } from '../../../i18n/translations'
|
||||
|
||||
export function getStaticPaths() {
|
||||
export const getStaticPaths: GetStaticPaths = () => {
|
||||
return demos.map((demo) => ({
|
||||
params: { slug: demo.slug }
|
||||
}))
|
||||
|
||||
@@ -35,9 +35,8 @@ const TICK_MS = 200
|
||||
|
||||
function readColors() {
|
||||
const style = getComputedStyle(document.documentElement)
|
||||
function get(name: string, fallback: string): string {
|
||||
return style.getPropertyValue(name).trim() || fallback
|
||||
}
|
||||
const get = (name: string, fallback: string): string =>
|
||||
style.getPropertyValue(name).trim() || fallback
|
||||
|
||||
return {
|
||||
bg: get('--color-primary-comfy-ink', '#211927'),
|
||||
@@ -60,12 +59,9 @@ function requireElement<T extends Element>(
|
||||
return el
|
||||
}
|
||||
|
||||
function isSVGSVG(el: Element): el is SVGSVGElement {
|
||||
return el instanceof SVGSVGElement
|
||||
}
|
||||
function isSVGG(el: Element): el is SVGGElement {
|
||||
return el instanceof SVGGElement
|
||||
}
|
||||
const isSVGSVG = (el: Element): el is SVGSVGElement =>
|
||||
el instanceof SVGSVGElement
|
||||
const isSVGG = (el: Element): el is SVGGElement => el instanceof SVGGElement
|
||||
function isSVGText(el: Element): el is SVGTextElement {
|
||||
return el instanceof SVGTextElement
|
||||
}
|
||||
@@ -131,9 +127,8 @@ function depth(cell: Cell): number {
|
||||
|
||||
function roundedPath(pts: [number, number][], radius: number): string {
|
||||
const n = pts.length
|
||||
function fmt(p: readonly [number, number]) {
|
||||
return `${p[0].toFixed(2)},${p[1].toFixed(2)}`
|
||||
}
|
||||
const fmt = (p: readonly [number, number]) =>
|
||||
`${p[0].toFixed(2)},${p[1].toFixed(2)}`
|
||||
let d = ''
|
||||
for (let i = 0; i < n; i++) {
|
||||
const prev = pts[(i - 1 + n) % n]
|
||||
@@ -211,7 +206,7 @@ function triggerExplosion() {
|
||||
const cx = ((COLS - ROWS) * STEP_X) / 2
|
||||
const cy = ((COLS + ROWS - 2) * STEP_Y) / 2
|
||||
|
||||
function launchParticle(i: number, j: number, fill: string): Particle {
|
||||
const launchParticle = (i: number, j: number, fill: string): Particle => {
|
||||
const [sx, sy] = iso(i, j)
|
||||
const baseAngle = Math.atan2(sy - cy, sx - cx)
|
||||
const angle = baseAngle + (Math.random() - 0.5) * 1.2
|
||||
@@ -244,9 +239,7 @@ function triggerExplosion() {
|
||||
const DROP_DURATION_MS = 450
|
||||
const DROP_HEIGHT = 600
|
||||
let foodDropStart = 0
|
||||
function easeOutCubic(t: number) {
|
||||
return 1 - Math.pow(1 - t, 3)
|
||||
}
|
||||
const easeOutCubic = (t: number) => 1 - Math.pow(1 - t, 3)
|
||||
|
||||
function foodDropOffset(now = performance.now()): number {
|
||||
if (!foodDropStart) return 0
|
||||
@@ -259,7 +252,7 @@ function foodDropOffset(now = performance.now()): number {
|
||||
const REBIRTH_STAGGER_MS = 90
|
||||
const REBIRTH_GROW_MS = 260
|
||||
let rebirthStart = 0
|
||||
function easeOutBack(t: number) {
|
||||
const easeOutBack = (t: number) => {
|
||||
const c1 = 1.70158
|
||||
const c3 = c1 + 1
|
||||
return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2)
|
||||
@@ -278,9 +271,7 @@ function rebirthScaleFor(idx: number, now = performance.now()): number {
|
||||
const CHOMP_DURATION_MS = 220
|
||||
const CHOMP_PEAK_SCALE = 1.15
|
||||
let chompStart = 0
|
||||
function easeOut(t: number) {
|
||||
return 1 - (1 - t) * (1 - t)
|
||||
}
|
||||
const easeOut = (t: number) => 1 - (1 - t) * (1 - t)
|
||||
|
||||
function chompScale(now = performance.now()): number {
|
||||
if (!chompStart) return 1
|
||||
@@ -308,7 +299,7 @@ function isAnimating(): boolean {
|
||||
|
||||
function ensureAnimationLoop() {
|
||||
if (animationHandle !== null) return
|
||||
function tick() {
|
||||
const tick = () => {
|
||||
if (
|
||||
explodeStart &&
|
||||
performance.now() - explodeStart >= EXPLODE_DURATION_MS
|
||||
@@ -420,12 +411,8 @@ function updateScoreDisplay() {
|
||||
scoreBestEl.textContent = String(best)
|
||||
}
|
||||
|
||||
function cellsEqual(a: Cell, b: Cell) {
|
||||
return a.i === b.i && a.j === b.j
|
||||
}
|
||||
function inBounds(c: Cell) {
|
||||
return c.i >= 0 && c.j >= 0 && c.i < COLS && c.j < ROWS
|
||||
}
|
||||
const cellsEqual = (a: Cell, b: Cell) => a.i === b.i && a.j === b.j
|
||||
const inBounds = (c: Cell) => c.i >= 0 && c.j >= 0 && c.i < COLS && c.j < ROWS
|
||||
|
||||
function reset() {
|
||||
const j0 = Math.floor(ROWS / 2)
|
||||
|
||||
@@ -5,13 +5,11 @@ import { TestIds } from '@e2e/fixtures/selectors'
|
||||
|
||||
export class QueuePanel {
|
||||
readonly overlayToggle: Locator
|
||||
readonly overlay: Locator
|
||||
readonly moreOptionsButton: Locator
|
||||
|
||||
constructor(readonly page: Page) {
|
||||
this.overlayToggle = page.getByTestId(TestIds.queue.overlayToggle)
|
||||
this.overlay = page.getByTestId(TestIds.queue.progressOverlay)
|
||||
this.moreOptionsButton = this.overlay.getByLabel(/More options/i)
|
||||
this.moreOptionsButton = page.getByLabel(/More options/i).first()
|
||||
}
|
||||
|
||||
async openClearHistoryDialog() {
|
||||
|
||||
@@ -158,7 +158,7 @@ export class AssetHelper {
|
||||
statusCode: number,
|
||||
error: string = 'Internal Server Error'
|
||||
): Promise<void> {
|
||||
async function handler(route: Route) {
|
||||
const handler = async (route: Route) => {
|
||||
return route.fulfill({
|
||||
status: statusCode,
|
||||
json: { error }
|
||||
|
||||
@@ -325,7 +325,7 @@ export class AssetsHelper {
|
||||
await this.page.unroute(pattern, existingHandler)
|
||||
}
|
||||
|
||||
async function handler(route: Route) {
|
||||
const handler = async (route: Route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
|
||||
@@ -1,26 +1,19 @@
|
||||
import { test as base, expect } from '@playwright/test'
|
||||
import type { Page, Route, WebSocketRoute } from '@playwright/test'
|
||||
import { test as base } from '@playwright/test'
|
||||
import type { Page, Route } from '@playwright/test'
|
||||
|
||||
import type { LogsRawResponse } from '@/schemas/apiSchema'
|
||||
|
||||
const RAW_LOGS_URL = '**/internal/logs/raw**'
|
||||
const SUBSCRIBE_LOGS_URL = '**/internal/logs/subscribe**'
|
||||
|
||||
export class LogsTerminalHelper {
|
||||
constructor(private readonly page: Page) {}
|
||||
|
||||
async mockRawLogs(messages: string[]): Promise<() => number> {
|
||||
let count = 0
|
||||
await this.page.unroute(RAW_LOGS_URL)
|
||||
await this.page.route(RAW_LOGS_URL, async (route: Route) => {
|
||||
count += 1
|
||||
await route.fulfill({
|
||||
async mockRawLogs(messages: string[]) {
|
||||
await this.page.route('**/internal/logs/raw**', (route: Route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(LogsTerminalHelper.buildRawLogsResponse(messages))
|
||||
})
|
||||
})
|
||||
return () => count
|
||||
)
|
||||
}
|
||||
|
||||
async mockRawLogsPending(messages: string[] = []): Promise<() => void> {
|
||||
@@ -28,8 +21,7 @@ export class LogsTerminalHelper {
|
||||
const pending = new Promise<void>((r) => {
|
||||
resolve = r
|
||||
})
|
||||
await this.page.unroute(RAW_LOGS_URL)
|
||||
await this.page.route(RAW_LOGS_URL, async (route: Route) => {
|
||||
await this.page.route('**/internal/logs/raw**', async (route: Route) => {
|
||||
await pending
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
@@ -41,39 +33,15 @@ export class LogsTerminalHelper {
|
||||
}
|
||||
|
||||
async mockRawLogsError() {
|
||||
await this.page.unroute(RAW_LOGS_URL)
|
||||
await this.page.route(RAW_LOGS_URL, (route: Route) =>
|
||||
await this.page.route('**/internal/logs/raw**', (route: Route) =>
|
||||
route.fulfill({ status: 500, body: 'Internal Server Error' })
|
||||
)
|
||||
}
|
||||
|
||||
async mockSubscribeLogs(): Promise<() => number> {
|
||||
let count = 0
|
||||
await this.page.unroute(SUBSCRIBE_LOGS_URL)
|
||||
await this.page.route(SUBSCRIBE_LOGS_URL, async (route: Route) => {
|
||||
count += 1
|
||||
await route.fulfill({ status: 200, body: '' })
|
||||
})
|
||||
return () => count
|
||||
}
|
||||
|
||||
/**
|
||||
* Force the frontend to reconnect by closing the proxied WebSocket. The
|
||||
* api layer reschedules a fresh `WebSocket(...)`, the routeWebSocket
|
||||
* handler fires again, and on `open` with `isReconnect=true` it dispatches
|
||||
* `'reconnected'`, which triggers the logs-terminal resync.
|
||||
*
|
||||
* Use the resync's `subscribeLogs(true)` HTTP call as the sync point — by
|
||||
* the time the count goes up, the new socket is open and resync has
|
||||
* completed enough to assert against the terminal.
|
||||
*/
|
||||
async triggerReconnect(
|
||||
ws: WebSocketRoute,
|
||||
subscribeFetches: () => number
|
||||
): Promise<void> {
|
||||
const before = subscribeFetches()
|
||||
await ws.close()
|
||||
await expect.poll(subscribeFetches).toBeGreaterThan(before)
|
||||
async mockSubscribeLogs() {
|
||||
await this.page.route('**/internal/logs/subscribe**', (route: Route) =>
|
||||
route.fulfill({ status: 200, body: '' })
|
||||
)
|
||||
}
|
||||
|
||||
static buildWsLogFrame(messages: string[]): string {
|
||||
|
||||
@@ -42,7 +42,7 @@ export async function setupNodeReplacement(
|
||||
options?: AddEventListenerOptions | boolean
|
||||
) {
|
||||
if (type === 'message' && typeof listener === 'function') {
|
||||
function wrapped(this: WebSocket, event: Event) {
|
||||
const wrapped = function (this: WebSocket, event: Event) {
|
||||
const msgEvent = event as MessageEvent
|
||||
if (typeof msgEvent.data === 'string') {
|
||||
try {
|
||||
|
||||
@@ -11,7 +11,6 @@ import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/w
|
||||
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
||||
import { SubgraphEditor } from '@e2e/fixtures/components/SubgraphEditor'
|
||||
import { TestIds } from '@e2e/fixtures/selectors'
|
||||
import type { Position, Size } from '@e2e/fixtures/types'
|
||||
import type { NodeReference } from '@e2e/fixtures/utils/litegraphUtils'
|
||||
import { SubgraphSlotReference } from '@e2e/fixtures/utils/litegraphUtils'
|
||||
|
||||
@@ -242,17 +241,6 @@ export class SubgraphHelper {
|
||||
return new SubgraphSlotReference('output', slotName || '', this.comfyPage)
|
||||
}
|
||||
|
||||
async getInputBounds(): Promise<Position & Size> {
|
||||
return await this.comfyPage.page.evaluate(() => {
|
||||
const graph = app!.canvas.graph as Subgraph
|
||||
const inputNode = graph.inputNode
|
||||
const [x, y] = app!.canvas.ds.convertOffsetToCanvas(inputNode.pos)
|
||||
const width = inputNode.size[0] * app!.canvas.ds.scale
|
||||
const height = inputNode.size[1] * app!.canvas.ds.scale
|
||||
return { x, y, width, height }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect a regular node output to a subgraph input.
|
||||
* This creates a new input slot on the subgraph if targetInputName is not provided.
|
||||
@@ -618,7 +606,7 @@ export class SubgraphHelper {
|
||||
]
|
||||
): { warnings: string[]; dispose: () => void } {
|
||||
const warnings: string[] = []
|
||||
function handler(msg: ConsoleMessage) {
|
||||
const handler = (msg: ConsoleMessage) => {
|
||||
const text = msg.text()
|
||||
if (patterns.some((p) => text.includes(p))) {
|
||||
warnings.push(text)
|
||||
|
||||
@@ -150,7 +150,7 @@ export class TemplateHelper {
|
||||
}
|
||||
|
||||
async mockThumbnails(): Promise<void> {
|
||||
async function thumbnailHandler(route: Route) {
|
||||
const thumbnailHandler = async (route: Route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
path: 'browser_tests/assets/example.webp',
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { test as base } from '@playwright/test'
|
||||
import type { Page } from '@playwright/test'
|
||||
import type { z } from 'zod'
|
||||
import {
|
||||
zHistoryManageRequest,
|
||||
zQueueManageRequest,
|
||||
zQueueManageResponse
|
||||
} from '@comfyorg/ingest-types/zod'
|
||||
|
||||
import type {
|
||||
JobStatus,
|
||||
@@ -14,8 +9,6 @@ import type {
|
||||
} from '@/platform/remote/comfyui/jobs/jobTypes'
|
||||
|
||||
type JobsListResponse = z.infer<typeof zJobsListResponse>
|
||||
type HistoryManageRequest = z.infer<typeof zHistoryManageRequest>
|
||||
type QueueManageRequest = z.infer<typeof zQueueManageRequest>
|
||||
|
||||
const terminalJobStatuses = [
|
||||
'completed',
|
||||
@@ -29,8 +22,7 @@ const activeJobStatuses = [
|
||||
const defaultJobsListLimit = 200
|
||||
const defaultScenarioHistoryLimit = 64
|
||||
const defaultJobsListOffset = 0
|
||||
|
||||
export const routeMockJobTimestamp = Date.UTC(2026, 0, 1, 12)
|
||||
const defaultRouteMockJobTimestamp = Date.UTC(2026, 0, 1, 12)
|
||||
|
||||
interface JobsListRoute {
|
||||
statuses: readonly JobStatus[]
|
||||
@@ -40,7 +32,7 @@ interface JobsListRoute {
|
||||
responseLimit?: number
|
||||
}
|
||||
|
||||
export interface JobsScenario {
|
||||
interface JobsScenario {
|
||||
history?: readonly RawJobListItem[]
|
||||
queue?: readonly RawJobListItem[]
|
||||
}
|
||||
@@ -73,9 +65,11 @@ function hasJobsListPageParams(
|
||||
)
|
||||
}
|
||||
|
||||
function matchesJobsListRoute(url: URL, route: JobsListRoute): boolean {
|
||||
function isJobsListRequest(url: URL, route: JobsListRoute): boolean {
|
||||
return (
|
||||
hasExactStatuses(url, route.statuses) && hasJobsListPageParams(url, route)
|
||||
url.pathname.endsWith('/api/jobs') &&
|
||||
hasExactStatuses(url, route.statuses) &&
|
||||
hasJobsListPageParams(url, route)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -105,9 +99,9 @@ export function createRouteMockJob({
|
||||
return {
|
||||
id,
|
||||
status: 'completed',
|
||||
create_time: routeMockJobTimestamp,
|
||||
execution_start_time: routeMockJobTimestamp,
|
||||
execution_end_time: routeMockJobTimestamp + 5_000,
|
||||
create_time: defaultRouteMockJobTimestamp,
|
||||
execution_start_time: defaultRouteMockJobTimestamp,
|
||||
execution_end_time: defaultRouteMockJobTimestamp + 5_000,
|
||||
preview_output: {
|
||||
filename: `output_${id}.png`,
|
||||
subfolder: '',
|
||||
@@ -156,8 +150,7 @@ export class JobsRouteMocker {
|
||||
const response = createJobsListResponse(route)
|
||||
|
||||
await this.page.route(
|
||||
(url) =>
|
||||
url.pathname.endsWith('/api/jobs') && matchesJobsListRoute(url, route),
|
||||
(url) => isJobsListRequest(url, route),
|
||||
async (requestRoute) => {
|
||||
if (requestRoute.request().method().toUpperCase() !== 'GET') {
|
||||
await requestRoute.fallback()
|
||||
@@ -168,44 +161,6 @@ export class JobsRouteMocker {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async mockClearQueue(): Promise<QueueManageRequest[]> {
|
||||
const response = zQueueManageResponse.parse({ cleared: true })
|
||||
return await this.mockPostManageRoute(
|
||||
'queue',
|
||||
zQueueManageRequest,
|
||||
response
|
||||
)
|
||||
}
|
||||
|
||||
async mockClearHistory(): Promise<HistoryManageRequest[]> {
|
||||
return await this.mockPostManageRoute('history', zHistoryManageRequest, {})
|
||||
}
|
||||
|
||||
private async mockPostManageRoute<TRequest>(
|
||||
type: 'queue' | 'history',
|
||||
requestSchema: z.ZodType<TRequest>,
|
||||
response: unknown
|
||||
): Promise<TRequest[]> {
|
||||
const requests: TRequest[] = []
|
||||
|
||||
await this.page.route(
|
||||
(url) => url.pathname.endsWith(`/api/${type}`),
|
||||
async (requestRoute) => {
|
||||
if (requestRoute.request().method().toUpperCase() !== 'POST') {
|
||||
await requestRoute.fallback()
|
||||
return
|
||||
}
|
||||
|
||||
requests.push(
|
||||
requestSchema.parse(requestRoute.request().postDataJSON())
|
||||
)
|
||||
await requestRoute.fulfill({ json: response })
|
||||
}
|
||||
)
|
||||
|
||||
return requests
|
||||
}
|
||||
}
|
||||
|
||||
export const jobsRouteFixture = base.extend<{
|
||||
@@ -213,5 +168,6 @@ export const jobsRouteFixture = base.extend<{
|
||||
}>({
|
||||
jobsRoutes: async ({ page }, use) => {
|
||||
await use(new JobsRouteMocker(page))
|
||||
await page.unrouteAll({ behavior: 'wait' })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -76,15 +76,7 @@ export const TestIds = {
|
||||
publishTabPanel: 'publish-tab-panel',
|
||||
apiSignin: 'api-signin-dialog',
|
||||
updatePassword: 'update-password-dialog',
|
||||
cloudNotification: 'cloud-notification-dialog',
|
||||
openSharedWorkflow: 'open-shared-workflow-dialog',
|
||||
openSharedWorkflowTitle: 'open-shared-workflow-title',
|
||||
openSharedWorkflowClose: 'open-shared-workflow-close',
|
||||
openSharedWorkflowErrorClose: 'open-shared-workflow-error-close',
|
||||
openSharedWorkflowCancel: 'open-shared-workflow-cancel',
|
||||
openSharedWorkflowOpenWithoutImporting:
|
||||
'open-shared-workflow-open-without-importing',
|
||||
openSharedWorkflowConfirm: 'open-shared-workflow-confirm'
|
||||
cloudNotification: 'cloud-notification-dialog'
|
||||
},
|
||||
keybindings: {
|
||||
presetMenu: 'keybinding-preset-menu'
|
||||
@@ -234,10 +226,7 @@ export const TestIds = {
|
||||
currentUserIndicator: 'current-user-indicator'
|
||||
},
|
||||
queue: {
|
||||
jobHistorySidebar: 'job-history-sidebar',
|
||||
progressOverlay: 'queue-progress-overlay',
|
||||
overlayToggle: 'queue-overlay-toggle',
|
||||
dockedJobHistoryAction: 'docked-job-history-action',
|
||||
jobDetailsPopover: 'queue-job-details-popover',
|
||||
clearHistoryAction: 'clear-history-action',
|
||||
jobAssetsList: 'job-assets-list',
|
||||
|
||||
@@ -1,252 +0,0 @@
|
||||
import { test as base } from '@playwright/test'
|
||||
import type { Page } from '@playwright/test'
|
||||
import type {
|
||||
Asset,
|
||||
ImportPublishedAssetsRequest,
|
||||
ListAssetsResponse
|
||||
} from '@comfyorg/ingest-types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
import type { zSharedWorkflowResponse } from '@/platform/workflow/sharing/schemas/shareSchemas'
|
||||
import type { AssetInfo } from '@/schemas/apiSchema'
|
||||
|
||||
type SharedWorkflowResponse = z.input<typeof zSharedWorkflowResponse>
|
||||
|
||||
export const sharedWorkflowImportScenario = {
|
||||
shareId: 'shared-missing-media-e2e',
|
||||
workflowId: 'shared-missing-media-workflow',
|
||||
publishedAssetId: 'published-input-asset-1',
|
||||
inputFileName: 'shared_imported_image.png'
|
||||
} as const
|
||||
|
||||
export type SharedWorkflowRequestEvent =
|
||||
| 'import'
|
||||
| 'input-assets-including-public-before-import'
|
||||
| 'input-assets-including-public-after-import'
|
||||
|
||||
export interface SharedWorkflowImportMocks {
|
||||
resetAndStartRecording: () => void
|
||||
getImportBody: () => ImportPublishedAssetsRequest | undefined
|
||||
getRequestEvents: () => SharedWorkflowRequestEvent[]
|
||||
waitForPublicInclusiveInputAssetResponseAfterImport: () => Promise<void>
|
||||
}
|
||||
|
||||
const defaultInputFileName = '00000000000000000000000Aexample.png'
|
||||
|
||||
const sharedWorkflowAsset: AssetInfo = {
|
||||
id: sharedWorkflowImportScenario.publishedAssetId,
|
||||
name: sharedWorkflowImportScenario.inputFileName,
|
||||
preview_url: '',
|
||||
storage_url: '',
|
||||
model: false,
|
||||
public: false,
|
||||
in_library: false
|
||||
}
|
||||
|
||||
const defaultInputAsset: Asset = {
|
||||
id: 'default-input-asset',
|
||||
name: defaultInputFileName,
|
||||
asset_hash: defaultInputFileName,
|
||||
size: 1_024,
|
||||
mime_type: 'image/png',
|
||||
tags: ['input'],
|
||||
created_at: '2026-05-01T00:00:00Z',
|
||||
updated_at: '2026-05-01T00:00:00Z',
|
||||
last_access_time: '2026-05-01T00:00:00Z'
|
||||
}
|
||||
|
||||
const importedInputAsset: Asset = {
|
||||
id: 'imported-input-asset',
|
||||
name: sharedWorkflowImportScenario.inputFileName,
|
||||
asset_hash: sharedWorkflowImportScenario.inputFileName,
|
||||
size: 1_024,
|
||||
mime_type: 'image/png',
|
||||
tags: ['input'],
|
||||
created_at: '2026-05-01T00:00:00Z',
|
||||
updated_at: '2026-05-01T00:00:00Z',
|
||||
last_access_time: '2026-05-01T00:00:00Z'
|
||||
}
|
||||
|
||||
const sharedWorkflowResponse: SharedWorkflowResponse = {
|
||||
share_id: sharedWorkflowImportScenario.shareId,
|
||||
workflow_id: sharedWorkflowImportScenario.workflowId,
|
||||
name: 'Shared Missing Media Workflow',
|
||||
listed: true,
|
||||
publish_time: '2026-05-01T00:00:00Z',
|
||||
workflow_json: {
|
||||
version: 0.4,
|
||||
last_node_id: 10,
|
||||
last_link_id: 0,
|
||||
nodes: [
|
||||
{
|
||||
id: 10,
|
||||
type: 'LoadImage',
|
||||
pos: [50, 200],
|
||||
size: [315, 314],
|
||||
flags: {},
|
||||
order: 0,
|
||||
mode: 0,
|
||||
inputs: [],
|
||||
outputs: [
|
||||
{
|
||||
name: 'IMAGE',
|
||||
type: 'IMAGE',
|
||||
links: null
|
||||
},
|
||||
{
|
||||
name: 'MASK',
|
||||
type: 'MASK',
|
||||
links: null
|
||||
}
|
||||
],
|
||||
properties: {
|
||||
'Node name for S&R': 'LoadImage'
|
||||
},
|
||||
widgets_values: [sharedWorkflowImportScenario.inputFileName, 'image']
|
||||
}
|
||||
],
|
||||
links: [],
|
||||
groups: [],
|
||||
config: {},
|
||||
extra: {
|
||||
ds: {
|
||||
offset: [0, 0],
|
||||
scale: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
assets: [sharedWorkflowAsset]
|
||||
}
|
||||
|
||||
export const sharedWorkflowImportFixture = base.extend<{
|
||||
sharedWorkflowImportMocks: SharedWorkflowImportMocks
|
||||
}>({
|
||||
sharedWorkflowImportMocks: async ({ page }, use) => {
|
||||
const mocks = await mockSharedWorkflowImportFlow(page)
|
||||
await use(mocks)
|
||||
}
|
||||
})
|
||||
|
||||
async function mockSharedWorkflowImportFlow(
|
||||
page: Page
|
||||
): Promise<SharedWorkflowImportMocks> {
|
||||
function noopResolveResponse() {}
|
||||
let isRecording = false
|
||||
let importEndpointCalled = false
|
||||
let importBody: ImportPublishedAssetsRequest | undefined
|
||||
let resolvePublicInclusiveInputAssetResponseAfterImport: () => void =
|
||||
noopResolveResponse
|
||||
let publicInclusiveInputAssetResponseAfterImport = new Promise<void>(
|
||||
(resolve) => {
|
||||
resolvePublicInclusiveInputAssetResponseAfterImport = resolve
|
||||
}
|
||||
)
|
||||
const requestEvents: SharedWorkflowRequestEvent[] = []
|
||||
|
||||
function resetPublicInclusiveInputAssetResponseWaiter() {
|
||||
publicInclusiveInputAssetResponseAfterImport = new Promise<void>(
|
||||
(resolve) => {
|
||||
resolvePublicInclusiveInputAssetResponseAfterImport = resolve
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function recordRequestEvent(event: SharedWorkflowRequestEvent) {
|
||||
if (isRecording) requestEvents.push(event)
|
||||
}
|
||||
|
||||
await page.route(
|
||||
`**/workflows/published/${sharedWorkflowImportScenario.shareId}`,
|
||||
async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(sharedWorkflowResponse)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
await page.route('**/api/assets/import', async (route) => {
|
||||
recordRequestEvent('import')
|
||||
importBody = route.request().postDataJSON() as ImportPublishedAssetsRequest
|
||||
importEndpointCalled = true
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({})
|
||||
})
|
||||
})
|
||||
|
||||
// Excludes `/api/assets/import` so the specific route above
|
||||
// remains isolated from the general asset listing mock.
|
||||
await page.route(/\/api\/assets(?=\?|$)/, async (route) => {
|
||||
const url = new URL(route.request().url())
|
||||
const includeTags = getTagParam(url, 'include_tags')
|
||||
const isInputAssetRequest = includeTags.includes('input')
|
||||
const includesPublicAssets =
|
||||
url.searchParams.get('include_public') === 'true'
|
||||
const isPublicInclusiveInputAssetRequest =
|
||||
isInputAssetRequest && includesPublicAssets
|
||||
const isAfterImportPublicInclusiveInputAssetRequest =
|
||||
isPublicInclusiveInputAssetRequest && importEndpointCalled
|
||||
|
||||
if (isPublicInclusiveInputAssetRequest) {
|
||||
recordRequestEvent(
|
||||
importEndpointCalled
|
||||
? 'input-assets-including-public-after-import'
|
||||
: 'input-assets-including-public-before-import'
|
||||
)
|
||||
}
|
||||
|
||||
const allAssets = [
|
||||
defaultInputAsset,
|
||||
...(importEndpointCalled ? [importedInputAsset] : [])
|
||||
]
|
||||
const assets = includeTags.length
|
||||
? allAssets.filter((asset) =>
|
||||
includeTags.every((tag) => asset.tags?.includes(tag))
|
||||
)
|
||||
: allAssets
|
||||
|
||||
const response: ListAssetsResponse = {
|
||||
assets,
|
||||
total: assets.length,
|
||||
has_more: false
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(response)
|
||||
})
|
||||
|
||||
if (isAfterImportPublicInclusiveInputAssetRequest) {
|
||||
resolvePublicInclusiveInputAssetResponseAfterImport()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
resetAndStartRecording: () => {
|
||||
isRecording = true
|
||||
importEndpointCalled = false
|
||||
importBody = undefined
|
||||
requestEvents.length = 0
|
||||
resetPublicInclusiveInputAssetResponseWaiter()
|
||||
},
|
||||
getImportBody: () => importBody,
|
||||
getRequestEvents: () => [...requestEvents],
|
||||
waitForPublicInclusiveInputAssetResponseAfterImport: () =>
|
||||
publicInclusiveInputAssetResponseAfterImport
|
||||
}
|
||||
}
|
||||
|
||||
function getTagParam(url: URL, key: string): string[] {
|
||||
return (
|
||||
url.searchParams
|
||||
.get(key)
|
||||
?.split(',')
|
||||
.map((tag) => tag.trim())
|
||||
.filter(Boolean) ?? []
|
||||
)
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
||||
import type { Position, Size } from '@e2e/fixtures/types'
|
||||
import { VueNodeFixture } from '@e2e/fixtures/utils/vueNodeFixtures'
|
||||
|
||||
export function getMiddlePoint(pos1: Position, pos2: Position) {
|
||||
export const getMiddlePoint = (pos1: Position, pos2: Position) => {
|
||||
return {
|
||||
x: (pos1.x + pos2.x) / 2,
|
||||
y: (pos1.y + pos2.y) / 2
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
||||
|
||||
export async function openMoreOptionsMenu(
|
||||
comfyPage: ComfyPage,
|
||||
nodeTitle: string
|
||||
) {
|
||||
const nodes = await comfyPage.nodeOps.getNodeRefsByTitle(nodeTitle)
|
||||
if (nodes.length === 0) {
|
||||
throw new Error(`No "${nodeTitle}" nodes found`)
|
||||
}
|
||||
|
||||
await nodes[0].centerOnNode()
|
||||
await nodes[0].click('title')
|
||||
|
||||
await expect(comfyPage.page.locator('.selection-toolbox')).toBeVisible()
|
||||
|
||||
const moreOptionsBtn = comfyPage.page.getByTestId('more-options-button')
|
||||
await expect(moreOptionsBtn).toBeVisible()
|
||||
await moreOptionsBtn.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const menu = comfyPage.page.locator('.p-contextmenu')
|
||||
await expect(menu).toBeVisible()
|
||||
|
||||
return menu
|
||||
}
|
||||
@@ -45,7 +45,7 @@ test.describe('Actionbar', { tag: '@ui' }, () => {
|
||||
const requestPromise = comfyPage.page.waitForResponse('**/api/prompt')
|
||||
|
||||
// Find and set the width on the latent node
|
||||
async function triggerChange(value: number) {
|
||||
const triggerChange = async (value: number) => {
|
||||
return await comfyPage.page.evaluate((value) => {
|
||||
const node = window.app!.graph!._nodes.find(
|
||||
(n) => n.type === 'EmptyLatentImage'
|
||||
@@ -59,7 +59,7 @@ test.describe('Actionbar', { tag: '@ui' }, () => {
|
||||
}
|
||||
|
||||
// Trigger a status websocket message
|
||||
function triggerStatus(queueSize: number) {
|
||||
const triggerStatus = (queueSize: number) => {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: 'status',
|
||||
@@ -75,7 +75,7 @@ test.describe('Actionbar', { tag: '@ui' }, () => {
|
||||
}
|
||||
|
||||
// Extract the width from the queue response
|
||||
async function getQueuedWidth(resp: Promise<Response>) {
|
||||
const getQueuedWidth = async (resp: Promise<Response>) => {
|
||||
const obj = await (await resp).json()
|
||||
return obj['__request']['prompt']['5']['inputs']['width']
|
||||
}
|
||||
|
||||
@@ -147,68 +147,5 @@ test.describe('Bottom Panel Logs', { tag: '@ui' }, () => {
|
||||
)
|
||||
await expect(comfyPage.bottomPanel.logs.terminalRoot).toBeHidden()
|
||||
})
|
||||
|
||||
test('resyncs the terminal when the WebSocket reconnects', async ({
|
||||
comfyPage,
|
||||
logsTerminal,
|
||||
getWebSocket
|
||||
}) => {
|
||||
const subscribeFetches = await logsTerminal.mockSubscribeLogs()
|
||||
const initialLine = 'pre-reboot log line'
|
||||
const postRebootLineA = 'post-reboot line A'
|
||||
const postRebootLineB = 'post-reboot line B'
|
||||
|
||||
await logsTerminal.mockRawLogs([initialLine])
|
||||
await comfyPage.bottomPanel.toggleLogs()
|
||||
await expect(comfyPage.bottomPanel.logs.terminalRoot).toContainText(
|
||||
initialLine
|
||||
)
|
||||
|
||||
// Swap the raw-logs mock so the next fetch returns the post-reboot view.
|
||||
await logsTerminal.mockRawLogs([postRebootLineA, postRebootLineB])
|
||||
|
||||
const ws = await getWebSocket()
|
||||
await logsTerminal.triggerReconnect(ws, subscribeFetches)
|
||||
|
||||
await expect(comfyPage.bottomPanel.logs.terminalRoot).toContainText(
|
||||
postRebootLineA
|
||||
)
|
||||
await expect(comfyPage.bottomPanel.logs.terminalRoot).toContainText(
|
||||
postRebootLineB
|
||||
)
|
||||
// reset() before write means the pre-reboot line must be gone.
|
||||
await expect(comfyPage.bottomPanel.logs.terminalRoot).not.toContainText(
|
||||
initialLine
|
||||
)
|
||||
})
|
||||
|
||||
test('resumes WebSocket log streaming after the reconnect', async ({
|
||||
comfyPage,
|
||||
logsTerminal,
|
||||
getWebSocket
|
||||
}) => {
|
||||
const subscribeFetches = await logsTerminal.mockSubscribeLogs()
|
||||
await logsTerminal.mockRawLogs(['initial'])
|
||||
await comfyPage.bottomPanel.toggleLogs()
|
||||
await expect(comfyPage.bottomPanel.logs.terminalRoot).toContainText(
|
||||
'initial'
|
||||
)
|
||||
|
||||
await logsTerminal.mockRawLogs(['after-reboot snapshot'])
|
||||
|
||||
const ws = await getWebSocket()
|
||||
await logsTerminal.triggerReconnect(ws, subscribeFetches)
|
||||
|
||||
// The route handler fires again on the new connection; pull the latest
|
||||
// WebSocketRoute and push a live frame to prove the 'logs' listener
|
||||
// survived the reconnect.
|
||||
const liveLine = 'live log emitted after the reconnect'
|
||||
const newWs = await getWebSocket()
|
||||
newWs.send(LogsTerminalHelper.buildWsLogFrame([liveLine]))
|
||||
|
||||
await expect(comfyPage.bottomPanel.logs.terminalRoot).toContainText(
|
||||
liveLine
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,18 +5,16 @@ import {
|
||||
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
||||
import type { Size } from '@e2e/fixtures/types'
|
||||
|
||||
function expectedGroupSize(
|
||||
const expectedGroupSize = (
|
||||
nodeBounds: Size,
|
||||
padding: number,
|
||||
titleHeight: number
|
||||
): Size {
|
||||
return {
|
||||
width: nodeBounds.width + padding * 2,
|
||||
// Group height adds one title row above the contained node bounds (which
|
||||
// themselves already include the node's own title), independent of padding.
|
||||
height: nodeBounds.height + padding * 2 + titleHeight
|
||||
}
|
||||
}
|
||||
): Size => ({
|
||||
width: nodeBounds.width + padding * 2,
|
||||
// Group height adds one title row above the contained node bounds (which
|
||||
// themselves already include the node's own title), independent of padding.
|
||||
height: nodeBounds.height + padding * 2 + titleHeight
|
||||
})
|
||||
|
||||
test.describe('Canvas layout settings', { tag: '@canvas' }, () => {
|
||||
test.describe('Comfy.SnapToGrid.GridSize', () => {
|
||||
@@ -26,7 +24,7 @@ test.describe('Canvas layout settings', { tag: '@canvas' }, () => {
|
||||
await comfyPage.nodeOps.clearGraph()
|
||||
})
|
||||
|
||||
async function createNode(comfyPage: ComfyPage) {
|
||||
const createNode = async (comfyPage: ComfyPage) => {
|
||||
const note = await comfyPage.nodeOps.addNode('Note', undefined, {
|
||||
x: 0,
|
||||
y: 0
|
||||
@@ -81,10 +79,10 @@ test.describe('Canvas layout settings', { tag: '@canvas' }, () => {
|
||||
await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
|
||||
})
|
||||
|
||||
async function groupAroundAllNodesWithPadding(
|
||||
const groupAroundAllNodesWithPadding = async (
|
||||
comfyPage: ComfyPage,
|
||||
padding: number
|
||||
): Promise<Size> {
|
||||
): Promise<Size> => {
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.GroupSelectedNodes.Padding',
|
||||
padding
|
||||
@@ -128,16 +126,15 @@ test.describe('Canvas layout settings', { tag: '@canvas' }, () => {
|
||||
|
||||
test.describe('LiteGraph.ContextMenu.Scaling', () => {
|
||||
const ZOOM_SCALE = 2
|
||||
function litegraphContextMenu(comfyPage: ComfyPage) {
|
||||
return comfyPage.page.locator('.litecontextmenu')
|
||||
}
|
||||
const litegraphContextMenu = (comfyPage: ComfyPage) =>
|
||||
comfyPage.page.locator('.litecontextmenu')
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.workflow.loadWorkflow('widgets/load_image_widget')
|
||||
await comfyPage.canvasOps.setScale(ZOOM_SCALE)
|
||||
})
|
||||
|
||||
async function openComboMenu(comfyPage: ComfyPage) {
|
||||
const openComboMenu = async (comfyPage: ComfyPage) => {
|
||||
const loadImage = (
|
||||
await comfyPage.nodeOps.getNodeRefsByType('LoadImage')
|
||||
)[0]
|
||||
|
||||
@@ -3,14 +3,12 @@ import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
|
||||
|
||||
function getLocators(page: Page) {
|
||||
return {
|
||||
trigger: page.getByRole('button', { name: 'Canvas Mode' }),
|
||||
menu: page.getByRole('menu', { name: 'Canvas Mode' }),
|
||||
selectItem: page.getByRole('menuitemradio', { name: 'Select' }),
|
||||
handItem: page.getByRole('menuitemradio', { name: 'Hand' })
|
||||
}
|
||||
}
|
||||
const getLocators = (page: Page) => ({
|
||||
trigger: page.getByRole('button', { name: 'Canvas Mode' }),
|
||||
menu: page.getByRole('menu', { name: 'Canvas Mode' }),
|
||||
selectItem: page.getByRole('menuitemradio', { name: 'Select' }),
|
||||
handItem: page.getByRole('menuitemradio', { name: 'Hand' })
|
||||
})
|
||||
|
||||
const MODES = [
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ import { sleep } from '@e2e/fixtures/utils/timing'
|
||||
|
||||
const CLIP_NODE_COUNT = 2
|
||||
|
||||
async function getClipNodesDragBox(comfyPage: ComfyPage) {
|
||||
const getClipNodesDragBox = async (comfyPage: ComfyPage) => {
|
||||
const clipNodes = await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
|
||||
expect(
|
||||
clipNodes,
|
||||
@@ -242,11 +242,11 @@ test.describe('Canvas settings', { tag: '@canvas' }, () => {
|
||||
* hold), nudge by `(dx, dy)` absolute pixels, then release. Spec-local
|
||||
* because it exists only to probe the CanvasPointer timing thresholds.
|
||||
*/
|
||||
async function holdDragAt(
|
||||
const holdDragAt = async (
|
||||
comfyPage: ComfyPage,
|
||||
pos: { x: number; y: number },
|
||||
opts: { dx: number; dy: number; holdMs: number }
|
||||
) {
|
||||
) => {
|
||||
const abs = await comfyPage.canvasOps.toAbsolute(pos)
|
||||
await comfyPage.page.mouse.move(abs.x, abs.y)
|
||||
await comfyPage.page.mouse.down()
|
||||
@@ -383,9 +383,8 @@ test.describe('Canvas settings', { tag: '@canvas' }, () => {
|
||||
// (CI jitter, background throttling, canvas-idle behaviour). Assert the
|
||||
// render-loop throttle value instead — that is what actually governs
|
||||
// frame cadence.
|
||||
function getFrameGap(comfyPage: ComfyPage) {
|
||||
return comfyPage.page.evaluate(() => window.app!.canvas.maximumFps * 1000)
|
||||
}
|
||||
const getFrameGap = (comfyPage: ComfyPage) =>
|
||||
comfyPage.page.evaluate(() => window.app!.canvas.maximumFps * 1000)
|
||||
|
||||
test('caps the render loop frame gap', async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('LiteGraph.Canvas.MaximumFps', 30)
|
||||
|
||||
@@ -219,7 +219,7 @@ test.describe('Change Tracker', { tag: '@workflow' }, () => {
|
||||
comfyPage
|
||||
}) => {
|
||||
const node = (await comfyPage.nodeOps.getFirstNodeRef())!
|
||||
async function bypassAndPin() {
|
||||
const bypassAndPin = async () => {
|
||||
await beforeChange(comfyPage)
|
||||
await comfyPage.keyboard.bypass()
|
||||
await expect(node).toBeBypassed()
|
||||
@@ -228,14 +228,14 @@ test.describe('Change Tracker', { tag: '@workflow' }, () => {
|
||||
await afterChange(comfyPage)
|
||||
}
|
||||
|
||||
async function collapse() {
|
||||
const collapse = async () => {
|
||||
await beforeChange(comfyPage)
|
||||
await node.click('collapse', { moveMouseToEmptyArea: true })
|
||||
await expect(node).toBeCollapsed()
|
||||
await afterChange(comfyPage)
|
||||
}
|
||||
|
||||
async function multipleChanges() {
|
||||
const multipleChanges = async () => {
|
||||
await beforeChange(comfyPage)
|
||||
// Call other actions that uses begin/endChange
|
||||
await node.click('title')
|
||||
|
||||
@@ -133,11 +133,10 @@ test.describe('Default Keybindings', { tag: '@keyboard' }, () => {
|
||||
await node.click('title')
|
||||
|
||||
// Normal mode is ALWAYS (0)
|
||||
function getMode() {
|
||||
return comfyPage.page.evaluate((nodeId) => {
|
||||
const getMode = () =>
|
||||
comfyPage.page.evaluate((nodeId) => {
|
||||
return window.app!.canvas.graph!.getNodeById(nodeId)!.mode
|
||||
}, node.id)
|
||||
}
|
||||
|
||||
await expect.poll(() => getMode()).toBe(0)
|
||||
|
||||
|
||||
@@ -290,7 +290,7 @@ test('Blueprint overwrite', { tag: ['@subgraph'] }, async ({ comfyPage }) => {
|
||||
|
||||
const confirmDialog = comfyPage.confirmDialog.root
|
||||
const { incrementButton } = comfyPage.vueNodes.getInputNumberControls(steps)
|
||||
async function dirtyGraphAndSave() {
|
||||
const dirtyGraphAndSave = async () => {
|
||||
await incrementButton.click()
|
||||
await comfyPage.page.keyboard.press('Control+s')
|
||||
}
|
||||
|
||||
@@ -21,14 +21,13 @@ test.describe('Group Copy Paste', { tag: ['@canvas'] }, () => {
|
||||
await comfyPage.clipboard.paste()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
function getGroupPositions() {
|
||||
return comfyPage.page.evaluate(() =>
|
||||
const getGroupPositions = () =>
|
||||
comfyPage.page.evaluate(() =>
|
||||
window.app!.graph.groups.map((g: { pos: number[] }) => ({
|
||||
x: g.pos[0],
|
||||
y: g.pos[1]
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
await expect.poll(getGroupPositions).toHaveLength(2)
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ test.describe('Group Node', { tag: '@node' }, () => {
|
||||
test('Manage group opens with the correct group selected', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
async function makeGroup(name: string, type1: string, type2: string) {
|
||||
const makeGroup = async (name: string, type1: string, type2: string) => {
|
||||
const node1 = (await comfyPage.nodeOps.getNodeRefsByType(type1))[0]
|
||||
const node2 = (await comfyPage.nodeOps.getNodeRefsByType(type2))[0]
|
||||
await node1.click('title')
|
||||
@@ -204,7 +204,7 @@ test.describe('Group Node', { tag: '@node' }, () => {
|
||||
test('Reconnects inputs after configuration changed via manage dialog save', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
async function expectSingleNode(type: string) {
|
||||
const expectSingleNode = async (type: string) => {
|
||||
const nodes = await comfyPage.nodeOps.getNodeRefsByType(type)
|
||||
expect(nodes).toHaveLength(1)
|
||||
return nodes[0]
|
||||
@@ -255,13 +255,13 @@ test.describe('Group Node', { tag: '@node' }, () => {
|
||||
const GROUP_NODE_NAME = 'group_node' // Node name in given workflow
|
||||
const GROUP_NODE_TYPE = `${GROUP_NODE_PREFIX}${GROUP_NODE_NAME}`
|
||||
|
||||
async function isRegisteredLitegraph(comfyPage: ComfyPage) {
|
||||
const isRegisteredLitegraph = async (comfyPage: ComfyPage) => {
|
||||
return await comfyPage.page.evaluate((nodeType: string) => {
|
||||
return !!window.LiteGraph!.registered_node_types[nodeType]
|
||||
}, GROUP_NODE_TYPE)
|
||||
}
|
||||
|
||||
async function isRegisteredNodeDefStore(comfyPage: ComfyPage) {
|
||||
const isRegisteredNodeDefStore = async (comfyPage: ComfyPage) => {
|
||||
await comfyPage.menu.nodeLibraryTab.open()
|
||||
const groupNodesFolderCt = await comfyPage.menu.nodeLibraryTab
|
||||
.getFolder(GROUP_NODE_CATEGORY)
|
||||
@@ -269,10 +269,10 @@ test.describe('Group Node', { tag: '@node' }, () => {
|
||||
return groupNodesFolderCt === 1
|
||||
}
|
||||
|
||||
async function verifyNodeLoaded(
|
||||
const verifyNodeLoaded = async (
|
||||
comfyPage: ComfyPage,
|
||||
expectedCount: number
|
||||
) {
|
||||
) => {
|
||||
expect(
|
||||
await comfyPage.nodeOps.getNodeRefsByType(GROUP_NODE_TYPE)
|
||||
).toHaveLength(expectedCount)
|
||||
@@ -361,15 +361,3 @@ test.describe('Group Node', { tag: '@node' }, () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('Convert to subgraph unpacks the group Node @vue-nodes', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow('groupnodes/legacy_group_node')
|
||||
await (await comfyPage.vueNodes.getFixtureByTitle('hello')).title.click()
|
||||
await comfyPage.page.keyboard.press('Control+Shift+e')
|
||||
await expect(comfyPage.vueNodes.getNodeByTitle('New Subgraph')).toBeVisible()
|
||||
|
||||
await comfyPage.vueNodes.enterSubgraph()
|
||||
await expect(comfyPage.vueNodes.getNodeByTitle('')).toHaveCount(2)
|
||||
})
|
||||
|
||||
@@ -510,7 +510,7 @@ test.describe('Image Compare', { tag: ['@widget', '@vue-nodes'] }, () => {
|
||||
const brokenAfter = 'http://127.0.0.1:1/broken2.png'
|
||||
|
||||
const pageErrors: Error[] = []
|
||||
function onPageError(err: Error) {
|
||||
const onPageError = (err: Error) => {
|
||||
pageErrors.push(err)
|
||||
}
|
||||
comfyPage.page.on('pageerror', onPageError)
|
||||
|
||||
@@ -82,10 +82,10 @@ test.describe('Node Interaction', () => {
|
||||
}
|
||||
)
|
||||
|
||||
async function dragSelectNodes(
|
||||
const dragSelectNodes = async (
|
||||
comfyPage: ComfyPage,
|
||||
clipNodes: NodeReference[]
|
||||
) {
|
||||
) => {
|
||||
const clipNode1Pos = await clipNodes[0].getPosition()
|
||||
const clipNode2Pos = await clipNodes[1].getPosition()
|
||||
const offset = 64
|
||||
@@ -117,16 +117,15 @@ test.describe('Node Interaction', () => {
|
||||
}) => {
|
||||
const clipNodes =
|
||||
await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
|
||||
function getPositions() {
|
||||
return Promise.all(clipNodes.map((node) => node.getPosition()))
|
||||
}
|
||||
async function testDirection({
|
||||
const getPositions = () =>
|
||||
Promise.all(clipNodes.map((node) => node.getPosition()))
|
||||
const testDirection = async ({
|
||||
direction,
|
||||
expectedPosition
|
||||
}: {
|
||||
direction: string
|
||||
expectedPosition: (originalPosition: Position) => Position
|
||||
}) {
|
||||
}) => {
|
||||
const originalPositions = await getPositions()
|
||||
await dragSelectNodes(comfyPage, clipNodes)
|
||||
await comfyPage.command.executeCommand(
|
||||
@@ -672,7 +671,7 @@ test.describe('Canvas Interaction', { tag: '@screenshot' }, () => {
|
||||
})
|
||||
|
||||
test('Cursor style changes when panning', async ({ comfyPage }) => {
|
||||
async function getCursorStyle() {
|
||||
const getCursorStyle = async () => {
|
||||
return await comfyPage.page.evaluate(() => {
|
||||
return (
|
||||
document.getElementById('graph-canvas')!.style.cursor || 'default'
|
||||
@@ -704,7 +703,7 @@ test.describe('Canvas Interaction', { tag: '@screenshot' }, () => {
|
||||
test('Properly resets dragging state after pan mode sequence', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
async function getCursorStyle() {
|
||||
const getCursorStyle = async () => {
|
||||
return await comfyPage.page.evaluate(() => {
|
||||
return (
|
||||
document.getElementById('graph-canvas')!.style.cursor || 'default'
|
||||
@@ -879,9 +878,8 @@ test.describe('Load workflow', { tag: '@screenshot' }, () => {
|
||||
)
|
||||
})
|
||||
|
||||
function generateUniqueFilename(extension = '') {
|
||||
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}${extension}`
|
||||
}
|
||||
const generateUniqueFilename = (extension = '') =>
|
||||
`${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}${extension}`
|
||||
|
||||
test.describe('Restore all open workflows on reload', () => {
|
||||
let workflowA: string
|
||||
@@ -1079,7 +1077,7 @@ test.describe('Viewport settings', () => {
|
||||
comfyPage,
|
||||
comfyMouse
|
||||
}) => {
|
||||
async function changeTab(tab: Locator) {
|
||||
const changeTab = async (tab: Locator) => {
|
||||
await tab.click()
|
||||
await comfyPage.nextFrame()
|
||||
await comfyMouse.move(DefaultGraphPositions.emptySpace)
|
||||
@@ -1408,7 +1406,7 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => {
|
||||
test('Cursor changes appropriately in different modes', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
async function getCursorStyle() {
|
||||
const getCursorStyle = async () => {
|
||||
return await comfyPage.page.evaluate(() => {
|
||||
return (
|
||||
document.getElementById('graph-canvas')!.style.cursor || 'default'
|
||||
|
||||
@@ -3,15 +3,14 @@ import type { Page } from '@playwright/test'
|
||||
|
||||
import { load3dTest as test } from '@e2e/fixtures/helpers/Load3DFixtures'
|
||||
|
||||
function getGizmoConfig(page: Page) {
|
||||
return page.evaluate(() => {
|
||||
const getGizmoConfig = (page: Page) =>
|
||||
page.evaluate(() => {
|
||||
const n = window.app!.graph.getNodeById(1)
|
||||
const modelConfig = n?.properties?.['Model Config'] as
|
||||
| { gizmo?: { enabled: boolean; mode: string } }
|
||||
| undefined
|
||||
return modelConfig?.gizmo
|
||||
})
|
||||
}
|
||||
|
||||
test.describe('Load3D Gizmo Controls', () => {
|
||||
test(
|
||||
|
||||
@@ -155,7 +155,7 @@ test.describe('Load3D', () => {
|
||||
async ({ comfyPage, load3d }) => {
|
||||
await expect(load3d.uploadBackgroundImageButton).toBeVisible()
|
||||
const node = await comfyPage.nodeOps.getNodeRefById(1)
|
||||
async function readBackgroundImage() {
|
||||
const readBackgroundImage = async () => {
|
||||
const properties =
|
||||
await node.getProperty<Record<string, { backgroundImage?: string }>>(
|
||||
'properties'
|
||||
@@ -222,7 +222,7 @@ test.describe('Load3D', () => {
|
||||
await expect(load3d.gridToggleButton).toBeVisible()
|
||||
|
||||
const node = await comfyPage.nodeOps.getNodeRefById(1)
|
||||
async function readShowGrid() {
|
||||
const readShowGrid = async () => {
|
||||
const properties =
|
||||
await node.getProperty<Record<string, { showGrid?: boolean }>>(
|
||||
'properties'
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 56 KiB |
@@ -1,65 +0,0 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
||||
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
|
||||
import { openMoreOptionsMenu } from '@e2e/fixtures/utils/selectionToolboxMoreOptions'
|
||||
|
||||
test.describe(
|
||||
'Node context menu shape submenu (FE-570)',
|
||||
{ tag: '@ui' },
|
||||
() => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.settings.setSetting('Comfy.Canvas.SelectionToolbox', true)
|
||||
await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
|
||||
})
|
||||
|
||||
async function expectShapePopoverVisible(comfyPage: ComfyPage) {
|
||||
const popover = comfyPage.page
|
||||
.locator('.p-popover')
|
||||
.filter({ hasText: 'Default' })
|
||||
await expect(popover).toBeVisible()
|
||||
await expect(popover).toContainText('Box')
|
||||
await expect(popover).toContainText('Card')
|
||||
|
||||
const popoverBox = await popover.boundingBox()
|
||||
expect(popoverBox).not.toBeNull()
|
||||
expect(popoverBox!.width).toBeGreaterThan(0)
|
||||
expect(popoverBox!.height).toBeGreaterThan(0)
|
||||
}
|
||||
|
||||
test('Shape popover opens when the menu fits in the viewport', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.page.setViewportSize({ width: 1280, height: 900 })
|
||||
const menu = await openMoreOptionsMenu(comfyPage, 'KSampler')
|
||||
const rootList = menu.locator(':scope > ul')
|
||||
|
||||
await expect
|
||||
.poll(() => rootList.evaluate((el) => getComputedStyle(el).overflowY))
|
||||
.toBe('visible')
|
||||
|
||||
await menu.getByRole('menuitem', { name: 'Shape' }).click()
|
||||
await expectShapePopoverVisible(comfyPage)
|
||||
})
|
||||
|
||||
test('Shape popover opens even when the menu must scroll', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.page.setViewportSize({ width: 1280, height: 520 })
|
||||
const menu = await openMoreOptionsMenu(comfyPage, 'KSampler')
|
||||
const rootList = menu.locator(':scope > ul')
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
rootList.evaluate((el) => el.scrollHeight > el.clientHeight)
|
||||
)
|
||||
.toBe(true)
|
||||
|
||||
const shapeItem = menu.getByRole('menuitem', { name: 'Shape' })
|
||||
await shapeItem.scrollIntoViewIfNeeded()
|
||||
await shapeItem.click()
|
||||
await expectShapePopoverVisible(comfyPage)
|
||||
})
|
||||
}
|
||||
)
|
||||
@@ -55,7 +55,7 @@ async function setLocaleAndWaitForWorkflowReload(
|
||||
const waitForReload = new Promise<void>((resolve, reject) => {
|
||||
const timeoutAt = performance.now() + 5000
|
||||
|
||||
function tick() {
|
||||
const tick = () => {
|
||||
if (changeTracker.isLoadingGraph) {
|
||||
sawLoading = true
|
||||
}
|
||||
|
||||
@@ -166,10 +166,10 @@ test.describe('Node search box', { tag: '@node' }, () => {
|
||||
})
|
||||
|
||||
test.describe('Filtering', () => {
|
||||
async function expectFilterChips(
|
||||
const expectFilterChips = async (
|
||||
comfyPage: ComfyPage,
|
||||
expectedTexts: string[]
|
||||
) {
|
||||
) => {
|
||||
const chips = comfyPage.searchBox.filterChips
|
||||
|
||||
// Check that the number of chips matches the expected count
|
||||
|
||||
@@ -243,18 +243,15 @@ test.describe('Node search box V2', { tag: '@node' }, () => {
|
||||
comfyPage
|
||||
}) => {
|
||||
const { searchBoxV2 } = comfyPage
|
||||
function switchToDesktop() {
|
||||
return comfyPage.page.setViewportSize({ width: 1280, height: 800 })
|
||||
}
|
||||
function switchToMobile() {
|
||||
return comfyPage.page.setViewportSize({ width: 360, height: 800 })
|
||||
}
|
||||
function expectExpanded(value: 'true' | 'false') {
|
||||
return expect(searchBoxV2.sidebarToggle).toHaveAttribute(
|
||||
const switchToDesktop = () =>
|
||||
comfyPage.page.setViewportSize({ width: 1280, height: 800 })
|
||||
const switchToMobile = () =>
|
||||
comfyPage.page.setViewportSize({ width: 360, height: 800 })
|
||||
const expectExpanded = (value: 'true' | 'false') =>
|
||||
expect(searchBoxV2.sidebarToggle).toHaveAttribute(
|
||||
'aria-expanded',
|
||||
value
|
||||
)
|
||||
}
|
||||
|
||||
await switchToDesktop()
|
||||
await searchBoxV2.open()
|
||||
|
||||
@@ -312,9 +312,7 @@ test.describe('Node search box V2 extended', { tag: '@node' }, () => {
|
||||
test.describe('Search behavior', () => {
|
||||
test('Search narrows results progressively', async ({ comfyPage }) => {
|
||||
const { searchBoxV2 } = comfyPage
|
||||
function getCount() {
|
||||
return searchBoxV2.results.count()
|
||||
}
|
||||
const getCount = () => searchBoxV2.results.count()
|
||||
|
||||
await searchBoxV2.open()
|
||||
|
||||
|
||||
@@ -758,8 +758,8 @@ test.describe('Painter', { tag: ['@widget', '@vue-nodes'] }, () => {
|
||||
await drawStroke(comfyPage.page, canvas, { yPct: 0.75 })
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
function hasContentAtRow(yFraction: number) {
|
||||
return canvas.evaluate((el: HTMLCanvasElement, y: number) => {
|
||||
const hasContentAtRow = (yFraction: number) =>
|
||||
canvas.evaluate((el: HTMLCanvasElement, y: number) => {
|
||||
const ctx = el.getContext('2d')
|
||||
if (!ctx) return false
|
||||
const cy = Math.floor(el.height * y)
|
||||
@@ -769,7 +769,6 @@ test.describe('Painter', { tag: ['@widget', '@vue-nodes'] }, () => {
|
||||
}
|
||||
return false
|
||||
}, yFraction)
|
||||
}
|
||||
|
||||
await expect
|
||||
.poll(() => hasContentAtRow(0.25), {
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import {
|
||||
comfyPageFixture as test,
|
||||
comfyExpect as expect
|
||||
} from '@e2e/fixtures/ComfyPage'
|
||||
|
||||
test('Price badge displays on subgraphs @vue-nodes', async ({ comfyPage }) => {
|
||||
const apiNodeName = 'Node With Price Badge'
|
||||
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'v1 (legacy)')
|
||||
|
||||
const priceBadge = comfyPage.page.locator('.lg-node-header i + span')
|
||||
const apiNode = comfyPage.vueNodes.getNodeByTitle(apiNodeName)
|
||||
|
||||
await comfyPage.menu.topbar.newWorkflowButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await comfyPage.page.mouse.dblclick(500, 500, { delay: 5 })
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode(apiNodeName)
|
||||
await expect(comfyPage.searchBox.input).toBeHidden()
|
||||
await expect(apiNode, 'Add partner node').toBeVisible()
|
||||
await expect(apiNode.locator(priceBadge), 'Has price badge').toBeVisible()
|
||||
|
||||
await comfyPage.contextMenu
|
||||
.openForVueNode(apiNode)
|
||||
.then((m) => m.clickMenuItemExact('Convert to Subgraph'))
|
||||
const subgraphNode = comfyPage.vueNodes.getNodeByTitle('New Subgraph')
|
||||
await expect(subgraphNode, 'Convert to Subgraph').toBeVisible()
|
||||
|
||||
const nodePrice = subgraphNode.locator(priceBadge)
|
||||
await expect(nodePrice, 'subgraphNode has price badge').toBeVisible()
|
||||
const initialPrice = Number(await nodePrice.innerText())
|
||||
|
||||
await comfyPage.subgraph.editor.togglePromotion(subgraphNode, {
|
||||
nodeName: apiNodeName,
|
||||
widgetName: 'price',
|
||||
toState: true
|
||||
})
|
||||
await comfyPage.vueNodes.selectComboOption('New Subgraph', 'price', '2x')
|
||||
await expect(nodePrice, 'Price is reactive').toHaveText(
|
||||
String(initialPrice * 2)
|
||||
)
|
||||
})
|
||||
@@ -77,7 +77,7 @@ const cloudUploadRaceTest = comfyPageFixture.extend<{
|
||||
}
|
||||
cloudUploadAssetStateByPage.set(page, state)
|
||||
|
||||
async function assetsRouteHandler(route: Route) {
|
||||
const assetsRouteHandler = async (route: Route) => {
|
||||
const allAssets = [
|
||||
cloudDefaultGraphInputAsset,
|
||||
...(state.isUploadedAssetAvailable ? [cloudUploadedVideoAsset] : [])
|
||||
@@ -149,7 +149,7 @@ async function delayNextUpload(comfyPage: ComfyPage) {
|
||||
releaseUpload = resolve
|
||||
})
|
||||
|
||||
async function uploadRouteHandler(route: Route) {
|
||||
const uploadRouteHandler = async (route: Route) => {
|
||||
resolveUploadStarted()
|
||||
await release
|
||||
await route.continue()
|
||||
|
||||
@@ -15,9 +15,7 @@ const REQUEST_ID_SECONDARY = 2
|
||||
const REQUEST_ID_MISMATCH = 999
|
||||
|
||||
let nextRequestId = 1000
|
||||
function newRequestId() {
|
||||
return nextRequestId++
|
||||
}
|
||||
const newRequestId = () => nextRequestId++
|
||||
|
||||
function bannerLocator(page: Page) {
|
||||
return page.getByTestId(TestIds.queue.notificationBanner)
|
||||
|
||||
@@ -6,11 +6,11 @@ import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
|
||||
test.describe('Remote COMBO Widget', { tag: '@widget' }, () => {
|
||||
const mockOptions = ['d', 'c', 'b', 'a']
|
||||
|
||||
async function addRemoteWidgetNode(
|
||||
const addRemoteWidgetNode = async (
|
||||
comfyPage: ComfyPage,
|
||||
nodeName: string,
|
||||
count: number = 1
|
||||
) {
|
||||
) => {
|
||||
const tab = comfyPage.menu.nodeLibraryTab
|
||||
await tab.open()
|
||||
await tab.getFolder('DevTools').click()
|
||||
@@ -21,24 +21,24 @@ test.describe('Remote COMBO Widget', { tag: '@widget' }, () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function getWidgetOptions(
|
||||
const getWidgetOptions = async (
|
||||
comfyPage: ComfyPage,
|
||||
nodeName: string
|
||||
): Promise<string[] | undefined> {
|
||||
): Promise<string[] | undefined> => {
|
||||
return await comfyPage.page.evaluate((name) => {
|
||||
const node = window.app!.graph!.nodes.find((node) => node.title === name)
|
||||
return node!.widgets![0].options.values as string[] | undefined
|
||||
}, nodeName)
|
||||
}
|
||||
|
||||
async function getWidgetValue(comfyPage: ComfyPage, nodeName: string) {
|
||||
const getWidgetValue = async (comfyPage: ComfyPage, nodeName: string) => {
|
||||
return await comfyPage.page.evaluate((name) => {
|
||||
const node = window.app!.graph!.nodes.find((node) => node.title === name)
|
||||
return node!.widgets![0].value
|
||||
}, nodeName)
|
||||
}
|
||||
|
||||
function clickRefreshButton(comfyPage: ComfyPage, nodeName: string) {
|
||||
const clickRefreshButton = (comfyPage: ComfyPage, nodeName: string) => {
|
||||
return comfyPage.page.evaluate((name) => {
|
||||
const node = window.app!.graph!.nodes.find((node) => node.title === name)
|
||||
const buttonWidget = node!.widgets!.find((w) => w.name === 'refresh')
|
||||
|
||||
@@ -13,21 +13,16 @@ test.beforeEach(async ({ comfyPage }) => {
|
||||
const BLUE_COLOR = 'rgb(51, 51, 85)'
|
||||
const RED_COLOR = 'rgb(85, 51, 51)'
|
||||
|
||||
function getColorPickerButton(comfyPage: { page: Page }) {
|
||||
return comfyPage.page.getByTestId(TestIds.selectionToolbox.colorPickerButton)
|
||||
}
|
||||
const getColorPickerButton = (comfyPage: { page: Page }) =>
|
||||
comfyPage.page.getByTestId(TestIds.selectionToolbox.colorPickerButton)
|
||||
|
||||
function getColorPickerCurrentColor(comfyPage: { page: Page }) {
|
||||
return comfyPage.page.getByTestId(
|
||||
TestIds.selectionToolbox.colorPickerCurrentColor
|
||||
)
|
||||
}
|
||||
const getColorPickerCurrentColor = (comfyPage: { page: Page }) =>
|
||||
comfyPage.page.getByTestId(TestIds.selectionToolbox.colorPickerCurrentColor)
|
||||
|
||||
function getColorPickerGroup(comfyPage: { page: Page }) {
|
||||
return comfyPage.page.getByRole('group').filter({
|
||||
const getColorPickerGroup = (comfyPage: { page: Page }) =>
|
||||
comfyPage.page.getByRole('group').filter({
|
||||
has: comfyPage.page.getByTestId(TestIds.selectionToolbox.colorBlue)
|
||||
})
|
||||
}
|
||||
|
||||
test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
|
||||
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
||||
import { openMoreOptionsMenu } from '@e2e/fixtures/utils/selectionToolboxMoreOptions'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
@@ -19,8 +18,60 @@ test.describe(
|
||||
await comfyPage.nextFrame()
|
||||
})
|
||||
|
||||
function openMoreOptions(comfyPage: ComfyPage) {
|
||||
return openMoreOptionsMenu(comfyPage, 'KSampler')
|
||||
const openMoreOptions = async (comfyPage: ComfyPage) => {
|
||||
const ksamplerNodes =
|
||||
await comfyPage.nodeOps.getNodeRefsByTitle('KSampler')
|
||||
if (ksamplerNodes.length === 0) {
|
||||
throw new Error('No KSampler nodes found')
|
||||
}
|
||||
|
||||
// Drag the KSampler to the center of the screen
|
||||
const nodePos = await ksamplerNodes[0].getPosition()
|
||||
const viewportSize = comfyPage.page.viewportSize()
|
||||
if (!viewportSize) {
|
||||
throw new Error(
|
||||
'Viewport size is null - page may not be properly initialized'
|
||||
)
|
||||
}
|
||||
const centerX = viewportSize.width / 3
|
||||
const centerY = viewportSize.height / 2
|
||||
await comfyPage.canvasOps.dragAndDrop(
|
||||
{ x: nodePos.x, y: nodePos.y },
|
||||
{ x: centerX, y: centerY }
|
||||
)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await ksamplerNodes[0].click('title')
|
||||
|
||||
await expect(comfyPage.page.locator('.selection-toolbox')).toBeVisible()
|
||||
|
||||
const moreOptionsBtn = comfyPage.page.getByTestId('more-options-button')
|
||||
await expect(moreOptionsBtn).toBeVisible()
|
||||
|
||||
await moreOptionsBtn.click()
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const menuOptionsVisible = await comfyPage.page
|
||||
.getByText('Rename')
|
||||
.isVisible({ timeout: 2000 })
|
||||
.catch(() => false)
|
||||
if (menuOptionsVisible) {
|
||||
return
|
||||
}
|
||||
|
||||
await moreOptionsBtn.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const menuOptionsVisibleAfterClick = await comfyPage.page
|
||||
.getByText('Rename')
|
||||
.isVisible({ timeout: 2000 })
|
||||
.catch(() => false)
|
||||
if (menuOptionsVisibleAfterClick) {
|
||||
return
|
||||
}
|
||||
|
||||
throw new Error('Could not open More Options menu - popover not showing')
|
||||
}
|
||||
|
||||
test('hides Node Info from More Options menu when the new menu is disabled', async ({
|
||||
@@ -41,14 +92,11 @@ test.describe(
|
||||
)[0]
|
||||
|
||||
await openMoreOptions(comfyPage)
|
||||
// Shape now opens via body-appended popover (FE-570); a hover no
|
||||
// longer reveals the submenu — match the Color flow and click.
|
||||
await comfyPage.page.getByText('Shape', { exact: true }).click()
|
||||
const shapePopover = comfyPage.page
|
||||
.locator('.p-popover')
|
||||
.filter({ hasText: 'Default' })
|
||||
await expect(shapePopover.getByText('Box', { exact: true })).toBeVisible()
|
||||
await shapePopover.getByText('Box', { exact: true }).click()
|
||||
await comfyPage.page.getByText('Shape', { exact: true }).hover()
|
||||
await expect(
|
||||
comfyPage.page.getByText('Box', { exact: true })
|
||||
).toBeVisible()
|
||||
await comfyPage.page.getByText('Box', { exact: true }).click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect.poll(() => nodeRef.getProperty<number>('shape')).toBe(1)
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
import { expect, mergeTests } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture } from '@e2e/fixtures/ComfyPage'
|
||||
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
||||
import { TestIds } from '@e2e/fixtures/selectors'
|
||||
import {
|
||||
sharedWorkflowImportFixture,
|
||||
sharedWorkflowImportScenario
|
||||
} from '@e2e/fixtures/sharedWorkflowImportFixture'
|
||||
import type { SharedWorkflowImportMocks } from '@e2e/fixtures/sharedWorkflowImportFixture'
|
||||
import { PropertiesPanelHelper } from '@e2e/tests/propertiesPanel/PropertiesPanelHelper'
|
||||
import type { WorkspaceStore } from '@e2e/types/globals'
|
||||
|
||||
const IMPORT_ORDER_TIMEOUT_MS = 5_000
|
||||
|
||||
async function expectImportPrecedesPublicInclusiveInputAssetScan(
|
||||
mocks: SharedWorkflowImportMocks
|
||||
): Promise<void> {
|
||||
await expect(async () => {
|
||||
const events = mocks.getRequestEvents()
|
||||
const importIndex = events.indexOf('import')
|
||||
const afterImportIndex = events.indexOf(
|
||||
'input-assets-including-public-after-import'
|
||||
)
|
||||
|
||||
expect(
|
||||
events,
|
||||
'public-inclusive input assets must not be scanned before import'
|
||||
).not.toContain('input-assets-including-public-before-import')
|
||||
expect(importIndex, `events: ${events.join(',')}`).toBeGreaterThanOrEqual(0)
|
||||
expect(afterImportIndex, `events: ${events.join(',')}`).toBeGreaterThan(
|
||||
importIndex
|
||||
)
|
||||
}).toPass({ timeout: IMPORT_ORDER_TIMEOUT_MS })
|
||||
}
|
||||
|
||||
async function getCachedMissingMediaWarningNames(
|
||||
comfyPage: ComfyPage
|
||||
): Promise<string[] | null> {
|
||||
return await comfyPage.page.evaluate(() => {
|
||||
const workflow = (window.app!.extensionManager as WorkspaceStore).workflow
|
||||
.activeWorkflow
|
||||
if (!workflow) return null
|
||||
|
||||
return (
|
||||
workflow.pendingWarnings?.missingMediaCandidates?.map(
|
||||
(candidate) => candidate.name
|
||||
) ?? []
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async function expectNoMissingMediaAfterPublicInclusiveAssetScan(
|
||||
comfyPage: ComfyPage,
|
||||
mocks: SharedWorkflowImportMocks
|
||||
): Promise<void> {
|
||||
await mocks.waitForPublicInclusiveInputAssetResponseAfterImport()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect(
|
||||
comfyPage.page.getByTestId(TestIds.dialogs.errorOverlay)
|
||||
).toBeHidden()
|
||||
await expect
|
||||
.poll(() => getCachedMissingMediaWarningNames(comfyPage))
|
||||
.toEqual([])
|
||||
}
|
||||
|
||||
async function openPanelAndExpectNoMissingMedia(
|
||||
comfyPage: ComfyPage
|
||||
): Promise<void> {
|
||||
const page = comfyPage.page
|
||||
const errorOverlay = page.getByTestId(TestIds.dialogs.errorOverlay)
|
||||
await expect(errorOverlay).toBeHidden()
|
||||
|
||||
const panel = new PropertiesPanelHelper(page)
|
||||
await panel.open(comfyPage.actionbar.propertiesButton)
|
||||
await expect(
|
||||
panel.root.getByTestId(TestIds.propertiesPanel.errorsTab)
|
||||
).toBeHidden()
|
||||
await expect(page.getByTestId(TestIds.dialogs.missingMediaGroup)).toHaveCount(
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
const test = mergeTests(comfyPageFixture, sharedWorkflowImportFixture)
|
||||
|
||||
test.describe('Shared workflow missing media', { tag: '@cloud' }, () => {
|
||||
// Missing media only surfaces the overlay when the Errors tab is enabled
|
||||
// (src/stores/executionErrorStore.ts).
|
||||
test.use({
|
||||
initialSettings: {
|
||||
'Comfy.RightSidePanel.ShowErrorsTab': true
|
||||
}
|
||||
})
|
||||
|
||||
test.beforeEach(async ({ comfyPage, sharedWorkflowImportMocks }) => {
|
||||
sharedWorkflowImportMocks.resetAndStartRecording()
|
||||
await comfyPage.setup({
|
||||
clearStorage: false,
|
||||
url: `/?share=${sharedWorkflowImportScenario.shareId}`
|
||||
})
|
||||
})
|
||||
|
||||
test('imports shared media before loading workflow so missing media is not surfaced', async ({
|
||||
comfyPage,
|
||||
sharedWorkflowImportMocks
|
||||
}) => {
|
||||
const { page } = comfyPage
|
||||
|
||||
const dialog = page.getByTestId(TestIds.dialogs.openSharedWorkflow)
|
||||
await expect(
|
||||
dialog.getByTestId(TestIds.dialogs.openSharedWorkflowTitle)
|
||||
).toBeVisible()
|
||||
|
||||
await dialog.getByTestId(TestIds.dialogs.openSharedWorkflowConfirm).click()
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(() =>
|
||||
window.app!.graph.nodes.map((node) => ({
|
||||
type: node.type,
|
||||
value: node.widgets?.[0]?.value
|
||||
}))
|
||||
)
|
||||
)
|
||||
.toEqual([
|
||||
{
|
||||
type: 'LoadImage',
|
||||
value: sharedWorkflowImportScenario.inputFileName
|
||||
}
|
||||
])
|
||||
await expectImportPrecedesPublicInclusiveInputAssetScan(
|
||||
sharedWorkflowImportMocks
|
||||
)
|
||||
await expectNoMissingMediaAfterPublicInclusiveAssetScan(
|
||||
comfyPage,
|
||||
sharedWorkflowImportMocks
|
||||
)
|
||||
|
||||
expect(sharedWorkflowImportMocks.getImportBody()).toEqual({
|
||||
published_asset_ids: [sharedWorkflowImportScenario.publishedAssetId],
|
||||
share_id: sharedWorkflowImportScenario.shareId
|
||||
})
|
||||
expect(new URL(page.url()).searchParams.has('share')).toBe(false)
|
||||
await openPanelAndExpectNoMissingMedia(comfyPage)
|
||||
})
|
||||
})
|
||||
@@ -1,274 +0,0 @@
|
||||
import { expect, mergeTests } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture } from '@e2e/fixtures/ComfyPage'
|
||||
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
||||
import {
|
||||
createRouteMockJob,
|
||||
jobsRouteFixture,
|
||||
routeMockJobTimestamp
|
||||
} from '@e2e/fixtures/jobsRouteFixture'
|
||||
import type { JobsScenario } from '@e2e/fixtures/jobsRouteFixture'
|
||||
import { TestIds } from '@e2e/fixtures/selectors'
|
||||
import type { RawJobListItem } from '@/platform/remote/comfyui/jobs/jobTypes'
|
||||
|
||||
const historyJobs: RawJobListItem[] = [
|
||||
createRouteMockJob({
|
||||
id: 'history-completed',
|
||||
status: 'completed',
|
||||
create_time: routeMockJobTimestamp - 60_000,
|
||||
execution_start_time: routeMockJobTimestamp - 60_000,
|
||||
execution_end_time: routeMockJobTimestamp - 55_000,
|
||||
preview_output: {
|
||||
filename: 'completed-output.png',
|
||||
subfolder: '',
|
||||
type: 'output',
|
||||
nodeId: '1',
|
||||
mediaType: 'images'
|
||||
}
|
||||
}),
|
||||
createRouteMockJob({
|
||||
id: 'history-failed',
|
||||
status: 'failed',
|
||||
create_time: routeMockJobTimestamp - 120_000,
|
||||
execution_start_time: routeMockJobTimestamp - 120_000,
|
||||
execution_end_time: routeMockJobTimestamp - 118_000,
|
||||
outputs_count: 0,
|
||||
execution_error: {
|
||||
node_id: '1',
|
||||
node_type: 'SaveImage',
|
||||
exception_message: 'Intentional fixture failure',
|
||||
exception_type: 'Error',
|
||||
traceback: [],
|
||||
current_inputs: {},
|
||||
current_outputs: {}
|
||||
}
|
||||
}),
|
||||
createRouteMockJob({
|
||||
id: 'history-cancelled',
|
||||
status: 'cancelled',
|
||||
create_time: routeMockJobTimestamp - 180_000,
|
||||
execution_start_time: routeMockJobTimestamp - 180_000,
|
||||
execution_end_time: routeMockJobTimestamp - 179_000,
|
||||
outputs_count: 0
|
||||
})
|
||||
]
|
||||
|
||||
const activeJobs: RawJobListItem[] = [
|
||||
createRouteMockJob({
|
||||
id: 'queue-running',
|
||||
status: 'in_progress',
|
||||
create_time: routeMockJobTimestamp - 10_000,
|
||||
execution_start_time: routeMockJobTimestamp - 9_000,
|
||||
execution_end_time: null,
|
||||
outputs_count: 0
|
||||
}),
|
||||
createRouteMockJob({
|
||||
id: 'queue-pending',
|
||||
status: 'pending',
|
||||
create_time: routeMockJobTimestamp - 5_000,
|
||||
execution_start_time: null,
|
||||
execution_end_time: null,
|
||||
outputs_count: 0
|
||||
})
|
||||
]
|
||||
const runningOnlyJobs = activeJobs.filter((job) => job.status !== 'pending')
|
||||
|
||||
const test = mergeTests(comfyPageFixture, jobsRouteFixture).extend<{
|
||||
initialJobsScenario: JobsScenario
|
||||
mockInitialJobsScenario: void
|
||||
}>({
|
||||
initialJobsScenario: [
|
||||
{ history: historyJobs, queue: activeJobs },
|
||||
{ option: true }
|
||||
],
|
||||
mockInitialJobsScenario: [
|
||||
async ({ jobsRoutes, initialJobsScenario }, use) => {
|
||||
await jobsRoutes.mockJobsScenario(initialJobsScenario)
|
||||
await use()
|
||||
},
|
||||
{ auto: true }
|
||||
]
|
||||
})
|
||||
|
||||
async function openJobHistorySidebar(comfyPage: ComfyPage) {
|
||||
await comfyPage.page
|
||||
.getByTestId(TestIds.sidebar.toolbar)
|
||||
.getByRole('button', { name: 'Job History', exact: true })
|
||||
.click()
|
||||
await expect(jobHistorySidebar(comfyPage)).toBeVisible()
|
||||
}
|
||||
|
||||
function jobRow(comfyPage: ComfyPage) {
|
||||
const list = comfyPage.page.getByTestId(TestIds.queue.jobAssetsList)
|
||||
|
||||
return (jobId: string) => list.locator(`[data-job-id="${jobId}"]`)
|
||||
}
|
||||
|
||||
function jobHistorySidebar(comfyPage: ComfyPage) {
|
||||
return comfyPage.page.getByTestId(TestIds.queue.jobHistorySidebar)
|
||||
}
|
||||
|
||||
function clearQueueButton(comfyPage: ComfyPage) {
|
||||
return jobHistorySidebar(comfyPage).getByRole('button', {
|
||||
name: 'Clear queue',
|
||||
exact: true
|
||||
})
|
||||
}
|
||||
|
||||
async function openSidebarClearHistoryDialog(comfyPage: ComfyPage) {
|
||||
await jobHistorySidebar(comfyPage)
|
||||
.getByLabel(/More options/i)
|
||||
.click()
|
||||
await comfyPage.page.getByTestId(TestIds.queue.clearHistoryAction).click()
|
||||
}
|
||||
|
||||
test.describe('Job history sidebar', { tag: '@ui' }, () => {
|
||||
test.describe('docked overlay action', () => {
|
||||
test.use({ initialSettings: { 'Comfy.Queue.QPOV2': false } })
|
||||
|
||||
test('opens from the queue overlay docked history action', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.page.getByTestId(TestIds.queue.overlayToggle).click()
|
||||
await comfyPage.queuePanel.moreOptionsButton.click()
|
||||
await comfyPage.page
|
||||
.getByTestId(TestIds.queue.dockedJobHistoryAction)
|
||||
.click()
|
||||
|
||||
await expect(jobHistorySidebar(comfyPage)).toBeVisible()
|
||||
await expect(jobRow(comfyPage)('history-completed')).toBeVisible()
|
||||
await expect(jobRow(comfyPage)('queue-pending')).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('expanded history tab', () => {
|
||||
test.use({ initialSettings: { 'Comfy.Queue.QPOV2': true } })
|
||||
|
||||
test('shows terminal and active job states', async ({ comfyPage }) => {
|
||||
await openJobHistorySidebar(comfyPage)
|
||||
|
||||
const row = jobRow(comfyPage)
|
||||
await expect(row('queue-pending')).toBeVisible()
|
||||
await expect(row('queue-running')).toBeVisible()
|
||||
await expect(row('history-completed')).toBeVisible()
|
||||
await expect(row('history-failed')).toBeVisible()
|
||||
await expect(row('history-cancelled')).toBeVisible()
|
||||
|
||||
await expect(clearQueueButton(comfyPage)).toBeEnabled()
|
||||
})
|
||||
|
||||
test('filters completed and failed history jobs', async ({ comfyPage }) => {
|
||||
await openJobHistorySidebar(comfyPage)
|
||||
|
||||
await comfyPage.page
|
||||
.getByRole('button', { name: 'Completed', exact: true })
|
||||
.click()
|
||||
|
||||
const row = jobRow(comfyPage)
|
||||
await expect(row('history-completed')).toBeVisible()
|
||||
await expect(row('history-failed')).toBeHidden()
|
||||
await expect(row('queue-running')).toBeHidden()
|
||||
|
||||
await comfyPage.page
|
||||
.getByRole('button', { name: 'Failed', exact: true })
|
||||
.click()
|
||||
|
||||
await expect(row('history-failed')).toBeVisible()
|
||||
await expect(row('history-cancelled')).toBeVisible()
|
||||
await expect(row('history-completed')).toBeHidden()
|
||||
})
|
||||
|
||||
test('searches by job id and output filename', async ({ comfyPage }) => {
|
||||
await openJobHistorySidebar(comfyPage)
|
||||
|
||||
const row = jobRow(comfyPage)
|
||||
const searchInput = comfyPage.page.getByPlaceholder('Search...')
|
||||
|
||||
await searchInput.fill('history-failed')
|
||||
await expect(row('history-failed')).toBeVisible()
|
||||
await expect(row('history-completed')).toBeHidden()
|
||||
await expect(row('queue-running')).toBeHidden()
|
||||
|
||||
await searchInput.fill('completed-output')
|
||||
await expect(row('history-completed')).toBeVisible()
|
||||
await expect(row('history-failed')).toBeHidden()
|
||||
|
||||
await searchInput.clear()
|
||||
await expect(row('history-completed')).toBeVisible()
|
||||
await expect(row('queue-running')).toBeVisible()
|
||||
})
|
||||
|
||||
test('clears pending queue jobs and leaves running/history jobs', async ({
|
||||
comfyPage,
|
||||
jobsRoutes
|
||||
}) => {
|
||||
await openJobHistorySidebar(comfyPage)
|
||||
|
||||
const row = jobRow(comfyPage)
|
||||
await expect(row('queue-pending')).toBeVisible()
|
||||
|
||||
const clearQueueRequests = await jobsRoutes.mockClearQueue()
|
||||
const clearHistoryRequests = await jobsRoutes.mockClearHistory()
|
||||
await jobsRoutes.mockJobsScenario({
|
||||
history: historyJobs,
|
||||
queue: runningOnlyJobs
|
||||
})
|
||||
|
||||
await clearQueueButton(comfyPage).click()
|
||||
|
||||
await expect.poll(() => clearQueueRequests.length).toBe(1)
|
||||
expect(clearQueueRequests).toContainEqual({ clear: true })
|
||||
await expect(row('queue-pending')).toBeHidden()
|
||||
await expect(row('queue-running')).toBeVisible()
|
||||
await expect(row('history-completed')).toBeVisible()
|
||||
await expect(clearQueueButton(comfyPage)).toBeDisabled()
|
||||
expect(clearHistoryRequests).toHaveLength(0)
|
||||
})
|
||||
|
||||
test('clears history from the sidebar menu and keeps active jobs', async ({
|
||||
comfyPage,
|
||||
jobsRoutes
|
||||
}) => {
|
||||
await openJobHistorySidebar(comfyPage)
|
||||
|
||||
const row = jobRow(comfyPage)
|
||||
await expect(row('history-completed')).toBeVisible()
|
||||
|
||||
const clearHistoryRequests = await jobsRoutes.mockClearHistory()
|
||||
const clearQueueRequests = await jobsRoutes.mockClearQueue()
|
||||
await jobsRoutes.mockJobsScenario({
|
||||
history: [],
|
||||
queue: activeJobs
|
||||
})
|
||||
|
||||
await openSidebarClearHistoryDialog(comfyPage)
|
||||
await expect(
|
||||
comfyPage.page.getByText('Clear your job queue history?')
|
||||
).toBeVisible()
|
||||
await comfyPage.page
|
||||
.getByRole('button', { name: 'Clear', exact: true })
|
||||
.click()
|
||||
|
||||
await expect.poll(() => clearHistoryRequests.length).toBe(1)
|
||||
expect(clearHistoryRequests).toContainEqual({ clear: true })
|
||||
await expect(row('history-completed')).toBeHidden()
|
||||
await expect(row('history-failed')).toBeHidden()
|
||||
await expect(row('queue-running')).toBeVisible()
|
||||
await expect(row('queue-pending')).toBeVisible()
|
||||
expect(clearQueueRequests).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('without pending queue jobs', () => {
|
||||
test.use({
|
||||
initialJobsScenario: { history: historyJobs, queue: runningOnlyJobs },
|
||||
initialSettings: { 'Comfy.Queue.QPOV2': true }
|
||||
})
|
||||
|
||||
test('disables clear queue', async ({ comfyPage }) => {
|
||||
await openJobHistorySidebar(comfyPage)
|
||||
|
||||
await expect(clearQueueButton(comfyPage)).toBeDisabled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -114,12 +114,11 @@ test.describe('Sidebar splitter width independence', () => {
|
||||
await dragGutter(comfyPage, 80)
|
||||
|
||||
// Check that saved sizes sum to ~100%
|
||||
function getSidebarSizes() {
|
||||
return comfyPage.page.evaluate(() => {
|
||||
const getSidebarSizes = () =>
|
||||
comfyPage.page.evaluate(() => {
|
||||
const raw = localStorage.getItem('unified-sidebar')
|
||||
return raw ? (JSON.parse(raw) as number[]) : null
|
||||
})
|
||||
}
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
|
||||
@@ -25,7 +25,7 @@ const MISSING_NODES_SUBGRAPH_NODE_ID = '2'
|
||||
* the root graph, then the inner subgraph node that appears inside. Matches
|
||||
* how a user navigates via the canvas.
|
||||
*/
|
||||
async function enterNestedSubgraphs(comfyPage: ComfyPage) {
|
||||
const enterNestedSubgraphs = async (comfyPage: ComfyPage) => {
|
||||
const outerNode = await comfyPage.nodeOps.getNodeRefById(
|
||||
OUTER_SUBGRAPH_NODE_ID_IN_NESTED
|
||||
)
|
||||
|
||||
@@ -689,9 +689,7 @@ test('Can intermix linked and proxy @vue-nodes', async ({ comfyPage }) => {
|
||||
const fromSlot = ksampler.getSlot('steps')
|
||||
const toPos = await comfyPage.subgraph.getInputSlot().getOpenSlotPosition()
|
||||
await fromSlot.dragTo(comfyPage.canvas, { targetPosition: toPos })
|
||||
function isConnected() {
|
||||
return comfyPage.vueNodes.isSlotConnected(fromSlot)
|
||||
}
|
||||
const isConnected = () => comfyPage.vueNodes.isSlotConnected(fromSlot)
|
||||
await expect.poll(isConnected).toBe(true)
|
||||
|
||||
await comfyPage.subgraph.exitViaBreadcrumb()
|
||||
@@ -737,9 +735,7 @@ test('Link already promoted widget @vue-nodes', async ({ comfyPage }) => {
|
||||
const fromSlot = ksampler.getSlot('steps')
|
||||
const toPos = await comfyPage.subgraph.getInputSlot().getOpenSlotPosition()
|
||||
await fromSlot.dragTo(comfyPage.canvas, { targetPosition: toPos })
|
||||
function isConnected() {
|
||||
return comfyPage.vueNodes.isSlotConnected(fromSlot)
|
||||
}
|
||||
const isConnected = () => comfyPage.vueNodes.isSlotConnected(fromSlot)
|
||||
await expect.poll(isConnected).toBe(true)
|
||||
|
||||
await comfyPage.subgraph.exitViaBreadcrumb()
|
||||
@@ -816,9 +812,7 @@ test('Linked widgets can not be demoted @vue-nodes', async ({ comfyPage }) => {
|
||||
const fromSlot = ksampler.getSlot('steps')
|
||||
const toPos = await comfyPage.subgraph.getInputSlot().getOpenSlotPosition()
|
||||
await fromSlot.dragTo(comfyPage.canvas, { targetPosition: toPos })
|
||||
function isConnected() {
|
||||
return comfyPage.vueNodes.isSlotConnected(fromSlot)
|
||||
}
|
||||
const isConnected = () => comfyPage.vueNodes.isSlotConnected(fromSlot)
|
||||
await expect.poll(isConnected).toBe(true)
|
||||
|
||||
await comfyPage.subgraph.exitViaBreadcrumb()
|
||||
|
||||
@@ -368,16 +368,15 @@ test.describe('Subgraph Serialization', { tag: ['@subgraph'] }, () => {
|
||||
]
|
||||
|
||||
const SENTINEL_IDS = new Set([-1, -10, -20])
|
||||
function isSentinelNodeId(id: number | string): id is number {
|
||||
return typeof id === 'number' && SENTINEL_IDS.has(id)
|
||||
}
|
||||
const isSentinelNodeId = (id: number | string): id is number =>
|
||||
typeof id === 'number' && SENTINEL_IDS.has(id)
|
||||
|
||||
function checkEndpoint(
|
||||
const checkEndpoint = (
|
||||
label: string,
|
||||
kind: 'origin_id' | 'target_id',
|
||||
id: number | string,
|
||||
g: typeof graph
|
||||
): string | null {
|
||||
): string | null => {
|
||||
if (isSentinelNodeId(id)) return null
|
||||
if (typeof id !== 'number' || !g._nodes_by_id[id]) {
|
||||
return `${label}: ${kind} ${id} invalid or not found`
|
||||
|
||||
@@ -632,75 +632,3 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test(
|
||||
'link interactions',
|
||||
{ tag: ['@vue-nodes', '@subgraph'] },
|
||||
async ({ comfyPage }) => {
|
||||
await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph')
|
||||
await comfyPage.vueNodes.enterSubgraph('2')
|
||||
|
||||
const ksampler = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
|
||||
const seedSlot = ksampler.getSlot('seed')
|
||||
const seedIOSlot = await comfyPage.subgraph.getInputSlot('seed')
|
||||
|
||||
await test.step('Make second INT typed connection', async () => {
|
||||
const toPos = await seedIOSlot.getOpenSlotPosition()
|
||||
await seedSlot.dragTo(comfyPage.canvas, { targetPosition: toPos })
|
||||
function isConnected() {
|
||||
return comfyPage.vueNodes.isSlotConnected(seedSlot)
|
||||
}
|
||||
await expect.poll(isConnected).toBe(true)
|
||||
})
|
||||
|
||||
const stepsSlot = ksampler.getSlot('steps')
|
||||
|
||||
await test.step('Node -> I/O hover effect', async () => {
|
||||
await stepsSlot.hover()
|
||||
await stepsSlot.click({ trial: true })
|
||||
await comfyPage.page.mouse.down()
|
||||
await comfyPage.canvas.hover({ position: await seedIOSlot.getPosition() })
|
||||
|
||||
const rawClip = await comfyPage.subgraph.getInputBounds()
|
||||
const absolutePos = await comfyPage.canvasOps.toAbsolute(rawClip)
|
||||
const clip = { ...rawClip, ...absolutePos }
|
||||
await expect(comfyPage.page).toHaveScreenshot('vue-io-highlight.png', {
|
||||
clip
|
||||
})
|
||||
|
||||
//cancel link operation
|
||||
await stepsSlot.hover()
|
||||
await comfyPage.page.mouse.up()
|
||||
})
|
||||
|
||||
await ksampler.title.hover()
|
||||
|
||||
const slotParent = stepsSlot.locator('../..')
|
||||
await expect(slotParent, 'unconnected slot is hidden').toHaveCSS(
|
||||
'opacity',
|
||||
'0'
|
||||
)
|
||||
|
||||
await test.step('Connect I/O to node with snap', async () => {
|
||||
function hasSnap() {
|
||||
return comfyPage.page.evaluate(() => !!app!.canvas._highlight_pos)
|
||||
}
|
||||
expect(await hasSnap()).toBe(false)
|
||||
|
||||
const emptySlotPos = await seedIOSlot.getOpenSlotPosition()
|
||||
await comfyPage.canvas.hover({ position: emptySlotPos })
|
||||
await comfyPage.page.mouse.down()
|
||||
await stepsSlot.hover()
|
||||
await expect.poll(hasSnap).toBe(true)
|
||||
await comfyPage.page.mouse.up()
|
||||
|
||||
//move hover off the slot
|
||||
await ksampler.title.hover()
|
||||
})
|
||||
|
||||
await expect(slotParent, 'connected slot is visible').not.toHaveCSS(
|
||||
'opacity',
|
||||
'0'
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.0 KiB |
@@ -14,7 +14,7 @@ test.describe(
|
||||
)
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
async function assertInSubgraph(inSubgraph: boolean) {
|
||||
const assertInSubgraph = async (inSubgraph: boolean) => {
|
||||
await expect
|
||||
.poll(() => comfyPage.subgraph.isInSubgraph())
|
||||
.toBe(inSubgraph)
|
||||
|
||||
@@ -143,7 +143,7 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => {
|
||||
})
|
||||
|
||||
await expect(
|
||||
comfyPage.page.getByTestId(TestIds.dialogs.openSharedWorkflowTitle)
|
||||
comfyPage.page.getByRole('heading', { name: 'Open shared workflow' })
|
||||
).toBeVisible()
|
||||
|
||||
await expect(comfyPage.templates.content).toBeHidden()
|
||||
|
||||
@@ -7,9 +7,9 @@ test.describe('Version Mismatch Warnings', { tag: '@slow' }, () => {
|
||||
const ALWAYS_AHEAD_OF_INSTALLED_VERSION = '100.100.100'
|
||||
const ALWAYS_BEHIND_INSTALLED_VERSION = '0.0.0'
|
||||
|
||||
function createMockSystemStatsRes(
|
||||
const createMockSystemStatsRes = (
|
||||
requiredFrontendVersion: string
|
||||
): SystemStats {
|
||||
): SystemStats => {
|
||||
return {
|
||||
system: {
|
||||
os: 'posix',
|
||||
|
||||
@@ -79,7 +79,7 @@ async function getNodeGroupCenteringErrors(
|
||||
|
||||
const nodeRect = nodeElement.getBoundingClientRect()
|
||||
|
||||
function getCenteringError(group: GraphGroup): NodeGroupCenteringError {
|
||||
const getCenteringError = (group: GraphGroup): NodeGroupCenteringError => {
|
||||
const [groupStartX, groupStartY] = app.canvasPosToClientPos([
|
||||
group.pos[0],
|
||||
group.pos[1]
|
||||
|
||||
@@ -1151,14 +1151,12 @@ test.describe('Vue Node Widget Link Position', { tag: '@vue-nodes' }, () => {
|
||||
const ksampler = await comfyPage.page.evaluate(() => {
|
||||
const node = window.app!.graph.nodes.find((n) => n.type === 'KSampler')
|
||||
if (!node) return null
|
||||
const ksamplerNode = node
|
||||
function findIndex(name: string) {
|
||||
return ksamplerNode.inputs.findIndex(
|
||||
const findIndex = (name: string) =>
|
||||
node.inputs.findIndex(
|
||||
(input) => input.name === name || input.widget?.name === name
|
||||
)
|
||||
}
|
||||
return {
|
||||
id: ksamplerNode.id,
|
||||
id: node.id,
|
||||
denoiseIndex: findIndex('denoise'),
|
||||
schedulerIndex: findIndex('scheduler')
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@ import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
||||
import type { Position } from '@e2e/fixtures/types'
|
||||
|
||||
test.describe('Vue Node Moving', { tag: '@vue-nodes' }, () => {
|
||||
async function getHeaderPos(
|
||||
const getHeaderPos = async (
|
||||
comfyPage: ComfyPage,
|
||||
title: string
|
||||
): Promise<{ x: number; y: number; width: number; height: number }> {
|
||||
): Promise<{ x: number; y: number; width: number; height: number }> => {
|
||||
const box = await comfyPage.vueNodes
|
||||
.getNodeByTitle(title)
|
||||
.getByTestId('node-title')
|
||||
@@ -21,30 +21,27 @@ test.describe('Vue Node Moving', { tag: '@vue-nodes' }, () => {
|
||||
return box
|
||||
}
|
||||
|
||||
async function getLoadCheckpointHeaderPos(comfyPage: ComfyPage) {
|
||||
return getHeaderPos(comfyPage, 'Load Checkpoint')
|
||||
}
|
||||
const getLoadCheckpointHeaderPos = async (comfyPage: ComfyPage) =>
|
||||
getHeaderPos(comfyPage, 'Load Checkpoint')
|
||||
|
||||
async function expectPosChanged(pos1: Position, pos2: Position) {
|
||||
const expectPosChanged = async (pos1: Position, pos2: Position) => {
|
||||
const diffX = Math.abs(pos2.x - pos1.x)
|
||||
const diffY = Math.abs(pos2.y - pos1.y)
|
||||
expect(diffX).toBeGreaterThan(0)
|
||||
expect(diffY).toBeGreaterThan(0)
|
||||
}
|
||||
|
||||
function deltaBetween(before: Position, after: Position) {
|
||||
return {
|
||||
x: after.x - before.x,
|
||||
y: after.y - before.y
|
||||
}
|
||||
}
|
||||
const deltaBetween = (before: Position, after: Position) => ({
|
||||
x: after.x - before.x,
|
||||
y: after.y - before.y
|
||||
})
|
||||
|
||||
function expectSameDelta(a: Position, b: Position, tol = 2) {
|
||||
const expectSameDelta = (a: Position, b: Position, tol = 2) => {
|
||||
expect(Math.abs(a.x - b.x)).toBeLessThanOrEqual(tol)
|
||||
expect(Math.abs(a.y - b.y)).toBeLessThanOrEqual(tol)
|
||||
}
|
||||
|
||||
async function dragFromTabButton(comfyPage: ComfyPage, button: Locator) {
|
||||
const dragFromTabButton = async (comfyPage: ComfyPage, button: Locator) => {
|
||||
const box = await button.boundingBox()
|
||||
if (!box) throw new Error('Tab button has no bounding box')
|
||||
const start = {
|
||||
@@ -175,7 +172,7 @@ test.describe('Vue Node Moving', { tag: '@vue-nodes' }, () => {
|
||||
const dx = 120
|
||||
const dy = 80
|
||||
|
||||
async function clickNodeTitleWithMeta(title: string) {
|
||||
const clickNodeTitleWithMeta = async (title: string) => {
|
||||
await comfyPage.vueNodes
|
||||
.getNodeByTitle(title)
|
||||
.getByTestId('node-title')
|
||||
|
||||
@@ -15,11 +15,10 @@ test('@vue-nodes In App Mode, widget width updates with panel size', async ({
|
||||
await comfyPage.appMode.enterAppModeWithInputs([['10', 'legacy_widget']])
|
||||
})
|
||||
|
||||
function getWidth() {
|
||||
return comfyPage.page.evaluate(
|
||||
const getWidth = () =>
|
||||
comfyPage.page.evaluate(
|
||||
() => graph!.getNodeById(10)!.widgets![0].width ?? 0
|
||||
)
|
||||
}
|
||||
|
||||
await test.step('Mouse clicks resolve to button regions', async () => {
|
||||
const legacyWidget = comfyPage.appMode.linearWidgets.locator('canvas')
|
||||
|
||||
@@ -5,15 +5,11 @@ import {
|
||||
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
||||
|
||||
test.describe('Vue Multiline String Widget', { tag: '@vue-nodes' }, () => {
|
||||
function getFirstClipNode(comfyPage: ComfyPage) {
|
||||
return comfyPage.vueNodes
|
||||
.getNodeByTitle('CLIP Text Encode (Prompt)')
|
||||
.first()
|
||||
}
|
||||
const getFirstClipNode = (comfyPage: ComfyPage) =>
|
||||
comfyPage.vueNodes.getNodeByTitle('CLIP Text Encode (Prompt)').first()
|
||||
|
||||
function getFirstMultilineStringWidget(comfyPage: ComfyPage) {
|
||||
return getFirstClipNode(comfyPage).getByRole('textbox', { name: 'text' })
|
||||
}
|
||||
const getFirstMultilineStringWidget = (comfyPage: ComfyPage) =>
|
||||
getFirstClipNode(comfyPage).getByRole('textbox', { name: 'text' })
|
||||
|
||||
test('should allow entering text', async ({ comfyPage }) => {
|
||||
const textarea = getFirstMultilineStringWidget(comfyPage)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user