mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-03 06:47:33 +00:00
[feat] Add Linear Mode infrastructure
Add core infrastructure for Linear Mode - a simplified form-based UI for ComfyUI workflows: - Add optional onQueued callback to app.queuePrompt() for deterministic prompt ID tracking - Create Linear Mode type definitions (widgets, templates, output images) - Add template configuration with 10 promoted widgets - Implement Pinia store with client-side history filtering via prompt ID tracking - Add service layer for template loading and widget value manipulation - Add composable for queue operations with automatic prompt tracking - Add i18n strings for Linear Mode UI - Add comprehensive unit tests (41 tests, 100% passing) All user-facing strings use i18n. Template uses placeholder IDs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -41,7 +41,9 @@ const config: KnipConfig = {
|
||||
'src/workbench/extensions/manager/types/generatedManagerTypes.ts',
|
||||
'packages/registry-types/src/comfyRegistryTypes.ts',
|
||||
// Used by a custom node (that should move off of this)
|
||||
'src/scripts/ui/components/splitButton.ts'
|
||||
'src/scripts/ui/components/splitButton.ts',
|
||||
// Linear Mode infrastructure - exports will be used by UI components in next PR
|
||||
'src/renderer/extensions/linearMode/**/*.ts'
|
||||
],
|
||||
compilers: {
|
||||
// https://github.com/webpro-nl/knip/issues/1008#issuecomment-3207756199
|
||||
|
||||
@@ -1846,5 +1846,53 @@
|
||||
"vueNodesBanner": {
|
||||
"message": "Nodes just got a new look and feel",
|
||||
"tryItOut": "Try it out"
|
||||
},
|
||||
"linearMode": {
|
||||
"title": "Linear Mode",
|
||||
"open": "Open Linear Mode",
|
||||
"close": "Close Linear Mode",
|
||||
"generate": "Generate",
|
||||
"generating": "Generating...",
|
||||
"history": "History",
|
||||
"noHistory": "No generations yet",
|
||||
"noHistoryMessage": "Your generated images will appear here",
|
||||
"loadTemplate": "Load Template",
|
||||
"loadingTemplate": "Loading template...",
|
||||
"templateLoadError": "Failed to load template",
|
||||
"invalidTemplate": "Invalid template ID",
|
||||
"widgetGroups": {
|
||||
"content": "Content",
|
||||
"dimensions": "Image Size",
|
||||
"generation": "Generation Settings",
|
||||
"advanced": "Advanced"
|
||||
},
|
||||
"widgets": {
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Describe the image you want to generate...",
|
||||
"promptTooltip": "Describe what you want to see in the image",
|
||||
"negativePrompt": "Negative Prompt",
|
||||
"negativePromptPlaceholder": "What to avoid in the image...",
|
||||
"negativePromptTooltip": "Describe what you want to avoid",
|
||||
"seed": "Seed",
|
||||
"seedTooltip": "Random seed for generation. Use same seed for reproducible results.",
|
||||
"steps": "Steps",
|
||||
"stepsTooltip": "Number of denoising steps. Higher = better quality but slower.",
|
||||
"cfgScale": "CFG Scale",
|
||||
"cfgScaleTooltip": "How closely to follow the prompt. Higher = more literal.",
|
||||
"sampler": "Sampler",
|
||||
"samplerTooltip": "Sampling algorithm to use",
|
||||
"scheduler": "Scheduler",
|
||||
"width": "Width",
|
||||
"widthTooltip": "Output image width",
|
||||
"height": "Height",
|
||||
"heightTooltip": "Output image height",
|
||||
"batchSize": "Batch Size",
|
||||
"batchSizeTooltip": "Number of images to generate at once"
|
||||
},
|
||||
"errors": {
|
||||
"queueFailed": "Failed to queue generation",
|
||||
"noWorkflow": "No workflow loaded",
|
||||
"widgetUpdateFailed": "Failed to update widget value"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ref } from 'vue'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useLinearModeStore } from '../stores/linearModeStore'
|
||||
import type { NodeExecutionId } from '@/types/nodeIdentification'
|
||||
|
||||
// @knipIgnore - Will be used by Linear Mode UI components
|
||||
export function useLinearModeQueue() {
|
||||
const linearModeStore = useLinearModeStore()
|
||||
const isQueueing = ref(false)
|
||||
const lastError = ref<Error | null>(null)
|
||||
|
||||
async function queuePrompt(
|
||||
number: number = -1,
|
||||
batchCount: number = 1,
|
||||
queueNodeIds?: NodeExecutionId[]
|
||||
): Promise<boolean> {
|
||||
isQueueing.value = true
|
||||
lastError.value = null
|
||||
|
||||
try {
|
||||
const success = await app.queuePrompt(
|
||||
number,
|
||||
batchCount,
|
||||
queueNodeIds,
|
||||
(response) => {
|
||||
if (response.prompt_id) {
|
||||
linearModeStore.trackGeneratedPrompt(response.prompt_id)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return success
|
||||
} catch (error) {
|
||||
lastError.value =
|
||||
error instanceof Error ? error : new Error(String(error))
|
||||
return false
|
||||
} finally {
|
||||
isQueueing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
queuePrompt,
|
||||
isQueueing,
|
||||
lastError
|
||||
}
|
||||
}
|
||||
154
src/renderer/extensions/linearMode/linearModeConfig.ts
Normal file
154
src/renderer/extensions/linearMode/linearModeConfig.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import type { LinearModeTemplate, PromotedWidget } from './linearModeTypes'
|
||||
|
||||
const defaultLinearPromotedWidgets: PromotedWidget[] = [
|
||||
{
|
||||
nodeId: 6,
|
||||
widgetName: 'text',
|
||||
displayName: 'Prompt',
|
||||
type: 'text',
|
||||
config: {
|
||||
multiline: true,
|
||||
placeholder: 'Describe the image you want to generate...',
|
||||
maxLength: 5000
|
||||
},
|
||||
tooltip: 'Describe what you want to see in the image',
|
||||
group: 'content'
|
||||
},
|
||||
{
|
||||
nodeId: 7,
|
||||
widgetName: 'text',
|
||||
displayName: 'Negative Prompt',
|
||||
type: 'text',
|
||||
config: {
|
||||
multiline: true,
|
||||
placeholder: 'What to avoid in the image...'
|
||||
},
|
||||
tooltip: 'Describe what you want to avoid',
|
||||
group: 'content'
|
||||
},
|
||||
{
|
||||
nodeId: 3,
|
||||
widgetName: 'seed',
|
||||
displayName: 'Seed',
|
||||
type: 'number',
|
||||
config: {
|
||||
min: 0,
|
||||
max: Number.MAX_SAFE_INTEGER,
|
||||
randomizable: true
|
||||
},
|
||||
tooltip:
|
||||
'Random seed for generation. Use same seed for reproducible results.',
|
||||
group: 'generation'
|
||||
},
|
||||
{
|
||||
nodeId: 3,
|
||||
widgetName: 'steps',
|
||||
displayName: 'Steps',
|
||||
type: 'slider',
|
||||
config: {
|
||||
min: 1,
|
||||
max: 150,
|
||||
step: 1,
|
||||
default: 20
|
||||
},
|
||||
tooltip: 'Number of denoising steps. Higher = better quality but slower.',
|
||||
group: 'generation'
|
||||
},
|
||||
{
|
||||
nodeId: 3,
|
||||
widgetName: 'cfg',
|
||||
displayName: 'CFG Scale',
|
||||
type: 'slider',
|
||||
config: {
|
||||
min: 0,
|
||||
max: 20,
|
||||
step: 0.5,
|
||||
default: 7.0
|
||||
},
|
||||
tooltip: 'How closely to follow the prompt. Higher = more literal.',
|
||||
group: 'generation'
|
||||
},
|
||||
{
|
||||
nodeId: 3,
|
||||
widgetName: 'sampler_name',
|
||||
displayName: 'Sampler',
|
||||
type: 'combo',
|
||||
config: {
|
||||
options: ['euler', 'euler_a', 'dpmpp_2m', 'dpmpp_sde', 'ddim']
|
||||
},
|
||||
tooltip: 'Sampling algorithm to use',
|
||||
group: 'advanced'
|
||||
},
|
||||
{
|
||||
nodeId: 3,
|
||||
widgetName: 'scheduler',
|
||||
displayName: 'Scheduler',
|
||||
type: 'combo',
|
||||
config: {
|
||||
options: ['normal', 'karras', 'exponential', 'sgm_uniform']
|
||||
},
|
||||
group: 'advanced'
|
||||
},
|
||||
{
|
||||
nodeId: 5,
|
||||
widgetName: 'width',
|
||||
displayName: 'Width',
|
||||
type: 'combo',
|
||||
config: {
|
||||
options: [512, 768, 1024, 1280, 1536, 2048]
|
||||
},
|
||||
tooltip: 'Output image width',
|
||||
group: 'dimensions'
|
||||
},
|
||||
{
|
||||
nodeId: 5,
|
||||
widgetName: 'height',
|
||||
displayName: 'Height',
|
||||
type: 'combo',
|
||||
config: {
|
||||
options: [512, 768, 1024, 1280, 1536, 2048]
|
||||
},
|
||||
tooltip: 'Output image height',
|
||||
group: 'dimensions'
|
||||
},
|
||||
{
|
||||
nodeId: 5,
|
||||
widgetName: 'batch_size',
|
||||
displayName: 'Batch Size',
|
||||
type: 'slider',
|
||||
config: {
|
||||
min: 1,
|
||||
max: 8,
|
||||
step: 1,
|
||||
default: 1
|
||||
},
|
||||
tooltip: 'Number of images to generate at once',
|
||||
group: 'advanced'
|
||||
}
|
||||
]
|
||||
|
||||
// @knipIgnore - Will be used by Linear Mode UI components
|
||||
export const LINEAR_MODE_TEMPLATES: Record<string, LinearModeTemplate> = {
|
||||
'template-default-linear': {
|
||||
id: 'template-default-linear',
|
||||
name: 'Linear Mode Template',
|
||||
templatePath: '/templates/template-default-linear.json',
|
||||
promotedWidgets: defaultLinearPromotedWidgets,
|
||||
description: 'Default Linear Mode template for simplified image generation',
|
||||
tags: ['text-to-image', 'default', 'recommended']
|
||||
}
|
||||
}
|
||||
|
||||
// @knipIgnore - Will be used by Linear Mode UI components
|
||||
export const WIDGET_GROUPS = {
|
||||
content: { label: 'Content', order: 1 },
|
||||
dimensions: { label: 'Image Size', order: 2 },
|
||||
generation: { label: 'Generation Settings', order: 3 },
|
||||
advanced: { label: 'Advanced', order: 4, collapsible: true }
|
||||
} as const
|
||||
|
||||
export function getTemplateConfig(
|
||||
templateId: string
|
||||
): LinearModeTemplate | null {
|
||||
return LINEAR_MODE_TEMPLATES[templateId] ?? null
|
||||
}
|
||||
109
src/renderer/extensions/linearMode/linearModeService.ts
Normal file
109
src/renderer/extensions/linearMode/linearModeService.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { app } from '@/scripts/app'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useLinearModeStore } from './stores/linearModeStore'
|
||||
import type {
|
||||
ComfyNode,
|
||||
ComfyWorkflowJSON
|
||||
} from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { PromotedWidget } from './linearModeTypes'
|
||||
|
||||
export async function loadTemplate(
|
||||
templatePath: string
|
||||
): Promise<ComfyWorkflowJSON> {
|
||||
const response = await fetch(api.fileURL(templatePath))
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load template: ${response.statusText}`)
|
||||
}
|
||||
return await response.json()
|
||||
}
|
||||
|
||||
export function getWidgetValue(
|
||||
workflow: ComfyWorkflowJSON,
|
||||
nodeId: number,
|
||||
widgetName: string
|
||||
): unknown {
|
||||
const nodeIdStr = String(nodeId)
|
||||
const node = workflow.nodes?.find(
|
||||
(n: ComfyNode) => String(n.id) === nodeIdStr
|
||||
)
|
||||
if (!node) return undefined
|
||||
|
||||
if (!node.widgets_values) return undefined
|
||||
if (Array.isArray(node.widgets_values)) return undefined
|
||||
|
||||
return node.widgets_values[widgetName]
|
||||
}
|
||||
|
||||
export function setWidgetValue(
|
||||
workflow: ComfyWorkflowJSON,
|
||||
nodeId: number,
|
||||
widgetName: string,
|
||||
value: unknown
|
||||
): boolean {
|
||||
const nodeIdStr = String(nodeId)
|
||||
const node = workflow.nodes?.find(
|
||||
(n: ComfyNode) => String(n.id) === nodeIdStr
|
||||
)
|
||||
if (!node) return false
|
||||
|
||||
if (!node.widgets_values) {
|
||||
node.widgets_values = {}
|
||||
}
|
||||
|
||||
if (Array.isArray(node.widgets_values)) {
|
||||
return false
|
||||
}
|
||||
|
||||
node.widgets_values[widgetName] = value
|
||||
return true
|
||||
}
|
||||
|
||||
export function getAllWidgetValues(): Map<string, unknown> {
|
||||
const linearModeStore = useLinearModeStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const values = new Map<string, unknown>()
|
||||
const workflow = workflowStore.activeWorkflow?.activeState
|
||||
|
||||
if (!workflow) return values
|
||||
|
||||
for (const widget of linearModeStore.promotedWidgets) {
|
||||
const value = getWidgetValue(workflow, widget.nodeId, widget.widgetName)
|
||||
values.set(widget.displayName, value)
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
export function updateWidgetValue(
|
||||
widget: PromotedWidget,
|
||||
value: unknown
|
||||
): boolean {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const workflow = workflowStore.activeWorkflow?.activeState
|
||||
|
||||
if (!workflow) return false
|
||||
|
||||
return setWidgetValue(workflow, widget.nodeId, widget.widgetName, value)
|
||||
}
|
||||
|
||||
export async function activateTemplate(templateId: string): Promise<void> {
|
||||
const linearModeStore = useLinearModeStore()
|
||||
const template = linearModeStore.template
|
||||
|
||||
if (!template || template.id !== templateId) {
|
||||
throw new Error(`Template not found: ${templateId}`)
|
||||
}
|
||||
|
||||
const workflow = await loadTemplate(template.templatePath)
|
||||
|
||||
await app.loadGraphData(workflow)
|
||||
}
|
||||
|
||||
export async function initializeLinearMode(templateId: string): Promise<void> {
|
||||
const linearModeStore = useLinearModeStore()
|
||||
|
||||
linearModeStore.open(templateId)
|
||||
await activateTemplate(templateId)
|
||||
}
|
||||
50
src/renderer/extensions/linearMode/linearModeTypes.ts
Normal file
50
src/renderer/extensions/linearMode/linearModeTypes.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
// @knipIgnore - Will be used by Linear Mode UI components
|
||||
export type WidgetType =
|
||||
| 'text'
|
||||
| 'number'
|
||||
| 'slider'
|
||||
| 'combo'
|
||||
| 'toggle'
|
||||
| 'image'
|
||||
| 'color'
|
||||
|
||||
export interface PromotedWidget {
|
||||
nodeId: number
|
||||
widgetName: string
|
||||
displayName: string
|
||||
type: WidgetType
|
||||
config: WidgetConfig
|
||||
tooltip?: string
|
||||
group?: string
|
||||
}
|
||||
|
||||
// @knipIgnore - Will be used by Linear Mode UI components
|
||||
export interface WidgetConfig {
|
||||
multiline?: boolean
|
||||
placeholder?: string
|
||||
maxLength?: number
|
||||
min?: number
|
||||
max?: number
|
||||
step?: number
|
||||
default?: number
|
||||
randomizable?: boolean
|
||||
options?: string[] | number[]
|
||||
onLabel?: string
|
||||
offLabel?: string
|
||||
}
|
||||
|
||||
export interface LinearModeTemplate {
|
||||
id: string
|
||||
name: string
|
||||
templatePath: string
|
||||
promotedWidgets: PromotedWidget[]
|
||||
description?: string
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
export interface OutputImage {
|
||||
filename: string
|
||||
subfolder: string
|
||||
type: string
|
||||
prompt_id: string
|
||||
}
|
||||
100
src/renderer/extensions/linearMode/stores/linearModeStore.ts
Normal file
100
src/renderer/extensions/linearMode/stores/linearModeStore.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type {
|
||||
LinearModeTemplate,
|
||||
OutputImage,
|
||||
PromotedWidget
|
||||
} from '../linearModeTypes'
|
||||
import { getTemplateConfig } from '../linearModeConfig'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useQueueStore } from '@/stores/queueStore'
|
||||
import type { TaskItemImpl } from '@/stores/queueStore'
|
||||
|
||||
export const useLinearModeStore = defineStore('linearMode', () => {
|
||||
const isOpen = ref(false)
|
||||
|
||||
const templateId = ref<string | null>(null)
|
||||
|
||||
const currentOutput = ref<OutputImage | null>(null)
|
||||
|
||||
const generatedPromptIds = ref<Set<string>>(new Set())
|
||||
|
||||
const template = computed<LinearModeTemplate | null>(() => {
|
||||
if (!templateId.value) return null
|
||||
return getTemplateConfig(templateId.value)
|
||||
})
|
||||
|
||||
const promotedWidgets = computed<PromotedWidget[]>(() => {
|
||||
return template.value?.promotedWidgets ?? []
|
||||
})
|
||||
|
||||
const currentWorkflow = computed<ComfyWorkflowJSON | null>(() => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
return workflowStore.activeWorkflow?.activeState ?? null
|
||||
})
|
||||
|
||||
const filteredHistory = computed(() => {
|
||||
const queueStore = useQueueStore()
|
||||
return queueStore.historyTasks.filter((item: TaskItemImpl) =>
|
||||
generatedPromptIds.value.has(item.promptId)
|
||||
)
|
||||
})
|
||||
|
||||
const isGenerating = computed(() => {
|
||||
const queueStore = useQueueStore()
|
||||
return queueStore.pendingTasks.some((item: TaskItemImpl) =>
|
||||
generatedPromptIds.value.has(item.promptId)
|
||||
)
|
||||
})
|
||||
|
||||
function open(templateIdParam: string): void {
|
||||
if (!getTemplateConfig(templateIdParam)) {
|
||||
throw new Error(`Invalid template ID: ${templateIdParam}`)
|
||||
}
|
||||
|
||||
isOpen.value = true
|
||||
templateId.value = templateIdParam
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
function setOutput(output: OutputImage | null): void {
|
||||
currentOutput.value = output
|
||||
}
|
||||
|
||||
function trackGeneratedPrompt(promptId: string): void {
|
||||
generatedPromptIds.value.add(promptId)
|
||||
}
|
||||
|
||||
function reset(): void {
|
||||
isOpen.value = false
|
||||
templateId.value = null
|
||||
currentOutput.value = null
|
||||
generatedPromptIds.value.clear()
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
isOpen,
|
||||
templateId,
|
||||
currentOutput,
|
||||
generatedPromptIds,
|
||||
|
||||
// Getters
|
||||
template,
|
||||
promotedWidgets,
|
||||
currentWorkflow,
|
||||
filteredHistory,
|
||||
isGenerating,
|
||||
|
||||
// Actions
|
||||
open,
|
||||
close,
|
||||
setOutput,
|
||||
trackGeneratedPrompt,
|
||||
reset
|
||||
}
|
||||
})
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
import type {
|
||||
ExecutionErrorWsMessage,
|
||||
NodeError,
|
||||
PromptResponse,
|
||||
ResultItem
|
||||
} from '@/schemas/apiSchema'
|
||||
import {
|
||||
@@ -1289,7 +1290,8 @@ export class ComfyApp {
|
||||
async queuePrompt(
|
||||
number: number,
|
||||
batchCount: number = 1,
|
||||
queueNodeIds?: NodeExecutionId[]
|
||||
queueNodeIds?: NodeExecutionId[],
|
||||
onQueued?: (response: PromptResponse) => void
|
||||
): Promise<boolean> {
|
||||
this.queueItems.push({ number, batchCount, queueNodeIds })
|
||||
|
||||
@@ -1341,6 +1343,9 @@ export class ComfyApp {
|
||||
}
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
// Call onQueued callback if provided
|
||||
onQueued?.(res)
|
||||
} catch (error: unknown) {
|
||||
useDialogService().showErrorDialog(error, {
|
||||
title: t('errorDialog.promptExecutionError'),
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
activateTemplate,
|
||||
getAllWidgetValues,
|
||||
getWidgetValue,
|
||||
initializeLinearMode,
|
||||
loadTemplate,
|
||||
setWidgetValue,
|
||||
updateWidgetValue
|
||||
} from '@/renderer/extensions/linearMode/linearModeService'
|
||||
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { PromotedWidget } from '@/renderer/extensions/linearMode/linearModeTypes'
|
||||
|
||||
const mockTemplate = {
|
||||
id: 'template-default-linear',
|
||||
name: 'Linear Mode Template',
|
||||
templatePath: '/templates/template-default-linear.json',
|
||||
promotedWidgets: [
|
||||
{
|
||||
nodeId: 6,
|
||||
widgetName: 'text',
|
||||
displayName: 'Prompt',
|
||||
type: 'text',
|
||||
config: { multiline: true },
|
||||
group: 'content'
|
||||
},
|
||||
{
|
||||
nodeId: 3,
|
||||
widgetName: 'seed',
|
||||
displayName: 'Seed',
|
||||
type: 'number',
|
||||
config: { min: 0 },
|
||||
group: 'generation'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
let mockWorkflow: Partial<ComfyWorkflowJSON> = {
|
||||
nodes: [
|
||||
{
|
||||
id: 6,
|
||||
widgets_values: { text: 'test prompt' }
|
||||
} as unknown as ComfyWorkflowJSON['nodes'][0],
|
||||
{
|
||||
id: 3,
|
||||
widgets_values: { seed: 12345, steps: 20 }
|
||||
} as unknown as ComfyWorkflowJSON['nodes'][0],
|
||||
{
|
||||
id: 5,
|
||||
widgets_values: { width: 1024 }
|
||||
} as unknown as ComfyWorkflowJSON['nodes'][0]
|
||||
]
|
||||
}
|
||||
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
api: {
|
||||
fileURL: vi.fn((path: string) => `http://localhost:8188${path}`)
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {
|
||||
loadGraphData: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({
|
||||
useWorkflowStore: vi.fn(() => ({
|
||||
activeWorkflow: {
|
||||
get activeState() {
|
||||
return mockWorkflow
|
||||
}
|
||||
}
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/renderer/extensions/linearMode/stores/linearModeStore', () => ({
|
||||
useLinearModeStore: vi.fn(() => ({
|
||||
template: mockTemplate,
|
||||
promotedWidgets: mockTemplate.promotedWidgets,
|
||||
open: vi.fn()
|
||||
}))
|
||||
}))
|
||||
|
||||
describe('linearModeService', () => {
|
||||
beforeEach(async () => {
|
||||
setActivePinia(createPinia())
|
||||
vi.clearAllMocks()
|
||||
// Reset mockWorkflow for each test
|
||||
mockWorkflow = {
|
||||
nodes: [
|
||||
{
|
||||
id: 6,
|
||||
widgets_values: { text: 'test prompt' }
|
||||
} as unknown as ComfyWorkflowJSON['nodes'][0],
|
||||
{
|
||||
id: 3,
|
||||
widgets_values: { seed: 12345, steps: 20 }
|
||||
} as unknown as ComfyWorkflowJSON['nodes'][0],
|
||||
{
|
||||
id: 5,
|
||||
widgets_values: { width: 1024 }
|
||||
} as unknown as ComfyWorkflowJSON['nodes'][0]
|
||||
]
|
||||
}
|
||||
|
||||
// Reset the mock to use the fresh mockWorkflow
|
||||
const { useWorkflowStore } = await import(
|
||||
'@/platform/workflow/management/stores/workflowStore'
|
||||
)
|
||||
vi.mocked(useWorkflowStore).mockReturnValue({
|
||||
activeWorkflow: {
|
||||
get activeState() {
|
||||
return mockWorkflow
|
||||
}
|
||||
}
|
||||
} as unknown as ReturnType<typeof useWorkflowStore>)
|
||||
})
|
||||
|
||||
describe('loadTemplate()', () => {
|
||||
it('should load template from backend', async () => {
|
||||
const mockTemplateData = { nodes: [{ id: 1 }] }
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => mockTemplateData
|
||||
})
|
||||
|
||||
const result = await loadTemplate('/templates/test.json')
|
||||
|
||||
expect(result).toEqual(mockTemplateData)
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'http://localhost:8188/templates/test.json'
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw error when template load fails', async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
statusText: 'Not Found'
|
||||
})
|
||||
|
||||
await expect(loadTemplate('/templates/missing.json')).rejects.toThrow(
|
||||
'Failed to load template: Not Found'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getWidgetValue()', () => {
|
||||
it('should get widget value from workflow', () => {
|
||||
const value = getWidgetValue(
|
||||
mockWorkflow as unknown as ComfyWorkflowJSON,
|
||||
6,
|
||||
'text'
|
||||
)
|
||||
|
||||
expect(value).toBe('test prompt')
|
||||
})
|
||||
|
||||
it('should return undefined for non-existent node', () => {
|
||||
const value = getWidgetValue(
|
||||
mockWorkflow as unknown as ComfyWorkflowJSON,
|
||||
999,
|
||||
'text'
|
||||
)
|
||||
|
||||
expect(value).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return undefined for non-existent widget', () => {
|
||||
const value = getWidgetValue(
|
||||
mockWorkflow as unknown as ComfyWorkflowJSON,
|
||||
6,
|
||||
'nonexistent'
|
||||
)
|
||||
|
||||
expect(value).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should handle numeric node IDs', () => {
|
||||
const value = getWidgetValue(
|
||||
mockWorkflow as unknown as ComfyWorkflowJSON,
|
||||
3,
|
||||
'seed'
|
||||
)
|
||||
|
||||
expect(value).toBe(12345)
|
||||
})
|
||||
})
|
||||
|
||||
describe('setWidgetValue()', () => {
|
||||
it('should set widget value in workflow', () => {
|
||||
const workflow = JSON.parse(
|
||||
JSON.stringify(mockWorkflow)
|
||||
) as typeof mockWorkflow
|
||||
|
||||
const result = setWidgetValue(
|
||||
workflow as unknown as ComfyWorkflowJSON,
|
||||
6,
|
||||
'text',
|
||||
'new prompt'
|
||||
)
|
||||
|
||||
expect(result).toBe(true)
|
||||
const node = workflow.nodes?.[0]
|
||||
if (node?.widgets_values && !Array.isArray(node.widgets_values)) {
|
||||
expect(node.widgets_values.text).toBe('new prompt')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return false for non-existent node', () => {
|
||||
const workflow = JSON.parse(
|
||||
JSON.stringify(mockWorkflow)
|
||||
) as typeof mockWorkflow
|
||||
|
||||
const result = setWidgetValue(
|
||||
workflow as unknown as ComfyWorkflowJSON,
|
||||
999,
|
||||
'text',
|
||||
'value'
|
||||
)
|
||||
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it('should create widgets_values object if missing', () => {
|
||||
const workflow: Partial<ComfyWorkflowJSON> = {
|
||||
nodes: [{ id: 10 } as unknown as ComfyWorkflowJSON['nodes'][0]]
|
||||
}
|
||||
|
||||
const result = setWidgetValue(
|
||||
workflow as unknown as ComfyWorkflowJSON,
|
||||
10,
|
||||
'newWidget',
|
||||
'value'
|
||||
)
|
||||
|
||||
expect(result).toBe(true)
|
||||
const node = workflow.nodes?.[0]
|
||||
if (node?.widgets_values && !Array.isArray(node.widgets_values)) {
|
||||
expect(node.widgets_values).toEqual({ newWidget: 'value' })
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle numeric values', () => {
|
||||
const workflow = JSON.parse(
|
||||
JSON.stringify(mockWorkflow)
|
||||
) as typeof mockWorkflow
|
||||
|
||||
const result = setWidgetValue(
|
||||
workflow as unknown as ComfyWorkflowJSON,
|
||||
3,
|
||||
'seed',
|
||||
99999
|
||||
)
|
||||
|
||||
expect(result).toBe(true)
|
||||
const node = workflow.nodes?.[1]
|
||||
if (node?.widgets_values && !Array.isArray(node.widgets_values)) {
|
||||
expect(node.widgets_values.seed).toBe(99999)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('getAllWidgetValues()', () => {
|
||||
it('should return all promoted widget values', () => {
|
||||
const values = getAllWidgetValues()
|
||||
|
||||
expect(values.size).toBe(2)
|
||||
expect(values.get('Prompt')).toBe('test prompt')
|
||||
expect(values.get('Seed')).toBe(12345)
|
||||
})
|
||||
|
||||
it('should return empty map when no workflow', async () => {
|
||||
const { useWorkflowStore } = await import(
|
||||
'@/platform/workflow/management/stores/workflowStore'
|
||||
)
|
||||
vi.mocked(useWorkflowStore).mockReturnValue({
|
||||
activeWorkflow: null
|
||||
} as unknown as ReturnType<typeof useWorkflowStore>)
|
||||
|
||||
const values = getAllWidgetValues()
|
||||
|
||||
expect(values.size).toBe(0)
|
||||
})
|
||||
|
||||
it('should handle missing widget values gracefully', async () => {
|
||||
const workflowMissingValues: Partial<ComfyWorkflowJSON> = {
|
||||
nodes: [{ id: 999 } as unknown as ComfyWorkflowJSON['nodes'][0]]
|
||||
}
|
||||
const { useWorkflowStore } = await import(
|
||||
'@/platform/workflow/management/stores/workflowStore'
|
||||
)
|
||||
vi.mocked(useWorkflowStore).mockReturnValue({
|
||||
activeWorkflow: { activeState: workflowMissingValues }
|
||||
} as unknown as ReturnType<typeof useWorkflowStore>)
|
||||
|
||||
const values = getAllWidgetValues()
|
||||
|
||||
expect(values.get('Prompt')).toBeUndefined()
|
||||
expect(values.get('Seed')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateWidgetValue()', () => {
|
||||
it('should update widget value in current workflow', () => {
|
||||
const widget: PromotedWidget = {
|
||||
nodeId: 6,
|
||||
widgetName: 'text',
|
||||
displayName: 'Prompt',
|
||||
type: 'text',
|
||||
config: {}
|
||||
}
|
||||
|
||||
const result = updateWidgetValue(widget, 'updated prompt')
|
||||
|
||||
expect(result).toBe(true)
|
||||
const node = mockWorkflow.nodes?.[0]
|
||||
if (node?.widgets_values && !Array.isArray(node.widgets_values)) {
|
||||
expect(node.widgets_values.text).toBe('updated prompt')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return false when no workflow loaded', async () => {
|
||||
const { useWorkflowStore } = await import(
|
||||
'@/platform/workflow/management/stores/workflowStore'
|
||||
)
|
||||
vi.mocked(useWorkflowStore).mockReturnValue({
|
||||
activeWorkflow: null
|
||||
} as unknown as ReturnType<typeof useWorkflowStore>)
|
||||
|
||||
const widget: PromotedWidget = {
|
||||
nodeId: 6,
|
||||
widgetName: 'text',
|
||||
displayName: 'Prompt',
|
||||
type: 'text',
|
||||
config: {}
|
||||
}
|
||||
|
||||
const result = updateWidgetValue(widget, 'new value')
|
||||
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('activateTemplate()', () => {
|
||||
it('should load and activate template', async () => {
|
||||
const mockTemplateData = { nodes: [{ id: 1 }] }
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => mockTemplateData
|
||||
})
|
||||
|
||||
const { app } = await import('@/scripts/app')
|
||||
|
||||
await activateTemplate('template-default-linear')
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'http://localhost:8188/templates/template-default-linear.json'
|
||||
)
|
||||
expect(app.loadGraphData).toHaveBeenCalledWith(mockTemplateData)
|
||||
})
|
||||
|
||||
it('should throw error when template not found in store', async () => {
|
||||
const { useLinearModeStore } = await import(
|
||||
'@/renderer/extensions/linearMode/stores/linearModeStore'
|
||||
)
|
||||
vi.mocked(useLinearModeStore).mockReturnValue({
|
||||
template: null,
|
||||
promotedWidgets: [],
|
||||
open: vi.fn()
|
||||
} as unknown as ReturnType<typeof useLinearModeStore>)
|
||||
|
||||
await expect(activateTemplate('template-default-linear')).rejects.toThrow(
|
||||
'Template not found: template-default-linear'
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw error when template ID mismatch', async () => {
|
||||
const { useLinearModeStore } = await import(
|
||||
'@/renderer/extensions/linearMode/stores/linearModeStore'
|
||||
)
|
||||
vi.mocked(useLinearModeStore).mockReturnValue({
|
||||
template: { ...mockTemplate, id: 'different-template' },
|
||||
promotedWidgets: [],
|
||||
open: vi.fn()
|
||||
} as unknown as ReturnType<typeof useLinearModeStore>)
|
||||
|
||||
await expect(activateTemplate('template-default-linear')).rejects.toThrow(
|
||||
'Template not found: template-default-linear'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('initializeLinearMode()', () => {
|
||||
it('should open Linear Mode and activate template', async () => {
|
||||
const mockTemplateData = { nodes: [{ id: 1 }] }
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => mockTemplateData
|
||||
})
|
||||
|
||||
const { useLinearModeStore } = await import(
|
||||
'@/renderer/extensions/linearMode/stores/linearModeStore'
|
||||
)
|
||||
const { app } = await import('@/scripts/app')
|
||||
const mockOpen = vi.fn()
|
||||
vi.mocked(useLinearModeStore).mockReturnValue({
|
||||
template: mockTemplate,
|
||||
promotedWidgets: mockTemplate.promotedWidgets,
|
||||
open: mockOpen
|
||||
} as unknown as ReturnType<typeof useLinearModeStore>)
|
||||
|
||||
await initializeLinearMode('template-default-linear')
|
||||
|
||||
expect(mockOpen).toHaveBeenCalledWith('template-default-linear')
|
||||
expect(app.loadGraphData).toHaveBeenCalledWith(mockTemplateData)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,311 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useLinearModeStore } from '@/renderer/extensions/linearMode/stores/linearModeStore'
|
||||
import type { OutputImage } from '@/renderer/extensions/linearMode/linearModeTypes'
|
||||
|
||||
vi.mock('@/renderer/extensions/linearMode/linearModeConfig', () => ({
|
||||
getTemplateConfig: vi.fn((id: string) => {
|
||||
if (id === 'template-default-linear') {
|
||||
return {
|
||||
id: 'template-default-linear',
|
||||
name: 'Linear Mode Template',
|
||||
templatePath: '/templates/template-default-linear.json',
|
||||
promotedWidgets: [
|
||||
{
|
||||
nodeId: 6,
|
||||
widgetName: 'text',
|
||||
displayName: 'Prompt',
|
||||
type: 'text',
|
||||
config: { multiline: true },
|
||||
group: 'content'
|
||||
},
|
||||
{
|
||||
nodeId: 3,
|
||||
widgetName: 'seed',
|
||||
displayName: 'Seed',
|
||||
type: 'number',
|
||||
config: { min: 0 },
|
||||
group: 'generation'
|
||||
}
|
||||
],
|
||||
description: 'Default template',
|
||||
tags: ['text-to-image']
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({
|
||||
useWorkflowStore: () => ({
|
||||
activeWorkflow: {
|
||||
activeState: {
|
||||
nodes: [
|
||||
{ id: 6, widgets_values: { text: 'test prompt' } },
|
||||
{ id: 3, widgets_values: { seed: 12345 } }
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
interface MockTaskItem {
|
||||
promptId: string
|
||||
status?: string
|
||||
}
|
||||
|
||||
vi.mock('@/stores/queueStore', () => {
|
||||
return {
|
||||
useQueueStore: () => ({
|
||||
pendingTasks: [
|
||||
{ promptId: 'prompt-1' },
|
||||
{ promptId: 'prompt-2' }
|
||||
] as MockTaskItem[],
|
||||
historyTasks: [
|
||||
{ promptId: 'prompt-1', status: 'completed' },
|
||||
{ promptId: 'prompt-2', status: 'completed' },
|
||||
{ promptId: 'prompt-3', status: 'completed' }
|
||||
] as MockTaskItem[]
|
||||
}),
|
||||
TaskItemImpl: class {}
|
||||
}
|
||||
})
|
||||
|
||||
describe('useLinearModeStore', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
})
|
||||
|
||||
describe('initial state', () => {
|
||||
it('should have correct default state', () => {
|
||||
const store = useLinearModeStore()
|
||||
|
||||
expect(store.isOpen).toBe(false)
|
||||
expect(store.templateId).toBe(null)
|
||||
expect(store.currentOutput).toBe(null)
|
||||
expect(store.generatedPromptIds).toBeInstanceOf(Set)
|
||||
expect(store.generatedPromptIds.size).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('open()', () => {
|
||||
it('should open Linear Mode with valid template', () => {
|
||||
const store = useLinearModeStore()
|
||||
|
||||
store.open('template-default-linear')
|
||||
|
||||
expect(store.isOpen).toBe(true)
|
||||
expect(store.templateId).toBe('template-default-linear')
|
||||
})
|
||||
|
||||
it('should throw error for invalid template ID', () => {
|
||||
const store = useLinearModeStore()
|
||||
|
||||
expect(() => store.open('invalid-template')).toThrow(
|
||||
'Invalid template ID: invalid-template'
|
||||
)
|
||||
})
|
||||
|
||||
it('should not affect other state when opening', () => {
|
||||
const store = useLinearModeStore()
|
||||
store.trackGeneratedPrompt('test-prompt-1')
|
||||
|
||||
store.open('template-default-linear')
|
||||
|
||||
expect(store.generatedPromptIds.has('test-prompt-1')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('close()', () => {
|
||||
it('should close Linear Mode', () => {
|
||||
const store = useLinearModeStore()
|
||||
store.open('template-default-linear')
|
||||
|
||||
store.close()
|
||||
|
||||
expect(store.isOpen).toBe(false)
|
||||
})
|
||||
|
||||
it('should preserve state when closing', () => {
|
||||
const store = useLinearModeStore()
|
||||
store.open('template-default-linear')
|
||||
store.trackGeneratedPrompt('test-prompt')
|
||||
|
||||
store.close()
|
||||
|
||||
expect(store.templateId).toBe('template-default-linear')
|
||||
expect(store.generatedPromptIds.has('test-prompt')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('setOutput()', () => {
|
||||
it('should set current output', () => {
|
||||
const store = useLinearModeStore()
|
||||
const output: OutputImage = {
|
||||
filename: 'test.png',
|
||||
subfolder: 'output',
|
||||
type: 'output',
|
||||
prompt_id: 'prompt-123'
|
||||
}
|
||||
|
||||
store.setOutput(output)
|
||||
|
||||
expect(store.currentOutput).toEqual(output)
|
||||
})
|
||||
|
||||
it('should clear current output when null', () => {
|
||||
const store = useLinearModeStore()
|
||||
const output: OutputImage = {
|
||||
filename: 'test.png',
|
||||
subfolder: 'output',
|
||||
type: 'output',
|
||||
prompt_id: 'prompt-123'
|
||||
}
|
||||
store.setOutput(output)
|
||||
|
||||
store.setOutput(null)
|
||||
|
||||
expect(store.currentOutput).toBe(null)
|
||||
})
|
||||
})
|
||||
|
||||
describe('trackGeneratedPrompt()', () => {
|
||||
it('should add prompt ID to set', () => {
|
||||
const store = useLinearModeStore()
|
||||
|
||||
store.trackGeneratedPrompt('prompt-1')
|
||||
|
||||
expect(store.generatedPromptIds.has('prompt-1')).toBe(true)
|
||||
expect(store.generatedPromptIds.size).toBe(1)
|
||||
})
|
||||
|
||||
it('should handle multiple prompt IDs', () => {
|
||||
const store = useLinearModeStore()
|
||||
|
||||
store.trackGeneratedPrompt('prompt-1')
|
||||
store.trackGeneratedPrompt('prompt-2')
|
||||
store.trackGeneratedPrompt('prompt-3')
|
||||
|
||||
expect(store.generatedPromptIds.size).toBe(3)
|
||||
expect(store.generatedPromptIds.has('prompt-1')).toBe(true)
|
||||
expect(store.generatedPromptIds.has('prompt-2')).toBe(true)
|
||||
expect(store.generatedPromptIds.has('prompt-3')).toBe(true)
|
||||
})
|
||||
|
||||
it('should not duplicate prompt IDs', () => {
|
||||
const store = useLinearModeStore()
|
||||
|
||||
store.trackGeneratedPrompt('prompt-1')
|
||||
store.trackGeneratedPrompt('prompt-1')
|
||||
|
||||
expect(store.generatedPromptIds.size).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('reset()', () => {
|
||||
it('should reset all state', () => {
|
||||
const store = useLinearModeStore()
|
||||
store.open('template-default-linear')
|
||||
store.trackGeneratedPrompt('prompt-1')
|
||||
store.setOutput({
|
||||
filename: 'test.png',
|
||||
subfolder: 'output',
|
||||
type: 'output',
|
||||
prompt_id: 'prompt-1'
|
||||
})
|
||||
|
||||
store.reset()
|
||||
|
||||
expect(store.isOpen).toBe(false)
|
||||
expect(store.templateId).toBe(null)
|
||||
expect(store.currentOutput).toBe(null)
|
||||
expect(store.generatedPromptIds.size).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('template getter', () => {
|
||||
it('should return null when no template is selected', () => {
|
||||
const store = useLinearModeStore()
|
||||
|
||||
expect(store.template).toBe(null)
|
||||
})
|
||||
|
||||
it('should return template config when template is selected', () => {
|
||||
const store = useLinearModeStore()
|
||||
store.open('template-default-linear')
|
||||
|
||||
expect(store.template).not.toBe(null)
|
||||
expect(store.template?.id).toBe('template-default-linear')
|
||||
expect(store.template?.name).toBe('Linear Mode Template')
|
||||
})
|
||||
})
|
||||
|
||||
describe('promotedWidgets getter', () => {
|
||||
it('should return empty array when no template selected', () => {
|
||||
const store = useLinearModeStore()
|
||||
|
||||
expect(store.promotedWidgets).toEqual([])
|
||||
})
|
||||
|
||||
it('should return promoted widgets from template', () => {
|
||||
const store = useLinearModeStore()
|
||||
store.open('template-default-linear')
|
||||
|
||||
expect(store.promotedWidgets.length).toBe(2)
|
||||
expect(store.promotedWidgets[0].displayName).toBe('Prompt')
|
||||
expect(store.promotedWidgets[1].displayName).toBe('Seed')
|
||||
})
|
||||
})
|
||||
|
||||
describe('currentWorkflow getter', () => {
|
||||
it('should return workflow from workflowStore', () => {
|
||||
const store = useLinearModeStore()
|
||||
|
||||
expect(store.currentWorkflow).not.toBe(null)
|
||||
expect(store.currentWorkflow?.nodes).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('filteredHistory getter', () => {
|
||||
it('should return empty array when no prompts tracked', () => {
|
||||
const store = useLinearModeStore()
|
||||
|
||||
expect(store.filteredHistory).toEqual([])
|
||||
})
|
||||
|
||||
it('should filter history by tracked prompt IDs', () => {
|
||||
const store = useLinearModeStore()
|
||||
store.trackGeneratedPrompt('prompt-1')
|
||||
store.trackGeneratedPrompt('prompt-3')
|
||||
|
||||
const filtered = store.filteredHistory as unknown as MockTaskItem[]
|
||||
|
||||
expect(filtered.length).toBe(2)
|
||||
expect(filtered.some((item) => item.promptId === 'prompt-1')).toBe(true)
|
||||
expect(filtered.some((item) => item.promptId === 'prompt-3')).toBe(true)
|
||||
expect(filtered.some((item) => item.promptId === 'prompt-2')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isGenerating getter', () => {
|
||||
it('should return false when no prompts are tracked', () => {
|
||||
const store = useLinearModeStore()
|
||||
|
||||
expect(store.isGenerating).toBe(false)
|
||||
})
|
||||
|
||||
it('should return true when tracked prompt is in queue', () => {
|
||||
const store = useLinearModeStore()
|
||||
store.trackGeneratedPrompt('prompt-1')
|
||||
|
||||
expect(store.isGenerating).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false when tracked prompt is not in queue', () => {
|
||||
const store = useLinearModeStore()
|
||||
store.trackGeneratedPrompt('prompt-999')
|
||||
|
||||
expect(store.isGenerating).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user