Files
ComfyUI_frontend/tests-ui/stores/templateRankingStore.test.ts
Christian Byrne fbdaf5d7f3 feat: New Template Library (#7062)
## Summary

Implement the new design for template library

## Changes

- What
  - New sort option: `Popular` and  `Recommended`
  - New category: `Popular`, leverage the `Popular` sorting
  - Support add category stick to top of the side bar 
- Support template customized visible in different platform by
`includeOnDistributions` field

### How to make `Popular` and `Recommended` work

Add usage-based ordering to workflow templates with position bias
correction, manual ranking (searchRank), and freshness boost.

New sort modes:
- "Recommended" (default): usage × 0.5 + searchRank × 0.3 + freshness ×
0.2
- "Popular": usage × 0.9 + freshness × 0.1

## Screenshots (if applicable)

New default ordering:

<img width="1812" height="1852" alt="Selection_2485"
src="https://github.com/user-attachments/assets/8f4ed6e9-9cf4-43a8-8796-022dcf4c277e"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7062-feat-usage-based-template-ordering-2bb6d73d365081f1ac65f8ad55fe8ce6)
by [Unito](https://www.unito.io)

Popular category:

<img width="281" height="283" alt="image"
src="https://github.com/user-attachments/assets/fd54fcb8-6caa-4982-a6b6-1f70ca4b31e3"
/>

---------

Co-authored-by: Yourz <crazilou@vip.qq.com>
Co-authored-by: GitHub Action <action@github.com>
2026-01-06 19:10:40 +01:00

136 lines
4.8 KiB
TypeScript

import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useTemplateRankingStore } from '@/stores/templateRankingStore'
// Mock axios
vi.mock('axios', () => ({
default: {
get: vi.fn()
}
}))
describe('templateRankingStore', () => {
beforeEach(() => {
setActivePinia(createPinia())
vi.clearAllMocks()
})
describe('computeFreshness', () => {
it('returns 1.0 for brand new template (today)', () => {
const store = useTemplateRankingStore()
const today = new Date().toISOString().split('T')[0]
const freshness = store.computeFreshness(today)
expect(freshness).toBeCloseTo(1.0, 1)
})
it('returns ~0.5 for 90-day old template', () => {
const store = useTemplateRankingStore()
const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000)
.toISOString()
.split('T')[0]
const freshness = store.computeFreshness(ninetyDaysAgo)
expect(freshness).toBeCloseTo(0.5, 1)
})
it('returns 0.1 minimum for very old template', () => {
const store = useTemplateRankingStore()
const freshness = store.computeFreshness('2020-01-01')
expect(freshness).toBe(0.1)
})
it('returns 0.5 for undefined date', () => {
const store = useTemplateRankingStore()
expect(store.computeFreshness(undefined)).toBe(0.5)
})
it('returns 0.5 for invalid date', () => {
const store = useTemplateRankingStore()
expect(store.computeFreshness('not-a-date')).toBe(0.5)
})
})
describe('computeDefaultScore', () => {
it('uses default searchRank of 5 when not provided', () => {
const store = useTemplateRankingStore()
// Set largestUsageScore to avoid NaN when usage is 0
store.largestUsageScore = 100
const score = store.computeDefaultScore('2024-01-01', undefined, 0)
// With no usage score loaded, usage = 0
// internal = 5/10 = 0.5, freshness ~0.1 (old date)
// score = 0 * 0.5 + 0.5 * 0.3 + 0.1 * 0.2 = 0.15 + 0.02 = 0.17
expect(score).toBeCloseTo(0.17, 1)
})
it('high searchRank (10) boosts score', () => {
const store = useTemplateRankingStore()
store.largestUsageScore = 100
const lowRank = store.computeDefaultScore('2024-01-01', 1, 0)
const highRank = store.computeDefaultScore('2024-01-01', 10, 0)
expect(highRank).toBeGreaterThan(lowRank)
})
it('low searchRank (1) demotes score', () => {
const store = useTemplateRankingStore()
store.largestUsageScore = 100
const neutral = store.computeDefaultScore('2024-01-01', 5, 0)
const demoted = store.computeDefaultScore('2024-01-01', 1, 0)
expect(demoted).toBeLessThan(neutral)
})
it('searchRank difference is significant', () => {
const store = useTemplateRankingStore()
store.largestUsageScore = 100
const rank1 = store.computeDefaultScore('2024-01-01', 1, 0)
const rank10 = store.computeDefaultScore('2024-01-01', 10, 0)
// Difference should be 0.9 * 0.3 = 0.27 (30% weight, 0.9 range)
expect(rank10 - rank1).toBeCloseTo(0.27, 2)
})
})
describe('computePopularScore', () => {
it('does not use searchRank', () => {
const store = useTemplateRankingStore()
store.largestUsageScore = 100
// Popular score ignores searchRank - just usage + freshness
const score1 = store.computePopularScore('2024-01-01', 0)
const score2 = store.computePopularScore('2024-01-01', 0)
expect(score1).toBe(score2)
})
it('newer templates score higher', () => {
const store = useTemplateRankingStore()
store.largestUsageScore = 100
const today = new Date().toISOString().split('T')[0]
const oldScore = store.computePopularScore('2020-01-01', 0)
const newScore = store.computePopularScore(today, 0)
expect(newScore).toBeGreaterThan(oldScore)
})
})
describe('searchRank edge cases', () => {
it('handles searchRank of 0 (should still work, treated as very low)', () => {
const store = useTemplateRankingStore()
store.largestUsageScore = 100
const score = store.computeDefaultScore('2024-01-01', 0, 0)
expect(score).toBeGreaterThanOrEqual(0)
})
it('handles searchRank above 10 (clamping not enforced, but works)', () => {
const store = useTemplateRankingStore()
store.largestUsageScore = 100
const rank10 = store.computeDefaultScore('2024-01-01', 10, 0)
const rank15 = store.computeDefaultScore('2024-01-01', 15, 0)
expect(rank15).toBeGreaterThan(rank10)
})
it('handles negative searchRank', () => {
const store = useTemplateRankingStore()
store.largestUsageScore = 100
const score = store.computeDefaultScore('2024-01-01', -5, 0)
// Should still compute, just negative contribution from searchRank
expect(typeof score).toBe('number')
})
})
})