Compare commits

..

1 Commits

Author SHA1 Message Date
coderabbitai[bot]
9632e6dea0 CodeRabbit Generated Unit Tests: Add unit tests 2026-06-17 17:16:24 +00:00
10 changed files with 239 additions and 202 deletions

View File

@@ -1,55 +0,0 @@
# Description: Team-gated multi-model Cursor review — a thin caller for the
# reusable workflow in Comfy-Org/github-workflows, which is the single source of
# truth for the panel, judge, prompts, and scripts. Triggered by the
# 'cursor-review' label.
#
# Access control (team-only, two layers):
# 1. Only users with triage permission or higher can apply a label in a public
# repo, so the public cannot trigger this.
# 2. The reusable workflow's secret-bearing jobs do not run on fork PRs (forks
# get no secrets), so CURSOR_API_KEY is reachable only on internal branches.
name: 'PR: Cursor Review'
on:
pull_request:
types: [labeled, unlabeled]
permissions:
contents: read
pull-requests: write
concurrency:
# Re-labeling cancels an in-flight run for the same PR + label.
group: cursor-review-pr-${{ github.event.pull_request.number }}-${{ github.event.label.name }}
cancel-in-progress: true
jobs:
cursor-review:
if: github.event.action == 'labeled' && github.event.label.name == 'cursor-review'
# SHA-pinned per zizmor `unpinned-uses: hash-pin`. Bump this SHA to pick up
# upstream changes; keep `workflows_ref` matching so prompts/scripts load
# from the same commit as the workflow definition.
uses: Comfy-Org/github-workflows/.github/workflows/cursor-review.yml@047ca48febe3a6647608ed2e0c4331b491cb9d6a # github-workflows#9
with:
# Overriding diff_excludes replaces the reusable default wholesale, so
# this restates the generated/vendored defaults and adds this repo's heavy
# paths (Playwright snapshots, generated manager types).
diff_excludes: >-
:!**/package-lock.json
:!**/yarn.lock
:!**/pnpm-lock.yaml
:!**/node_modules/**
:!**/.claude/**
:!**/dist/**
:!**/vendor/**
:!**/*.generated.*
:!**/*.min.js
:!**/*.min.css
:!**/*-snapshots/**
:!src/workbench/extensions/manager/types/generatedManagerTypes.ts
# Load the prompts/scripts from the same ref as `uses:`.
workflows_ref: 047ca48febe3a6647608ed2e0c4331b491cb9d6a
secrets:
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
# Optional — enables start/complete Slack DMs to the triggerer.
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}

View File

@@ -1385,9 +1385,9 @@ const translations = {
'zh-CN': '随时加购积分'
},
'pricing.included.feature5.description': {
en: 'Purchase additional credits at any time. Top-up credits are valid for 1 year from the date of purchase and do not roll over with your monthly plan.',
en: 'Purchase additional credits at any time. Unused top-ups roll over to the next month automatically for up to 1 year.',
'zh-CN':
'可随时购买额外积分。充值积分自购买之日起 1 年内有效,且不会随月度计划结转。'
'可随时购买额外积分。未使用的充值积分自动结转至下月,最长保留 1 年。'
},
'pricing.included.feature6.title': {
en: 'Pre-installed models',

View File

@@ -1,45 +0,0 @@
import type { Page } from '@playwright/test'
function flagAttributeFor(testId: string) {
const encoded = Array.from(testId, (ch) =>
ch.charCodeAt(0).toString(16)
).join('')
return `data-flashed-${encoded}`
}
/**
* Flags the first time an element matching `[data-testid="<testId>"]` is
* present and rendered, sampled every frame via `requestAnimationFrame` from
* page load. Catches a dialog that mounts and unmounts within a few frames,
* which `toBeHidden()` (final state only) cannot.
*
* Must be called before navigation (e.g. before `comfyPage.setup()`).
*/
export async function trackElementFlash(
page: Page,
testId: string
): Promise<{ hasFlashed: () => Promise<boolean> }> {
const flagAttribute = flagAttributeFor(testId)
await page.addInitScript(
({ id, attribute }: { id: string; attribute: string }) => {
const sample = () => {
const el = document.querySelector(`[data-testid="${CSS.escape(id)}"]`)
if (el instanceof HTMLElement) {
const rect = el.getBoundingClientRect()
if (rect.width > 0 && rect.height > 0) {
document.documentElement.setAttribute(attribute, 'true')
}
}
requestAnimationFrame(sample)
}
requestAnimationFrame(sample)
},
{ id: testId, attribute: flagAttribute }
)
return {
hasFlashed: async () =>
(await page.locator('html').getAttribute(flagAttribute)) === 'true'
}
}

View File

@@ -15,10 +15,6 @@ import { createMixedMediaJobs } from '@e2e/fixtures/helpers/AssetsHelper'
// fixtures — Playwright runs auto fixtures before the `comfyPage` fixture's
// internal `setup()`, so the page first-loads with mocks already in place.
// See cloud-asset-default.spec.ts for the same pattern.
//
// Use `waitForAssets()` not `waitForAssets(MIXED_JOBS.length)`: VirtualGrid can
// virtualize the 3D card out of the initial render (#11635). Filtering reads the
// full store, so the per-filter count assertions still cover the behavior.
const MIXED_JOBS = createMixedMediaJobs(['images', 'video', 'audio', '3D'])
@@ -117,7 +113,7 @@ test.describe('Assets sidebar - media type filter', { tag: '@cloud' }, () => {
}) => {
const tab = comfyPage.menu.assetsTab
await tab.open()
await tab.waitForAssets()
await tab.waitForAssets(MIXED_JOBS.length)
await tab.openFilterMenu()
@@ -140,7 +136,7 @@ test.describe('Assets sidebar - media type filter', { tag: '@cloud' }, () => {
}) => {
const tab = comfyPage.menu.assetsTab
await tab.open()
await tab.waitForAssets()
await tab.waitForAssets(MIXED_JOBS.length)
await tab.openFilterMenu()
await tab.toggleMediaTypeFilter('image')
@@ -157,7 +153,7 @@ test.describe('Assets sidebar - media type filter', { tag: '@cloud' }, () => {
}) => {
const tab = comfyPage.menu.assetsTab
await tab.open()
await tab.waitForAssets()
await tab.waitForAssets(MIXED_JOBS.length)
await tab.openFilterMenu()
await tab.toggleMediaTypeFilter('video')
@@ -171,7 +167,7 @@ test.describe('Assets sidebar - media type filter', { tag: '@cloud' }, () => {
}) => {
const tab = comfyPage.menu.assetsTab
await tab.open()
await tab.waitForAssets()
await tab.waitForAssets(MIXED_JOBS.length)
await tab.openFilterMenu()
await tab.toggleMediaTypeFilter('audio')
@@ -183,7 +179,7 @@ test.describe('Assets sidebar - media type filter', { tag: '@cloud' }, () => {
test('Selecting only "3D" hides non-3D assets', async ({ comfyPage }) => {
const tab = comfyPage.menu.assetsTab
await tab.open()
await tab.waitForAssets()
await tab.waitForAssets(MIXED_JOBS.length)
await tab.openFilterMenu()
await tab.toggleMediaTypeFilter('3d')
@@ -197,7 +193,7 @@ test.describe('Assets sidebar - media type filter', { tag: '@cloud' }, () => {
}) => {
const tab = comfyPage.menu.assetsTab
await tab.open()
await tab.waitForAssets()
await tab.waitForAssets(MIXED_JOBS.length)
await tab.openFilterMenu()
await tab.toggleMediaTypeFilter('image')
@@ -215,7 +211,7 @@ test.describe('Assets sidebar - media type filter', { tag: '@cloud' }, () => {
}) => {
const tab = comfyPage.menu.assetsTab
await tab.open()
await tab.waitForAssets()
await tab.waitForAssets(MIXED_JOBS.length)
await tab.openFilterMenu()
await tab.toggleMediaTypeFilter('image')

View File

@@ -5,7 +5,6 @@ import type { WorkflowTemplates } from '@/platform/workflow/templates/types/temp
import { getWav } from '@e2e/fixtures/components/AudioPreview'
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
import { TestIds } from '@e2e/fixtures/selectors'
import { trackElementFlash } from '@e2e/fixtures/utils/flashDetector'
async function checkTemplateFileExists(
page: Page,
@@ -506,32 +505,3 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => {
expect(popup.url()).toEqual(tutorialUrl)
})
})
test.describe(
'Templates deeplink (new user)',
{ tag: ['@slow', '@workflow'] },
() => {
test('templates dialog never flashes when first-time user opens a template link', async ({
comfyPage
}) => {
const templatesFlash = await trackElementFlash(
comfyPage.page,
TestIds.templates.content
)
await comfyPage.settings.setSetting('Comfy.TutorialCompleted', false)
await comfyPage.setup({
clearStorage: true,
url: '/?template=default'
})
await expect
.poll(() => comfyPage.nodeOps.getGraphNodesCount())
.toBeGreaterThan(0)
expect(await templatesFlash.hasFlashed()).toBe(false)
await expect(comfyPage.templates.content).toBeHidden()
})
}
)

View File

@@ -4,14 +4,3 @@ comment:
require_changes: false
require_base: false
require_head: true
# Carry forward the last known coverage for a flag when its upload is missing or
# late. The `e2e` flag is uploaded by a separate workflow_run job that can fail
# or arrive after Codecov has already computed the patch status; without this,
# E2E-only code paths show up as patch misses and the patch status fails. See
# https://docs.codecov.com/docs/carryforward-flags
flags:
unit:
carryforward: true
e2e:
carryforward: true

View File

@@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick, ref } from 'vue'
import type { IFuseOptions } from 'fuse.js'
import type { Distribution } from '@/platform/distribution/types'
import type { TemplateInfo } from '@/platform/workflow/templates/types/template'
import { TemplateIncludeOnDistributionEnum } from '@/platform/workflow/templates/types/template'
import { useTemplateFiltering } from '@/composables/useTemplateFiltering'
@@ -436,7 +437,7 @@ describe('useTemplateFiltering', () => {
})
describe('Distribution filtering', () => {
const setDistribution = (distribution: 'desktop' | 'localhost' | 'cloud') =>
const setDistribution = (distribution: Distribution) =>
vi.stubGlobal('__DISTRIBUTION__', distribution)
const cloudTemplate: TemplateInfo = {
@@ -457,6 +458,14 @@ describe('useTemplateFiltering', () => {
includeOnDistributions: [TemplateIncludeOnDistributionEnum.Desktop]
}
const localTemplate: TemplateInfo = {
name: 'local-only',
description: 'Local template',
mediaType: 'image',
mediaSubtype: 'png',
includeOnDistributions: [TemplateIncludeOnDistributionEnum.Local]
}
const universalTemplate: TemplateInfo = {
name: 'universal',
description: 'Universal template',
@@ -540,14 +549,6 @@ describe('useTemplateFiltering', () => {
it('shows local templates on localhost distribution', () => {
setDistribution('localhost')
const localTemplate: TemplateInfo = {
name: 'local-only',
description: 'Local template',
mediaType: 'image',
mediaSubtype: 'png',
includeOnDistributions: [TemplateIncludeOnDistributionEnum.Local]
}
const templates = ref([localTemplate, cloudTemplate, desktopTemplate])
const { filteredTemplates, filteredCount, totalCount } =
@@ -558,6 +559,219 @@ describe('useTemplateFiltering', () => {
expect(filteredTemplates.value[0].name).toBe('local-only')
})
it('shows local templates on desktop2 distribution', () => {
setDistribution('desktop2')
const templates = ref([localTemplate, desktopTemplate, cloudTemplate])
const { filteredTemplates, filteredCount, totalCount } =
useTemplateFiltering(templates)
expect(filteredCount.value).toBe(1)
expect(totalCount.value).toBe(1)
expect(filteredTemplates.value[0].name).toBe('local-only')
})
it('excludes cloud-only templates on desktop2 distribution', () => {
setDistribution('desktop2')
const templates = ref([cloudTemplate, universalTemplate])
const { filteredTemplates, filteredCount, totalCount } =
useTemplateFiltering(templates)
expect(filteredCount.value).toBe(1)
expect(totalCount.value).toBe(1)
expect(filteredTemplates.value[0].name).toBe('universal')
})
it('excludes desktop-only templates on desktop2 distribution', () => {
setDistribution('desktop2')
const templates = ref([desktopTemplate, universalTemplate])
const { filteredTemplates, filteredCount } = useTemplateFiltering(templates)
expect(filteredCount.value).toBe(1)
expect(filteredTemplates.value[0].name).toBe('universal')
})
it('shows universal templates (no distribution constraint) on desktop2', () => {
setDistribution('desktop2')
const templates = ref([universalTemplate])
const { filteredCount, totalCount } = useTemplateFiltering(templates)
expect(filteredCount.value).toBe(1)
expect(totalCount.value).toBe(1)
})
it('desktop2 and localhost produce identical template filtering results', () => {
const templates = ref([
localTemplate,
cloudTemplate,
desktopTemplate,
universalTemplate
])
setDistribution('desktop2')
const { filteredTemplates: desktop2Filtered } =
useTemplateFiltering(templates)
setDistribution('localhost')
const { filteredTemplates: localhostFiltered } =
useTemplateFiltering(templates)
expect(desktop2Filtered.value.map((t) => t.name)).toEqual(
localhostFiltered.value.map((t) => t.name)
)
})
it('desktop2 does not include Mac-specific templates', () => {
setDistribution('desktop2')
const macTemplate: TemplateInfo = {
name: 'mac-template',
description: 'Mac only',
mediaType: 'image',
mediaSubtype: 'png',
includeOnDistributions: [TemplateIncludeOnDistributionEnum.Mac]
}
const templates = ref([macTemplate, localTemplate])
const { filteredTemplates, filteredCount } = useTemplateFiltering(templates)
expect(filteredCount.value).toBe(1)
expect(filteredTemplates.value[0].name).toBe('local-only')
})
it('desktop2 does not include Windows-specific templates', () => {
setDistribution('desktop2')
const windowsTemplate: TemplateInfo = {
name: 'windows-template',
description: 'Windows only',
mediaType: 'image',
mediaSubtype: 'png',
includeOnDistributions: [TemplateIncludeOnDistributionEnum.Windows]
}
const templates = ref([windowsTemplate, localTemplate])
const { filteredTemplates, filteredCount } = useTemplateFiltering(templates)
expect(filteredCount.value).toBe(1)
expect(filteredTemplates.value[0].name).toBe('local-only')
})
it('desktop2 distribution composes with model filter', () => {
setDistribution('desktop2')
const localFluxTemplate: TemplateInfo = {
name: 'local-flux',
description: 'Local Flux template',
mediaType: 'image',
mediaSubtype: 'png',
models: ['Flux'],
includeOnDistributions: [TemplateIncludeOnDistributionEnum.Local]
}
const localSDTemplate: TemplateInfo = {
name: 'local-sd',
description: 'Local SD template',
mediaType: 'image',
mediaSubtype: 'png',
models: ['SD 1.5'],
includeOnDistributions: [TemplateIncludeOnDistributionEnum.Local]
}
const cloudFluxTemplate: TemplateInfo = {
name: 'cloud-flux',
description: 'Cloud Flux template',
mediaType: 'image',
mediaSubtype: 'png',
models: ['Flux'],
includeOnDistributions: [TemplateIncludeOnDistributionEnum.Cloud]
}
const templates = ref([localFluxTemplate, localSDTemplate, cloudFluxTemplate])
const { selectedModels, filteredTemplates, filteredCount, totalCount } =
useTemplateFiltering(templates)
expect(totalCount.value).toBe(2)
selectedModels.value = ['Flux']
expect(filteredCount.value).toBe(1)
expect(filteredTemplates.value[0].name).toBe('local-flux')
})
it('desktop2 distribution composes with use case filter', () => {
setDistribution('desktop2')
const localVideoTemplate: TemplateInfo = {
name: 'local-video',
description: 'Local video template',
mediaType: 'video',
mediaSubtype: 'mp4',
tags: ['Video'],
includeOnDistributions: [TemplateIncludeOnDistributionEnum.Local]
}
const localImageTemplate: TemplateInfo = {
name: 'local-image',
description: 'Local image template',
mediaType: 'image',
mediaSubtype: 'png',
tags: ['Image Gen'],
includeOnDistributions: [TemplateIncludeOnDistributionEnum.Local]
}
const cloudVideoTemplate: TemplateInfo = {
name: 'cloud-video',
description: 'Cloud video template',
mediaType: 'video',
mediaSubtype: 'mp4',
tags: ['Video'],
includeOnDistributions: [TemplateIncludeOnDistributionEnum.Cloud]
}
const templates = ref([
localVideoTemplate,
localImageTemplate,
cloudVideoTemplate
])
const { selectedUseCases, filteredTemplates, filteredCount } =
useTemplateFiltering(templates)
selectedUseCases.value = ['Video']
expect(filteredCount.value).toBe(1)
expect(filteredTemplates.value[0].name).toBe('local-video')
})
it('desktop2 shows template with Local in multi-distribution list', () => {
setDistribution('desktop2')
const localAndCloudTemplate: TemplateInfo = {
name: 'local-and-cloud',
description: 'Available on Local and Cloud',
mediaType: 'image',
mediaSubtype: 'png',
includeOnDistributions: [
TemplateIncludeOnDistributionEnum.Local,
TemplateIncludeOnDistributionEnum.Cloud
]
}
const templates = ref([localAndCloudTemplate, desktopTemplate])
const { filteredTemplates, filteredCount } = useTemplateFiltering(templates)
expect(filteredCount.value).toBe(1)
expect(filteredTemplates.value[0].name).toBe('local-and-cloud')
})
it('desktop2 excludes template with only Cloud+Desktop multi-distribution', () => {
setDistribution('desktop2')
const templates = ref([multiDistTemplate])
const { filteredCount } = useTemplateFiltering(templates)
expect(filteredCount.value).toBe(0)
})
it('includes templates with multiple distributions when any match', () => {
setDistribution('cloud')
const templates = ref([multiDistTemplate])

View File

@@ -6,7 +6,7 @@ import type {
} from '@/extensions/core/load3d/interfaces'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
type Load3dSerializedBase = {
export type Load3dSerializedBase = {
camera_info: CameraState | null
model_3d_info: Model3DInfo
}

View File

@@ -591,31 +591,5 @@ describe('useWorkflowPersistenceV2', () => {
'Comfy.BrowseTemplates'
)
})
it('does not open templates browser when template param is in URL', async () => {
routeMocks.query = { template: 'default-template-id' }
const { initializeWorkflow } = mountWorkflowPersistence()
await initializeWorkflow()
expect(loadBlankWorkflowMock).toHaveBeenCalled()
expect(commandStoreMocks.execute).not.toHaveBeenCalledWith(
'Comfy.BrowseTemplates'
)
})
it('does not open templates browser when template intent is preserved across /user-select redirect', async () => {
preservedQueryMocks.payloads.template = {
template: 'default-template-id'
}
const { initializeWorkflow } = mountWorkflowPersistence()
await initializeWorkflow()
expect(loadBlankWorkflowMock).toHaveBeenCalled()
expect(commandStoreMocks.execute).not.toHaveBeenCalledWith(
'Comfy.BrowseTemplates'
)
})
})
})

View File

@@ -161,24 +161,18 @@ export function useWorkflowPersistenceV2() {
})
}
const hasPreservedIntent = (namespace: string, key: string) => {
if (typeof route.query[key] === 'string') return true
hydratePreservedQuery(namespace)
const merged = mergePreservedQueryIntoQuery(namespace, route.query)
return typeof merged?.[key] === 'string'
const hasSharedWorkflowIntent = () => {
if (typeof route.query.share === 'string') return true
hydratePreservedQuery(SHARE_NAMESPACE)
const merged = mergePreservedQueryIntoQuery(SHARE_NAMESPACE, route.query)
return typeof merged?.share === 'string'
}
const hasSharedWorkflowIntent = () =>
hasPreservedIntent(SHARE_NAMESPACE, 'share')
const hasTemplateUrlIntent = () =>
hasPreservedIntent(TEMPLATE_NAMESPACE, 'template')
const loadDefaultWorkflow = async () => {
if (!settingStore.get('Comfy.TutorialCompleted')) {
await settingStore.set('Comfy.TutorialCompleted', true)
await useWorkflowService().loadBlankWorkflow()
if (!hasSharedWorkflowIntent() && !hasTemplateUrlIntent()) {
if (!hasSharedWorkflowIntent()) {
await useCommandStore().execute('Comfy.BrowseTemplates')
}
} else {