Compare commits

..

1 Commits

Author SHA1 Message Date
coderabbitai[bot]
8418a14fd2 CodeRabbit Generated Unit Tests: Add unit tests 2026-05-20 04:28:55 +00:00
722 changed files with 4218 additions and 5220 deletions

View File

@@ -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

View File

@@ -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"],

View File

@@ -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

View File

@@ -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')

View File

@@ -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' })
}

View File

@@ -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)

View File

@@ -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>

View File

@@ -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

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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

View File

@@ -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
}

View File

@@ -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(

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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()

View File

@@ -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))
)

View File

@@ -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>

View File

@@ -52,7 +52,7 @@ const electron = electronAPI()
const terminalVisible = ref(false)
function toggleConsoleDrawer() {
const toggleConsoleDrawer = () => {
terminalVisible.value = !terminalVisible.value
}

View File

@@ -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')

View File

@@ -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',

View File

@@ -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 = {

View File

@@ -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()

View File

@@ -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
}

View File

@@ -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()

View File

@@ -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)

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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(

View File

@@ -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

View File

@@ -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))
}

View File

@@ -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)

View File

@@ -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))
}

View File

@@ -0,0 +1,95 @@
import { describe, expect, it } from 'vitest'
import { externalLinks, getRoutes } from './routes'
describe('getRoutes', () => {
it('returns the base routes unchanged for the default locale', () => {
const routes = getRoutes()
expect(routes.home).toBe('/')
expect(routes.cloud).toBe('/cloud')
expect(routes.careers).toBe('/careers')
expect(routes.termsOfService).toBe('/terms-of-service')
expect(routes.privacyPolicy).toBe('/privacy-policy')
})
it('returns the base routes unchanged for explicit "en" locale', () => {
const routes = getRoutes('en')
expect(routes.home).toBe('/')
expect(routes.download).toBe('/download')
expect(routes.cloudPricing).toBe('/cloud/pricing')
expect(routes.termsOfService).toBe('/terms-of-service')
})
it('prefixes every route including termsOfService for zh-CN locale', () => {
const routes = getRoutes('zh-CN')
// Previously termsOfService was locale-invariant and would NOT get a prefix.
// After removing localeInvariantRouteKeys, it now receives the prefix too.
expect(routes.termsOfService).toBe('/zh-CN/terms-of-service')
})
it('prefixes all routes for zh-CN locale', () => {
const routes = getRoutes('zh-CN')
expect(routes.home).toBe('/zh-CN/')
expect(routes.cloud).toBe('/zh-CN/cloud')
expect(routes.cloudPricing).toBe('/zh-CN/cloud/pricing')
expect(routes.cloudEnterprise).toBe('/zh-CN/cloud/enterprise')
expect(routes.api).toBe('/zh-CN/api')
expect(routes.gallery).toBe('/zh-CN/gallery')
expect(routes.about).toBe('/zh-CN/about')
expect(routes.careers).toBe('/zh-CN/careers')
expect(routes.customers).toBe('/zh-CN/customers')
expect(routes.demos).toBe('/zh-CN/demos')
expect(routes.privacyPolicy).toBe('/zh-CN/privacy-policy')
expect(routes.contact).toBe('/zh-CN/contact')
expect(routes.models).toBe('/zh-CN/p/supported-models')
expect(routes.download).toBe('/zh-CN/download')
})
it('every route value in a non-en locale starts with the locale prefix', () => {
const routes = getRoutes('zh-CN')
for (const [key, value] of Object.entries(routes)) {
expect(value, `route "${key}" should start with /zh-CN`).toMatch(
/^\/zh-CN\//
)
}
})
it('en locale routes are the same reference as baseRoutes (identity check)', () => {
const a = getRoutes('en')
const b = getRoutes('en')
// Both calls return the same base object
expect(a).toBe(b)
})
it('non-en locale routes are new objects (not the base reference)', () => {
const en = getRoutes('en')
const zhCN = getRoutes('zh-CN')
expect(zhCN).not.toBe(en)
})
})
describe('externalLinks', () => {
it('does not contain cloudStatus (removed in this PR)', () => {
expect('cloudStatus' in externalLinks).toBe(false)
})
it('contains expected external link keys', () => {
expect(externalLinks.cloud).toBe('https://cloud.comfy.org')
expect(externalLinks.discord).toMatch(/discord\.com/)
expect(externalLinks.docs).toMatch(/docs\.comfy\.org/)
expect(externalLinks.blog).toMatch(/blog\.comfy\.org/)
expect(externalLinks.github).toMatch(/github\.com\/Comfy-Org/)
expect(externalLinks.platform).toMatch(/platform\.comfy\.org/)
expect(externalLinks.support).toMatch(/support\.comfy\.org/)
expect(externalLinks.apiKeys).toMatch(/platform\.comfy\.org/)
expect(externalLinks.youtube).toMatch(/youtube\.com/)
})
it('all external link values are valid https URLs', () => {
for (const [key, url] of Object.entries(externalLinks)) {
expect(url, `externalLinks.${key} should start with https://`).toMatch(
/^https:\/\//
)
}
})
})

View File

@@ -0,0 +1,98 @@
import { describe, expect, it } from 'vitest'
import type { Department, Role, RolesSnapshot } from './roles'
describe('Role interface (applyUrl rename)', () => {
it('accepts an object with applyUrl as a valid Role', () => {
const role: Role = {
id: 'abc-123',
title: 'Senior Software Engineer',
department: 'Engineering',
location: 'San Francisco',
applyUrl: 'https://jobs.ashbyhq.com/comfy-org/abc-123/application'
}
expect(role.applyUrl).toBe(
'https://jobs.ashbyhq.com/comfy-org/abc-123/application'
)
// jobUrl must not exist on a valid Role object
expect('jobUrl' in role).toBe(false)
})
it('Role object has exactly the expected keys', () => {
const role: Role = {
id: 'xyz',
title: 'Designer',
department: 'Design',
location: 'Remote',
applyUrl: 'https://jobs.ashbyhq.com/comfy-org/xyz/application'
}
const keys = Object.keys(role).sort()
expect(keys).toEqual(['applyUrl', 'department', 'id', 'location', 'title'])
})
it('Department contains roles with applyUrl', () => {
const dept: Department = {
name: 'ENGINEERING',
key: 'engineering',
roles: [
{
id: 'role-1',
title: 'Software Engineer',
department: 'Engineering',
location: 'San Francisco',
applyUrl: 'https://jobs.ashbyhq.com/comfy-org/role-1/application'
}
]
}
expect(dept.roles[0]?.applyUrl).toMatch(
/^https:\/\/jobs\.ashbyhq\.com\//
)
expect('jobUrl' in (dept.roles[0] ?? {})).toBe(false)
})
it('RolesSnapshot has fetchedAt and departments with applyUrl roles', () => {
const snapshot: RolesSnapshot = {
fetchedAt: '2026-05-20T00:00:00.000Z',
departments: [
{
name: 'DESIGN',
key: 'design',
roles: [
{
id: 'designer-1',
title: 'Senior Product Designer',
department: 'Design',
location: 'San Francisco',
applyUrl:
'https://jobs.ashbyhq.com/comfy-org/designer-1/application'
}
]
}
]
}
expect(snapshot.fetchedAt).toBe('2026-05-20T00:00:00.000Z')
expect(snapshot.departments).toHaveLength(1)
const role = snapshot.departments[0]?.roles[0]
expect(role?.applyUrl).toMatch(/\/application$/)
})
it('applyUrl can point to an /application path (regression: was jobUrl without /application)', () => {
// Previously roles used jobUrl which linked to the job description page.
// Now applyUrl links to the application form (ending in /application).
const role: Role = {
id: 'r1',
title: 'Growth Engineer',
department: 'Engineering',
location: 'San Francisco',
applyUrl:
'https://jobs.ashbyhq.com/comfy-org/f1fdde76-84ae-48c1-b0f9-9654dd8e7de5/application'
}
expect(role.applyUrl).toContain('/application')
expect(role.applyUrl).toMatch(/^https:\/\/jobs\.ashbyhq\.com\//)
})
})

View File

@@ -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 },

View File

@@ -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 }
}))

View File

@@ -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 }
}))

View File

@@ -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 }
}))

View File

@@ -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 },

View File

@@ -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 }
}))

View File

@@ -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 }
}))

View File

@@ -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)

View File

@@ -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 }

View File

@@ -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',

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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',

View File

@@ -130,12 +130,10 @@ export const sharedWorkflowImportFixture = base.extend<{
async function mockSharedWorkflowImportFlow(
page: Page
): Promise<SharedWorkflowImportMocks> {
function noopResolveResponse() {}
let isRecording = false
let importEndpointCalled = false
let importBody: ImportPublishedAssetsRequest | undefined
let resolvePublicInclusiveInputAssetResponseAfterImport: () => void =
noopResolveResponse
let resolvePublicInclusiveInputAssetResponseAfterImport: () => void = () => {}
let publicInclusiveInputAssetResponseAfterImport = new Promise<void>(
(resolve) => {
resolvePublicInclusiveInputAssetResponseAfterImport = resolve

View File

@@ -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

View File

@@ -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']
}

View File

@@ -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]

View File

@@ -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 = [
{

View File

@@ -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)

View File

@@ -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')

View File

@@ -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)

View File

@@ -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')
}

View File

@@ -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)

View File

@@ -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)
})

View File

@@ -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)

View File

@@ -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'

View File

@@ -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(

View File

@@ -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

View File

@@ -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
}

View File

@@ -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

View File

@@ -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()

View File

@@ -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()

View File

@@ -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), {

View File

@@ -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)
)
})

View File

@@ -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()

View File

@@ -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)

View File

@@ -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')

View File

@@ -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 }) => {

View File

@@ -19,9 +19,8 @@ test.describe(
await comfyPage.nextFrame()
})
function openMoreOptions(comfyPage: ComfyPage) {
return openMoreOptionsMenu(comfyPage, 'KSampler')
}
const openMoreOptions = (comfyPage: ComfyPage) =>
openMoreOptionsMenu(comfyPage, 'KSampler')
test('hides Node Info from More Options menu when the new menu is disabled', async ({
comfyPage

View File

@@ -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 () => {

View File

@@ -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
)

View File

@@ -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()

View File

@@ -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`

View File

@@ -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

View File

@@ -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)

View File

@@ -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',

View File

@@ -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]

View File

@@ -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')
}

View File

@@ -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')

View File

@@ -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')

View File

@@ -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)

View File

@@ -56,8 +56,8 @@ test.describe('Combo text widget', { tag: ['@screenshot', '@widget'] }, () => {
test('should refresh combo values of optional inputs', async ({
comfyPage
}) => {
async function getComboValues() {
return comfyPage.page.evaluate(() => {
const getComboValues = async () =>
comfyPage.page.evaluate(() => {
return window
.app!.graph!.nodes.find(
(node) => node.title === 'Node With Optional Combo Input'
@@ -65,7 +65,6 @@ test.describe('Combo text widget', { tag: ['@screenshot', '@widget'] }, () => {
.widgets!.find((widget) => widget.name === 'optional_combo_input')!
.options.values
})
}
await comfyPage.workflow.loadWorkflow('inputs/optional_combo_input')
const initialComboValues = await getComboValues()
@@ -83,8 +82,8 @@ test.describe('Combo text widget', { tag: ['@screenshot', '@widget'] }, () => {
test('Should refresh combo values of nodes with v2 combo input spec', async ({
comfyPage
}) => {
async function getComboValues() {
return comfyPage.page.evaluate(() => {
const getComboValues = async () =>
comfyPage.page.evaluate(() => {
return window
.app!.graph!.nodes.find(
(node) => node.title === 'Node With V2 Combo Input'
@@ -92,7 +91,6 @@ test.describe('Combo text widget', { tag: ['@screenshot', '@widget'] }, () => {
.widgets!.find((widget) => widget.name === 'combo_input')!.options
.values
})
}
await comfyPage.workflow.loadWorkflow('inputs/node_with_v2_combo_input')
// click canvas to focus

View File

@@ -213,11 +213,10 @@ test.describe('Workflow Persistence', () => {
.poll(() => comfyPage.nodeOps.getNodeCount())
.toBeGreaterThanOrEqual(2)
function getNodeTypes() {
return comfyPage.page.evaluate(() =>
const getNodeTypes = () =>
comfyPage.page.evaluate(() =>
window.app!.graph.nodes.map((n: { type: string }) => n.type)
)
}
await expect.poll(getNodeTypes).toContain('KSampler')
await expect.poll(getNodeTypes).toContain('EmptyLatentImage')
await expect
@@ -553,12 +552,11 @@ test.describe('Workflow Persistence', () => {
await comfyPage.setup({ clearStorage: false })
await comfyPage.nextFrame()
function getSplitterSizes() {
return comfyPage.page.evaluate(() => {
const getSplitterSizes = () =>
comfyPage.page.evaluate(() => {
const raw = localStorage.getItem('Comfy.Splitter.MainSplitter')
return raw ? (JSON.parse(raw) as number[]) : null
})
}
await expect
.poll(async () => {

View File

@@ -3,11 +3,9 @@ import path from 'path'
type PathParts = readonly [string, ...string[]]
function getBackupPath(originalPath: string): string {
return `${originalPath}.bak`
}
const getBackupPath = (originalPath: string): string => `${originalPath}.bak`
function resolvePathIfExists(pathParts: PathParts): string | null {
const resolvePathIfExists = (pathParts: PathParts): string | null => {
const resolvedPath = path.resolve(...pathParts)
if (!fs.pathExistsSync(resolvedPath)) {
console.warn(`Path not found: ${resolvedPath}`)
@@ -16,7 +14,7 @@ function resolvePathIfExists(pathParts: PathParts): string | null {
return resolvedPath
}
function createScaffoldingCopy(srcDir: string, destDir: string) {
const createScaffoldingCopy = (srcDir: string, destDir: string) => {
// Get all items (files and directories) in the source directory
const items = fs.readdirSync(srcDir, { withFileTypes: true })

View File

@@ -60,7 +60,6 @@
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "catalog:",
"@comfyorg/design-system": "workspace:*",
"@comfyorg/fbx-exporter-three": "^1.0.1",
"@comfyorg/object-info-parser": "workspace:*",
"@comfyorg/registry-types": "workspace:*",
"@comfyorg/shared-frontend-utils": "workspace:*",

View File

@@ -195,7 +195,7 @@ export function processDynamicPrompt(input: string): string {
let result = ''
input = stripComments(input)
function handleEscape() {
const handleEscape = () => {
const nextChar = input[i++]
return '\\' + nextChar
}
@@ -347,7 +347,7 @@ export function formatDate(text: string, date: Date) {
* Generate a cache key from parameters
* Sorts the parameters to ensure consistent keys regardless of parameter order
*/
export function paramsToCacheKey(params: unknown): string {
export const paramsToCacheKey = (params: unknown): string => {
if (typeof params === 'string') return params
if (typeof params === 'object' && params !== null)
return Object.keys(params)
@@ -362,7 +362,7 @@ export function paramsToCacheKey(params: unknown): string {
* Generates a RFC4122 compliant UUID v4 using the native crypto API when available
* @returns A properly formatted UUID string
*/
export function generateUUID(): string {
export const generateUUID = (): string => {
// Use native crypto.randomUUID() if available (modern browsers)
if (
typeof crypto !== 'undefined' &&
@@ -379,21 +379,18 @@ export function generateUUID(): string {
})
}
function isCivitaiHost(hostname: string): boolean {
return (
hostname === 'civitai.com' ||
hostname.endsWith('.civitai.com') ||
hostname === 'civitai.red' ||
hostname.endsWith('.civitai.red')
)
}
const isCivitaiHost = (hostname: string): boolean =>
hostname === 'civitai.com' ||
hostname.endsWith('.civitai.com') ||
hostname === 'civitai.red' ||
hostname.endsWith('.civitai.red')
/**
* Checks if a URL belongs to any Civitai domain (civitai.com or civitai.red).
* Use this for source-name detection; use `isCivitaiModelUrl` when the URL
* must also match a specific model API path format.
*/
export function isCivitaiUrl(url: string): boolean {
export const isCivitaiUrl = (url: string): boolean => {
if (!isValidUrl(url)) return false
return isCivitaiHost(new URL(url).hostname.toLowerCase())
}
@@ -406,7 +403,7 @@ export function isCivitaiUrl(url: string): boolean {
* isCivitaiModelUrl('https://civitai.com/api/v1/models-versions/15342') // true
* isCivitaiModelUrl('https://example.com/model.safetensors') // false
*/
export function isCivitaiModelUrl(url: string): boolean {
export const isCivitaiModelUrl = (url: string): boolean => {
if (!isValidUrl(url)) return false
const urlObj = new URL(url)
@@ -429,7 +426,7 @@ export function isCivitaiModelUrl(url: string): boolean {
* 'https://huggingface.co/bfl/FLUX.1/resolve/main/flux1-canny-dev.safetensors?download=true'
* ) // https://huggingface.co/bfl/FLUX.1
*/
export function downloadUrlToHfRepoUrl(url: string): string {
export const downloadUrlToHfRepoUrl = (url: string): string => {
try {
const urlObj = new URL(url)
const pathname = urlObj.pathname

View File

@@ -1,7 +1,7 @@
import axios from 'axios'
const VALID_STATUS_CODES = [200, 201, 301, 302, 307, 308]
export async function checkUrlReachable(url: string): Promise<boolean> {
export const checkUrlReachable = async (url: string): Promise<boolean> => {
try {
const response = await axios.head(url)
// Additional check for successful response

45
pnpm-lock.yaml generated
View File

@@ -437,9 +437,6 @@ importers:
'@comfyorg/design-system':
specifier: workspace:*
version: link:packages/design-system
'@comfyorg/fbx-exporter-three':
specifier: ^1.0.1
version: 1.0.1(@types/three@0.170.0)(three@0.170.0)
'@comfyorg/object-info-parser':
specifier: workspace:*
version: link:packages/object-info-parser
@@ -1793,16 +1790,6 @@ packages:
'@comfyorg/comfyui-electron-types@0.6.2':
resolution: {integrity: sha512-r3By5Wbizq8jagUrhtcym79HYUTinsvoBnYkFFWbUmrURBWIaC0HduFVkRkI1PNdI76piW+JSOJJnw00YCVXeg==}
'@comfyorg/fbx-exporter-three@1.0.1':
resolution: {integrity: sha512-fQ1zBsgmmwfio6iEi91hRiFCr946yEgqR2DGh/UMismaLyUohiKGOJL/OnJQnW3+yne/PXxVoYgcortyumsO5w==}
engines: {node: '>=18'}
peerDependencies:
'@types/three': '>=0.160.0'
three: '>=0.160.0'
peerDependenciesMeta:
'@types/three':
optional: true
'@csstools/color-helpers@5.1.0':
resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==}
engines: {node: '>=18'}
@@ -4671,7 +4658,6 @@ packages:
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
deprecated: Potential CWE-502 - Update to 1.3.1 or higher
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==}
@@ -9884,8 +9870,8 @@ packages:
vue-component-type-helpers@3.2.6:
resolution: {integrity: sha512-O02tnvIfOQVmnvoWwuSydwRoHjZVt8UEBR+2p4rT35p8GAy5VTlWP8o5qXfJR/GWCN0nVZoYWsVUvx2jwgdBmQ==}
vue-component-type-helpers@3.3.0:
resolution: {integrity: sha512-vwR8DDsBysI9NWXa0okPFpCcW+BUC3sPTuLBNo1faMzw4QWMFd+3/lFYFu29ZN0q+8UReXWJHEYesC9dcXYCLg==}
vue-component-type-helpers@3.2.9:
resolution: {integrity: sha512-S3BiWYaLSzHxTpln665ELSrMR9UYmrIDUmhik7nVZxmJjTKL2/a+ew1hvGxksKelivm0ujjWfG1fYOiU/2e8rA==}
vue-demi@0.14.10:
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
@@ -10495,7 +10481,7 @@ snapshots:
'@astrojs/yaml2ts@0.2.3':
dependencies:
yaml: 2.9.0
yaml: 2.8.2
'@atlaskit/pragmatic-drag-and-drop@1.3.1':
dependencies:
@@ -11242,13 +11228,6 @@ snapshots:
'@comfyorg/comfyui-electron-types@0.6.2': {}
'@comfyorg/fbx-exporter-three@1.0.1(@types/three@0.170.0)(three@0.170.0)':
dependencies:
fflate: 0.8.2
three: 0.170.0
optionalDependencies:
'@types/three': 0.170.0
'@csstools/color-helpers@5.1.0': {}
'@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
@@ -13418,7 +13397,7 @@ snapshots:
storybook: 10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
type-fest: 2.19.0
vue: 3.5.13(typescript@5.9.3)
vue-component-type-helpers: 3.3.0
vue-component-type-helpers: 3.2.9
'@swc/helpers@0.5.17':
dependencies:
@@ -13972,7 +13951,7 @@ snapshots:
'@typescript-eslint/visitor-keys': 8.56.0
debug: 4.4.3
minimatch: 9.0.5
semver: 7.8.0
semver: 7.7.4
tinyglobby: 0.2.15
ts-api-utils: 2.4.0(typescript@5.9.3)
typescript: 5.9.3
@@ -14861,7 +14840,7 @@ snapshots:
picomatch: 4.0.3
prompts: 2.4.2
rehype: 13.0.2
semver: 7.8.0
semver: 7.7.4
shiki: 3.23.0
smol-toml: 1.6.1
svgo: 4.0.0
@@ -15681,7 +15660,7 @@ snapshots:
'@one-ini/wasm': 0.1.1
commander: 10.0.1
minimatch: 9.0.1
semver: 7.8.0
semver: 7.7.4
eight-colors@1.3.3: {}
@@ -18334,7 +18313,7 @@ snapshots:
ky: 1.14.3
registry-auth-token: 5.1.1
registry-url: 6.0.1
semver: 7.8.0
semver: 7.7.4
package-manager-detector@1.6.0: {}
@@ -19228,7 +19207,7 @@ snapshots:
dependencies:
'@img/colour': 1.1.0
detect-libc: 2.1.2
semver: 7.8.0
semver: 7.7.4
optionalDependencies:
'@img/sharp-darwin-arm64': 0.34.5
'@img/sharp-darwin-x64': 0.34.5
@@ -19829,7 +19808,7 @@ snapshots:
typescript-auto-import-cache@0.3.6:
dependencies:
semver: 7.8.0
semver: 7.7.4
typescript-eslint@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3):
dependencies:
@@ -20460,7 +20439,7 @@ snapshots:
volar-service-typescript@0.0.70(@volar/language-service@2.4.28):
dependencies:
path-browserify: 1.0.1
semver: 7.8.0
semver: 7.7.4
typescript-auto-import-cache: 0.3.6
vscode-languageserver-textdocument: 1.0.12
vscode-nls: 5.2.0
@@ -20529,7 +20508,7 @@ snapshots:
vue-component-type-helpers@3.2.6: {}
vue-component-type-helpers@3.3.0: {}
vue-component-type-helpers@3.2.9: {}
vue-demi@0.14.10(vue@3.5.13(typescript@5.9.3)):
dependencies:

View File

@@ -103,9 +103,8 @@ function shouldIgnoreKey(key: string): boolean {
// Search for key usage in source files
function isKeyUsed(key: string, sourceFiles: string[]): boolean {
// Escape special regex characters
function escapeRegex(str: string) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
const escapeRegex = (str: string) =>
str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const escapedKey = escapeRegex(key)
const lastPart = key.split('.').pop()
const escapedLastPart = lastPart ? escapeRegex(lastPart) : ''

View File

@@ -18,7 +18,7 @@ const localePath = './src/locales/en/main.json'
const commandsPath = './src/locales/en/commands.json'
const settingsPath = './src/locales/en/settings.json'
function extractMenuCommandLocaleStrings(): Set<string> {
const extractMenuCommandLocaleStrings = (): Set<string> => {
const labels = new Set<string>()
for (const [category, _] of CORE_MENU_COMMANDS) {
category.forEach((category) => labels.add(category))

View File

@@ -93,9 +93,7 @@ async function buildBundleReport() {
* @param {string[]} files
* @returns {string[]}
*/
function filterFiles(files) {
return files.filter((file) => file.endsWith('.json'))
}
const filterFiles = (files) => files.filter((file) => file.endsWith('.json'))
const currFiles = filterFiles(await readdir(currDir))
const baselineFiles = existsSync(prevDir)

Some files were not shown because too many files have changed in this diff Show More