mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-27 18:24:11 +00:00
Merge remote-tracking branch 'origin/main' into fix/spacebar-panning-vue-nodes
This commit is contained in:
@@ -83,7 +83,7 @@ test.describe('Templates', () => {
|
||||
|
||||
await comfyPage.page
|
||||
.locator(
|
||||
'nav > div:nth-child(2) > div > span:has-text("Getting Started")'
|
||||
'nav > div:nth-child(3) > div > span:has-text("Getting Started")'
|
||||
)
|
||||
.click()
|
||||
await comfyPage.templates.loadTemplate('default')
|
||||
|
||||
66
docs/TEMPLATE_RANKING.md
Normal file
66
docs/TEMPLATE_RANKING.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Template Ranking System
|
||||
|
||||
Usage-based ordering for workflow templates with position bias normalization.
|
||||
|
||||
Scores are pre-computed and normalized offline and shipped as static JSON (mirrors `sorted-custom-node-map.json` pattern for node search).
|
||||
|
||||
## Sort Modes
|
||||
|
||||
| Mode | Formula | Description |
|
||||
| -------------- | ------------------------------------------------ | ---------------------- |
|
||||
| `recommended` | `usage × 0.5 + internal × 0.3 + freshness × 0.2` | Curated recommendation |
|
||||
| `popular` | `usage × 0.9 + freshness × 0.1` | Pure user-driven |
|
||||
| `newest` | Date sort | Existing |
|
||||
| `alphabetical` | Name sort | Existing |
|
||||
|
||||
Freshness computed at runtime from `template.date`: `1.0 / (1 + daysSinceAdded / 90)`, min 0.1.
|
||||
|
||||
## Data Files
|
||||
|
||||
**Usage scores** (generated from Mixpanel):
|
||||
|
||||
```json
|
||||
// In templates/index.json, add to any template:
|
||||
{
|
||||
"name": "some_template",
|
||||
"usage": 1000,
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**Search rank** (set per-template in workflow_templates repo):
|
||||
|
||||
```json
|
||||
// In templates/index.json, add to any template:
|
||||
{
|
||||
"name": "some_template",
|
||||
"searchRank": 8, // Scale 1-10, default 5
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
| searchRank | Effect |
|
||||
| ---------- | ---------------------------- |
|
||||
| 1-4 | Demote (bury in results) |
|
||||
| 5 | Neutral (default if not set) |
|
||||
| 6-10 | Promote (boost in results) |
|
||||
|
||||
## Position Bias Correction
|
||||
|
||||
Raw usage reflects true preference AND UI position bias. We use linear interpolation:
|
||||
|
||||
```
|
||||
correction = 1 + (position - 1) / (maxPosition - 1)
|
||||
normalizedUsage = rawUsage × correction
|
||||
```
|
||||
|
||||
| Position | Boost |
|
||||
| -------- | ----- |
|
||||
| 1 | 1.0× |
|
||||
| 50 | 1.28× |
|
||||
| 100 | 1.57× |
|
||||
| 175 | 2.0× |
|
||||
|
||||
Templates buried at the bottom get up to 2× boost to compensate for reduced visibility.
|
||||
|
||||
---
|
||||
@@ -12,12 +12,17 @@ Documentation for unit tests is organized into three guides:
|
||||
|
||||
## Testing Structure
|
||||
|
||||
The ComfyUI Frontend project uses a mixed approach to unit test organization:
|
||||
The ComfyUI Frontend project uses **colocated tests** - test files are placed alongside their source files:
|
||||
|
||||
- **Component Tests**: Located directly alongside their components with a `.spec.ts` extension
|
||||
- **Unit Tests**: Located in the `tests-ui/tests/` directory
|
||||
- **Store Tests**: Located in the `tests-ui/tests/store/` directory
|
||||
- **Browser Tests**: These are located in the `browser_tests/` directory. There is a dedicated README in the `browser_tests/` directory, so it will not be covered here.
|
||||
- **Component Tests**: Located directly alongside their components (e.g., `MyComponent.test.ts` next to `MyComponent.vue`)
|
||||
- **Unit Tests**: Located alongside their source files (e.g., `myUtil.test.ts` next to `myUtil.ts`)
|
||||
- **Store Tests**: Located in `src/stores/` alongside their store files
|
||||
- **Browser Tests**: Located in the `browser_tests/` directory (see dedicated README there)
|
||||
|
||||
### Test File Naming
|
||||
|
||||
- Use `.test.ts` extension for test files
|
||||
- Name tests after their source file: `sourceFile.test.ts`
|
||||
|
||||
## Test Frameworks and Libraries
|
||||
|
||||
@@ -35,8 +40,11 @@ To run the tests locally:
|
||||
# Run unit tests
|
||||
pnpm test:unit
|
||||
|
||||
# Run a specific test file
|
||||
pnpm test:unit -- src/path/to/file.test.ts
|
||||
|
||||
# Run unit tests in watch mode
|
||||
pnpm test:unit -- --watch
|
||||
```
|
||||
|
||||
Refer to the specific guides for more detailed information on each testing type.
|
||||
Refer to the specific guides for more detailed information on each testing type.
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { getMediaTypeFromFilename, truncateFilename } from '@/utils/formatUtil'
|
||||
import { getMediaTypeFromFilename, truncateFilename } from './formatUtil'
|
||||
|
||||
describe('formatUtil', () => {
|
||||
describe('truncateFilename', () => {
|
||||
@@ -175,6 +175,7 @@
|
||||
<!-- Actual Template Cards -->
|
||||
<CardContainer
|
||||
v-for="template in isLoading ? [] : displayTemplates"
|
||||
v-show="isTemplateVisibleOnDistribution(template)"
|
||||
:key="template.name"
|
||||
ref="cardRefs"
|
||||
size="compact"
|
||||
@@ -405,6 +406,8 @@ import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useTemplateWorkflows } from '@/platform/workflow/templates/composables/useTemplateWorkflows'
|
||||
import { useWorkflowTemplatesStore } from '@/platform/workflow/templates/repositories/workflowTemplatesStore'
|
||||
import type { TemplateInfo } from '@/platform/workflow/templates/types/template'
|
||||
import { TemplateIncludeOnDistributionEnum } from '@/platform/workflow/templates/types/template'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
import type { NavGroupData, NavItemData } from '@/types/navTypes'
|
||||
import { OnCloseKey } from '@/types/widgetTypes'
|
||||
import { createGridStyle } from '@/utils/gridUtil'
|
||||
@@ -423,6 +426,30 @@ onMounted(() => {
|
||||
sessionStartTime.value = Date.now()
|
||||
})
|
||||
|
||||
const systemStatsStore = useSystemStatsStore()
|
||||
|
||||
const distributions = computed(() => {
|
||||
// eslint-disable-next-line no-undef
|
||||
switch (__DISTRIBUTION__) {
|
||||
case 'cloud':
|
||||
return [TemplateIncludeOnDistributionEnum.Cloud]
|
||||
case 'localhost':
|
||||
return [TemplateIncludeOnDistributionEnum.Local]
|
||||
case 'desktop':
|
||||
default:
|
||||
if (systemStatsStore.systemStats?.system.os === 'darwin') {
|
||||
return [
|
||||
TemplateIncludeOnDistributionEnum.Desktop,
|
||||
TemplateIncludeOnDistributionEnum.Mac
|
||||
]
|
||||
}
|
||||
return [
|
||||
TemplateIncludeOnDistributionEnum.Desktop,
|
||||
TemplateIncludeOnDistributionEnum.Windows
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// Wrap onClose to track session end
|
||||
const onClose = () => {
|
||||
if (isCloud) {
|
||||
@@ -511,6 +538,9 @@ const allTemplates = computed(() => {
|
||||
return workflowTemplatesStore.enhancedTemplates
|
||||
})
|
||||
|
||||
// Navigation
|
||||
const selectedNavItem = ref<string | null>('all')
|
||||
|
||||
// Filter templates based on selected navigation item
|
||||
const navigationFilteredTemplates = computed(() => {
|
||||
if (!selectedNavItem.value) {
|
||||
@@ -536,6 +566,36 @@ const {
|
||||
resetFilters
|
||||
} = useTemplateFiltering(navigationFilteredTemplates)
|
||||
|
||||
/**
|
||||
* Coordinates state between the selected navigation item and the sort order to
|
||||
* create deterministic, predictable behavior.
|
||||
* @param source The origin of the change ('nav' or 'sort').
|
||||
*/
|
||||
const coordinateNavAndSort = (source: 'nav' | 'sort') => {
|
||||
const isPopularNav = selectedNavItem.value === 'popular'
|
||||
const isPopularSort = sortBy.value === 'popular'
|
||||
|
||||
if (source === 'nav') {
|
||||
if (isPopularNav && !isPopularSort) {
|
||||
// When navigating to 'Popular' category, automatically set sort to 'Popular'.
|
||||
sortBy.value = 'popular'
|
||||
} else if (!isPopularNav && isPopularSort) {
|
||||
// When navigating away from 'Popular' category while sort is 'Popular', reset sort to default.
|
||||
sortBy.value = 'default'
|
||||
}
|
||||
} else if (source === 'sort') {
|
||||
// When sort is changed away from 'Popular' while in the 'Popular' category,
|
||||
// reset the category to 'All Templates' to avoid a confusing state.
|
||||
if (isPopularNav && !isPopularSort) {
|
||||
selectedNavItem.value = 'all'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for changes from the two sources ('nav' and 'sort') and trigger the coordinator.
|
||||
watch(selectedNavItem, () => coordinateNavAndSort('nav'))
|
||||
watch(sortBy, () => coordinateNavAndSort('sort'))
|
||||
|
||||
// Convert between string array and object array for MultiSelect component
|
||||
const selectedModelObjects = computed({
|
||||
get() {
|
||||
@@ -578,9 +638,6 @@ const cardRefs = ref<HTMLElement[]>([])
|
||||
// Force re-render key for templates when sorting changes
|
||||
const templateListKey = ref(0)
|
||||
|
||||
// Navigation
|
||||
const selectedNavItem = ref<string | null>('all')
|
||||
|
||||
// Search text for model filter
|
||||
const modelSearchText = ref<string>('')
|
||||
|
||||
@@ -645,11 +702,19 @@ const runsOnFilterLabel = computed(() => {
|
||||
|
||||
// Sort options
|
||||
const sortOptions = computed(() => [
|
||||
{ name: t('templateWorkflows.sort.newest', 'Newest'), value: 'newest' },
|
||||
{
|
||||
name: t('templateWorkflows.sort.default', 'Default'),
|
||||
value: 'default'
|
||||
},
|
||||
{
|
||||
name: t('templateWorkflows.sort.recommended', 'Recommended'),
|
||||
value: 'recommended'
|
||||
},
|
||||
{
|
||||
name: t('templateWorkflows.sort.popular', 'Popular'),
|
||||
value: 'popular'
|
||||
},
|
||||
{ name: t('templateWorkflows.sort.newest', 'Newest'), value: 'newest' },
|
||||
{
|
||||
name: t('templateWorkflows.sort.vramLowToHigh', 'VRAM Usage (Low to High)'),
|
||||
value: 'vram-low-to-high'
|
||||
@@ -750,7 +815,7 @@ const pageTitle = computed(() => {
|
||||
// Initialize templates loading with useAsyncState
|
||||
const { isLoading } = useAsyncState(
|
||||
async () => {
|
||||
// Run both operations in parallel for better performance
|
||||
// Run all operations in parallel for better performance
|
||||
await Promise.all([
|
||||
loadTemplates(),
|
||||
workflowTemplatesStore.loadWorkflowTemplates()
|
||||
@@ -763,6 +828,14 @@ const { isLoading } = useAsyncState(
|
||||
}
|
||||
)
|
||||
|
||||
const isTemplateVisibleOnDistribution = (template: TemplateInfo) => {
|
||||
return (template.includeOnDistributions?.length ?? 0) > 0
|
||||
? distributions.value.some((d) =>
|
||||
template.includeOnDistributions?.includes(d)
|
||||
)
|
||||
: true
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
cardRefs.value = [] // Release DOM refs
|
||||
})
|
||||
|
||||
@@ -22,7 +22,6 @@ const QueueJobItemStub = defineComponent({
|
||||
runningNodeName: { type: String, default: undefined },
|
||||
activeDetailsId: { type: String, default: null }
|
||||
},
|
||||
emits: ['cancel', 'delete', 'menu', 'view', 'details-enter', 'details-leave'],
|
||||
template: '<div class="queue-job-item-stub"></div>'
|
||||
})
|
||||
|
||||
@@ -2,12 +2,8 @@ import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
LGraphEventMode,
|
||||
type Positionable,
|
||||
Reroute
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode, Positionable } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphEventMode, Reroute } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, vi } from 'vitest'
|
||||
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { subgraphTest } from '../../litegraph/subgraph/fixtures/subgraphFixtures'
|
||||
import { subgraphTest } from '@/lib/litegraph/src/subgraph/__fixtures__/subgraphFixtures'
|
||||
|
||||
import { usePriceBadge } from '@/composables/node/usePriceBadge'
|
||||
|
||||
@@ -284,7 +284,7 @@ describe('useJobMenu', () => {
|
||||
|
||||
await nextTick()
|
||||
const entry = findActionEntry(jobMenuEntries.value, 'report-error')
|
||||
entry?.onClick?.()
|
||||
void entry?.onClick?.()
|
||||
|
||||
expect(dialogServiceMock.showExecutionErrorDialog).toHaveBeenCalledWith(
|
||||
error
|
||||
@@ -460,7 +460,7 @@ describe('useJobMenu', () => {
|
||||
|
||||
await nextTick()
|
||||
const entry = findActionEntry(jobMenuEntries.value, 'download')
|
||||
entry?.onClick?.()
|
||||
void entry?.onClick?.()
|
||||
|
||||
expect(downloadFileMock).toHaveBeenCalledWith('https://asset')
|
||||
})
|
||||
@@ -471,7 +471,7 @@ describe('useJobMenu', () => {
|
||||
|
||||
await nextTick()
|
||||
const entry = findActionEntry(jobMenuEntries.value, 'download')
|
||||
entry?.onClick?.()
|
||||
void entry?.onClick?.()
|
||||
|
||||
expect(downloadFileMock).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -9,8 +9,8 @@ import { app } from '@/scripts/app'
|
||||
|
||||
// Mock vue-i18n for useExternalLink
|
||||
const mockLocale = ref('en')
|
||||
vi.mock('vue-i18n', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('vue-i18n')>()
|
||||
vi.mock('vue-i18n', async () => {
|
||||
const actual = await vi.importActual('vue-i18n')
|
||||
return {
|
||||
...actual,
|
||||
useI18n: vi.fn(() => ({
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick, ref } from 'vue'
|
||||
|
||||
@@ -19,10 +20,22 @@ const defaultSettingStore = {
|
||||
set: vi.fn().mockResolvedValue(undefined)
|
||||
}
|
||||
|
||||
const defaultRankingStore = {
|
||||
computeDefaultScore: vi.fn(() => 0),
|
||||
computePopularScore: vi.fn(() => 0),
|
||||
getUsageScore: vi.fn(() => 0),
|
||||
computeFreshness: vi.fn(() => 0.5),
|
||||
isLoaded: { value: false }
|
||||
}
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: vi.fn(() => defaultSettingStore)
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/templateRankingStore', () => ({
|
||||
useTemplateRankingStore: vi.fn(() => defaultRankingStore)
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/telemetry', () => ({
|
||||
useTelemetry: vi.fn(() => ({
|
||||
trackTemplateFilterChanged: vi.fn()
|
||||
@@ -34,6 +47,7 @@ const { useTemplateFiltering } =
|
||||
|
||||
describe('useTemplateFiltering', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
@@ -6,12 +6,14 @@ import type { Ref } from 'vue'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import type { TemplateInfo } from '@/platform/workflow/templates/types/template'
|
||||
import { useTemplateRankingStore } from '@/stores/templateRankingStore'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
|
||||
export function useTemplateFiltering(
|
||||
templates: Ref<TemplateInfo[]> | TemplateInfo[]
|
||||
) {
|
||||
const settingStore = useSettingStore()
|
||||
const rankingStore = useTemplateRankingStore()
|
||||
|
||||
const searchQuery = ref('')
|
||||
const selectedModels = ref<string[]>(
|
||||
@@ -25,6 +27,8 @@ export function useTemplateFiltering(
|
||||
)
|
||||
const sortBy = ref<
|
||||
| 'default'
|
||||
| 'recommended'
|
||||
| 'popular'
|
||||
| 'alphabetical'
|
||||
| 'newest'
|
||||
| 'vram-low-to-high'
|
||||
@@ -151,10 +155,42 @@ export function useTemplateFiltering(
|
||||
return Number.POSITIVE_INFINITY
|
||||
}
|
||||
|
||||
watch(
|
||||
filteredByRunsOn,
|
||||
(templates) => {
|
||||
rankingStore.largestUsageScore = Math.max(
|
||||
...templates.map((t) => t.usage || 0)
|
||||
)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const sortedTemplates = computed(() => {
|
||||
const templates = [...filteredByRunsOn.value]
|
||||
|
||||
switch (sortBy.value) {
|
||||
case 'recommended':
|
||||
// Curated: usage × 0.5 + internal × 0.3 + freshness × 0.2
|
||||
return templates.sort((a, b) => {
|
||||
const scoreA = rankingStore.computeDefaultScore(
|
||||
a.date,
|
||||
a.searchRank,
|
||||
a.usage
|
||||
)
|
||||
const scoreB = rankingStore.computeDefaultScore(
|
||||
b.date,
|
||||
b.searchRank,
|
||||
b.usage
|
||||
)
|
||||
return scoreB - scoreA
|
||||
})
|
||||
case 'popular':
|
||||
// User-driven: usage × 0.9 + freshness × 0.1
|
||||
return templates.sort((a, b) => {
|
||||
const scoreA = rankingStore.computePopularScore(a.date, a.usage)
|
||||
const scoreB = rankingStore.computePopularScore(b.date, b.usage)
|
||||
return scoreB - scoreA
|
||||
})
|
||||
case 'alphabetical':
|
||||
return templates.sort((a, b) => {
|
||||
const nameA = a.title || a.name || ''
|
||||
@@ -184,7 +220,7 @@ export function useTemplateFiltering(
|
||||
return vramA - vramB
|
||||
})
|
||||
case 'model-size-low-to-high':
|
||||
return templates.sort((a: any, b: any) => {
|
||||
return templates.sort((a, b) => {
|
||||
const sizeA =
|
||||
typeof a.size === 'number' ? a.size : Number.POSITIVE_INFINITY
|
||||
const sizeB =
|
||||
@@ -194,7 +230,6 @@ export function useTemplateFiltering(
|
||||
})
|
||||
case 'default':
|
||||
default:
|
||||
// Keep original order (default order)
|
||||
return templates
|
||||
}
|
||||
})
|
||||
@@ -206,7 +241,7 @@ export function useTemplateFiltering(
|
||||
selectedModels.value = []
|
||||
selectedUseCases.value = []
|
||||
selectedRunsOn.value = []
|
||||
sortBy.value = 'newest'
|
||||
sortBy.value = 'default'
|
||||
}
|
||||
|
||||
const removeModelFilter = (model: string) => {
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { LGraphCanvas, SubgraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode
|
||||
} from '../litegraph/subgraph/fixtures/subgraphHelpers'
|
||||
} from '@/lib/litegraph/src/subgraph/__fixtures__/subgraphHelpers'
|
||||
|
||||
const canvasEl: Partial<HTMLCanvasElement> = { addEventListener() {} }
|
||||
const canvas: Partial<LGraphCanvas> = { canvas: canvasEl as HTMLCanvasElement }
|
||||
@@ -392,7 +392,8 @@ class Load3d {
|
||||
this.STATUS_MOUSE_ON_SCENE ||
|
||||
this.STATUS_MOUSE_ON_VIEWER ||
|
||||
this.isRecording() ||
|
||||
!this.INITIAL_RENDER_DONE
|
||||
!this.INITIAL_RENDER_DONE ||
|
||||
this.animationManager.isAnimationPlaying
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -166,6 +166,10 @@ export class LoaderManager implements LoaderManagerInterface {
|
||||
fbxModel.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
this.modelManager.originalMaterials.set(child, child.material)
|
||||
|
||||
if (child instanceof THREE.SkinnedMesh) {
|
||||
child.frustumCulled = false
|
||||
}
|
||||
}
|
||||
})
|
||||
break
|
||||
@@ -212,6 +216,10 @@ export class LoaderManager implements LoaderManagerInterface {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
child.geometry.computeVertexNormals()
|
||||
this.modelManager.originalMaterials.set(child, child.material)
|
||||
|
||||
if (child instanceof THREE.SkinnedMesh) {
|
||||
child.frustumCulled = false
|
||||
}
|
||||
}
|
||||
})
|
||||
break
|
||||
|
||||
@@ -3,7 +3,7 @@ import { describe } from 'vitest'
|
||||
|
||||
import { LGraph } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { dirtyTest } from './fixtures/testExtensions'
|
||||
import { dirtyTest } from './__fixtures__/testExtensions'
|
||||
|
||||
describe.skip('LGraph configure()', () => {
|
||||
dirtyTest(
|
||||
@@ -3,7 +3,7 @@ import { describe } from 'vitest'
|
||||
|
||||
import { LGraph } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { dirtyTest } from './fixtures/testExtensions'
|
||||
import { dirtyTest } from './__fixtures__/testExtensions'
|
||||
|
||||
describe.skip('LGraph (constructor only)', () => {
|
||||
dirtyTest(
|
||||
@@ -3,7 +3,7 @@ import { describe } from 'vitest'
|
||||
import { LGraph, LGraphGroup, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { ISerialisedGraph } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { test } from './fixtures/testExtensions'
|
||||
import { test } from './__fixtures__/testExtensions'
|
||||
|
||||
describe('LGraph Serialisation', () => {
|
||||
test('can (de)serialise node / group titles', ({ expect, minimalGraph }) => {
|
||||
@@ -1,10 +1,43 @@
|
||||
import { describe } from 'vitest'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { LGraph, LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { test } from './fixtures/testExtensions'
|
||||
import { test } from './__fixtures__/testExtensions'
|
||||
|
||||
function swapNodes(nodes: LGraphNode[]) {
|
||||
const firstNode = nodes[0]
|
||||
const lastNode = nodes[nodes.length - 1]
|
||||
nodes[0] = lastNode
|
||||
nodes[nodes.length - 1] = firstNode
|
||||
return nodes
|
||||
}
|
||||
|
||||
function createGraph(...nodes: LGraphNode[]) {
|
||||
const graph = new LGraph()
|
||||
nodes.forEach((node) => graph.add(node))
|
||||
return graph
|
||||
}
|
||||
|
||||
class DummyNode extends LGraphNode {
|
||||
constructor() {
|
||||
super('dummy')
|
||||
}
|
||||
}
|
||||
|
||||
describe('LGraph', () => {
|
||||
it('should serialize deterministic node order', async () => {
|
||||
LiteGraph.registerNodeType('dummy', DummyNode)
|
||||
const node1 = new DummyNode()
|
||||
const node2 = new DummyNode()
|
||||
const graph = createGraph(node1, node2)
|
||||
|
||||
const result1 = graph.serialize({ sortNodes: true })
|
||||
expect(result1.nodes).not.toHaveLength(0)
|
||||
graph._nodes = swapNodes(graph.nodes)
|
||||
const result2 = graph.serialize({ sortNodes: true })
|
||||
|
||||
expect(result1).toEqual(result2)
|
||||
})
|
||||
test('can be instantiated', ({ expect }) => {
|
||||
// @ts-expect-error Intentional - extra holds any / all consumer data that should be serialised
|
||||
const graph = new LGraph({ extra: 'TestGraph' })
|
||||
@@ -1,7 +1,6 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { LGraphButton } from '@/lib/litegraph/src/litegraph'
|
||||
import { Rectangle } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphButton, Rectangle } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
describe('LGraphButton', () => {
|
||||
describe('Constructor', () => {
|
||||
@@ -1,7 +1,10 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
LGraphCanvas,
|
||||
LGraphNode,
|
||||
LiteGraph
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
describe('LGraphCanvas Title Button Rendering', () => {
|
||||
let canvas: LGraphCanvas
|
||||
@@ -2,7 +2,7 @@ import { describe, expect } from 'vitest'
|
||||
|
||||
import { LGraphGroup } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { test } from './fixtures/testExtensions'
|
||||
import { test } from './__fixtures__/testExtensions'
|
||||
|
||||
describe('LGraphGroup', () => {
|
||||
test('serializes to the existing format', () => {
|
||||
@@ -2,7 +2,7 @@ import { beforeEach, describe, expect } from 'vitest'
|
||||
|
||||
import { LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { test } from './fixtures/testExtensions'
|
||||
import { test } from './__fixtures__/testExtensions'
|
||||
|
||||
describe('LGraphNode resize functionality', () => {
|
||||
let node: LGraphNode
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
NodeOutputSlot
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { test } from './fixtures/testExtensions'
|
||||
import { test } from './__fixtures__/testExtensions'
|
||||
|
||||
function getMockISerialisedNode(
|
||||
data: Partial<ISerialisedNode>
|
||||
@@ -1,8 +1,7 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { LGraphButton } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphButton, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
describe('LGraphNode Title Buttons', () => {
|
||||
describe('addTitleButton', () => {
|
||||
@@ -2,7 +2,7 @@ import { describe, expect } from 'vitest'
|
||||
|
||||
import { LLink } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { test } from './fixtures/testExtensions'
|
||||
import { test } from './__fixtures__/testExtensions'
|
||||
|
||||
describe('LLink', () => {
|
||||
test('matches previous snapshot', () => {
|
||||
@@ -1,3 +1,4 @@
|
||||
// oxlint-disable no-empty-pattern
|
||||
import { test as baseTest } from 'vitest'
|
||||
|
||||
import { LGraph } from '@/lib/litegraph/src/LGraph'
|
||||
@@ -33,7 +34,6 @@ interface DirtyFixtures {
|
||||
}
|
||||
|
||||
export const test = baseTest.extend<LitegraphFixtures>({
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
minimalGraph: async ({}, use) => {
|
||||
// Before each test function
|
||||
const serialisable = structuredClone(minimalSerialisableGraph)
|
||||
@@ -48,7 +48,7 @@ export const test = baseTest.extend<LitegraphFixtures>({
|
||||
floatingLink as unknown as ISerialisedGraph
|
||||
),
|
||||
linkedNodesGraph: structuredClone(linkedNodes as unknown as ISerialisedGraph),
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
|
||||
floatingBranchGraph: async ({}, use) => {
|
||||
const cloned = structuredClone(
|
||||
floatingBranch as unknown as ISerialisedGraph
|
||||
@@ -56,7 +56,7 @@ export const test = baseTest.extend<LitegraphFixtures>({
|
||||
const graph = new LGraph(cloned)
|
||||
await use(graph)
|
||||
},
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
|
||||
reroutesComplexGraph: async ({}, use) => {
|
||||
const cloned = structuredClone(
|
||||
reroutesComplex as unknown as ISerialisedGraph
|
||||
@@ -68,7 +68,6 @@ export const test = baseTest.extend<LitegraphFixtures>({
|
||||
|
||||
/** Test that use {@link DirtyFixtures}. One test per file. */
|
||||
export const dirtyTest = test.extend<DirtyFixtures>({
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
basicSerialisableGraph: async ({}, use) => {
|
||||
if (!basicSerialisableGraph.nodes) throw new Error('Invalid test object')
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// oxlint-disable no-empty-pattern
|
||||
import { test as baseTest, describe, expect, vi } from 'vitest'
|
||||
|
||||
import type {
|
||||
@@ -1,3 +1,4 @@
|
||||
// oxlint-disable no-empty-pattern
|
||||
// TODO: Fix these tests after migration
|
||||
import { afterEach, describe, expect, vi } from 'vitest'
|
||||
|
||||
@@ -9,7 +10,7 @@ import type {
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphNode, LLink, LinkConnector } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { test as baseTest } from './fixtures/testExtensions'
|
||||
import { test as baseTest } from '../__fixtures__/testExtensions'
|
||||
import type { ConnectingLink } from '@/lib/litegraph/src/interfaces'
|
||||
|
||||
interface TestContext {
|
||||
@@ -1077,6 +1078,7 @@ describe('LinkConnector Integration', () => {
|
||||
const originalParentChain = LLink.getReroutes(graph, toReroute)
|
||||
|
||||
const sortAndJoin = (numbers: Iterable<number>) =>
|
||||
// oxlint-disable-next-line require-array-sort-compare
|
||||
[...numbers].sort().join(',')
|
||||
const hasIdenticalLinks = (a: Reroute, b: Reroute) =>
|
||||
sortAndJoin(a.linkIds) === sortAndJoin(b.linkIds) &&
|
||||
@@ -12,7 +12,7 @@ import { ToInputFromIoNodeLink } from '@/lib/litegraph/src/canvas/ToInputFromIoN
|
||||
import type { NodeInputSlot } from '@/lib/litegraph/src/litegraph'
|
||||
import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums'
|
||||
|
||||
import { createTestSubgraph } from '../subgraph/fixtures/subgraphHelpers'
|
||||
import { createTestSubgraph } from '../subgraph/__fixtures__/subgraphHelpers'
|
||||
|
||||
describe('LinkConnector SubgraphInput connection validation', () => {
|
||||
let connector: LinkConnector
|
||||
@@ -1,7 +1,9 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { ToOutputRenderLink } from '@/lib/litegraph/src/litegraph'
|
||||
import { LinkDirection } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
LinkDirection,
|
||||
ToOutputRenderLink
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
describe('ToOutputRenderLink', () => {
|
||||
describe('connectToOutput', () => {
|
||||
@@ -1,3 +1,4 @@
|
||||
// oxlint-disable no-empty-pattern
|
||||
import { test as baseTest, describe, expect, vi } from 'vitest'
|
||||
|
||||
import { Rectangle } from '@/lib/litegraph/src/litegraph'
|
||||
@@ -6,7 +7,6 @@ import type { Point, Size } from '@/lib/litegraph/src/litegraph'
|
||||
// TODO: If there's a common test context, use it here
|
||||
// For now, we'll define a simple context for Rectangle tests
|
||||
const test = baseTest.extend<{ rect: Rectangle }>({
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
rect: async ({}, use) => {
|
||||
await use(new Rectangle())
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
import { clamp } from 'es-toolkit/compat'
|
||||
import { beforeEach, describe, expect, vi } from 'vitest'
|
||||
|
||||
import { LiteGraphGlobal } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphCanvas, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
LiteGraphGlobal,
|
||||
LGraphCanvas,
|
||||
LiteGraph
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { test } from './fixtures/testExtensions'
|
||||
import { test } from './__fixtures__/testExtensions'
|
||||
|
||||
describe('Litegraph module', () => {
|
||||
test('contains a global export', ({ expect }) => {
|
||||
@@ -1,14 +1,17 @@
|
||||
// TODO: Fix these tests after migration
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { ExecutableNodeDTO } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
LGraph,
|
||||
LGraphNode,
|
||||
ExecutableNodeDTO
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import {
|
||||
createNestedSubgraphs,
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode
|
||||
} from './fixtures/subgraphHelpers'
|
||||
} from './__fixtures__/subgraphHelpers'
|
||||
|
||||
describe.skip('ExecutableNodeDTO Creation', () => {
|
||||
it('should create DTO from regular node', () => {
|
||||
@@ -8,16 +8,19 @@
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { RecursionError } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraph, Subgraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { createUuidv4 } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
createUuidv4,
|
||||
RecursionError,
|
||||
LGraph,
|
||||
Subgraph
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { subgraphTest } from './fixtures/subgraphFixtures'
|
||||
import { subgraphTest } from './__fixtures__/subgraphFixtures'
|
||||
import {
|
||||
assertSubgraphStructure,
|
||||
createTestSubgraph,
|
||||
createTestSubgraphData
|
||||
} from './fixtures/subgraphHelpers'
|
||||
} from './__fixtures__/subgraphHelpers'
|
||||
|
||||
describe.skip('Subgraph Construction', () => {
|
||||
it('should create a subgraph with minimal data', () => {
|
||||
@@ -1,18 +1,17 @@
|
||||
// TODO: Fix these tests after migration
|
||||
import { assert, describe, expect, it } from 'vitest'
|
||||
|
||||
import type { LGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
type ISlotType,
|
||||
LGraphGroup,
|
||||
LGraphNode,
|
||||
LiteGraph
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraph, ISlotType } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import {
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode
|
||||
} from './fixtures/subgraphHelpers'
|
||||
} from './__fixtures__/subgraphHelpers'
|
||||
|
||||
function createNode(
|
||||
graph: LGraph,
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
createNestedSubgraphs,
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode
|
||||
} from './fixtures/subgraphHelpers'
|
||||
} from './__fixtures__/subgraphHelpers'
|
||||
|
||||
describe.skip('SubgraphEdgeCases - Recursion Detection', () => {
|
||||
it('should handle circular subgraph references without crashing', () => {
|
||||
@@ -1,8 +1,8 @@
|
||||
// TODO: Fix these tests after migration
|
||||
import { describe, expect, vi } from 'vitest'
|
||||
|
||||
import { subgraphTest } from './fixtures/subgraphFixtures'
|
||||
import { verifyEventSequence } from './fixtures/subgraphHelpers'
|
||||
import { subgraphTest } from './__fixtures__/subgraphFixtures'
|
||||
import { verifyEventSequence } from './__fixtures__/subgraphHelpers'
|
||||
|
||||
describe.skip('SubgraphEvents - Event Payload Verification', () => {
|
||||
subgraphTest(
|
||||
@@ -5,11 +5,11 @@ import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { ToInputFromIoNodeLink } from '@/lib/litegraph/src/canvas/ToInputFromIoNodeLink'
|
||||
import { LinkDirection } from '@/lib/litegraph/src//types/globalEnums'
|
||||
|
||||
import { subgraphTest } from './fixtures/subgraphFixtures'
|
||||
import { subgraphTest } from './__fixtures__/subgraphFixtures'
|
||||
import {
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode
|
||||
} from './fixtures/subgraphHelpers'
|
||||
} from './__fixtures__/subgraphHelpers'
|
||||
|
||||
describe('SubgraphIO - Input Slot Dual-Nature Behavior', () => {
|
||||
subgraphTest(
|
||||
@@ -3,11 +3,11 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { LGraph } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { subgraphTest } from './fixtures/subgraphFixtures'
|
||||
import { subgraphTest } from './__fixtures__/subgraphFixtures'
|
||||
import {
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode
|
||||
} from './fixtures/subgraphHelpers'
|
||||
} from './__fixtures__/subgraphHelpers'
|
||||
|
||||
describe.skip('SubgraphNode Memory Management', () => {
|
||||
describe.skip('Event Listener Cleanup', () => {
|
||||
@@ -9,11 +9,11 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { LGraph, Subgraph } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { subgraphTest } from './fixtures/subgraphFixtures'
|
||||
import { subgraphTest } from './__fixtures__/subgraphFixtures'
|
||||
import {
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode
|
||||
} from './fixtures/subgraphHelpers'
|
||||
} from './__fixtures__/subgraphHelpers'
|
||||
|
||||
describe.skip('SubgraphNode Construction', () => {
|
||||
it('should create a SubgraphNode from a subgraph definition', () => {
|
||||
@@ -7,7 +7,7 @@ import type { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode
|
||||
} from './fixtures/subgraphHelpers'
|
||||
} from './__fixtures__/subgraphHelpers'
|
||||
|
||||
describe.skip('SubgraphNode Title Button', () => {
|
||||
describe.skip('Constructor', () => {
|
||||
@@ -12,7 +12,7 @@ import { LGraph, Subgraph } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode
|
||||
} from './fixtures/subgraphHelpers'
|
||||
} from './__fixtures__/subgraphHelpers'
|
||||
|
||||
describe.skip('SubgraphSerialization - Basic Serialization', () => {
|
||||
it('should save and load simple subgraphs', () => {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user