Files
ComfyUI_frontend/tests-ui/tests/composables/node/useNodePricing.test.ts
Alexander Piskun f8912ebaf4 feat(api-nodes-pricing): add pricing for gemini-3-pro-preview model (#6735)
## Summary

Pricing is a little bit higher then for `2.5` models:

https://cloud.google.com/vertex-ai/generative-ai/pricing#gemini-models-3

## Screenshots (if applicable)

<img width="1202" height="821" alt="Screenshot From 2025-11-18 19-28-21"
src="https://github.com/user-attachments/assets/c4a279f3-8981-4424-93c5-efa7b68f0578"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6735-feat-api-nodes-pricing-add-pricing-for-gemini-3-pro-preview-model-2af6d73d36508196afaedaf1eeb21c52)
by [Unito](https://www.unito.io)
2025-11-18 14:48:09 -08:00

2247 lines
79 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { useNodePricing } from '@/composables/node/useNodePricing'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { IComboWidget } from '@/lib/litegraph/src/types/widgets'
// Helper function to create a mock node
function createMockNode(
nodeTypeName: string,
widgets: Array<{ name: string; value: any }> = [],
isApiNode = true,
inputs: Array<{
name: string
connected?: boolean
useLinksArray?: boolean
}> = []
): LGraphNode {
const mockWidgets = widgets.map(({ name, value }) => ({
name,
value,
type: 'combo'
})) as IComboWidget[]
const mockInputs =
inputs.length > 0
? inputs.map(({ name, connected, useLinksArray }) =>
useLinksArray
? { name, links: connected ? [1] : [] }
: { name, link: connected ? 1 : null }
)
: undefined
const node: any = {
id: Math.random().toString(),
widgets: mockWidgets,
constructor: {
nodeData: {
name: nodeTypeName,
api_node: isApiNode
}
}
}
if (mockInputs) {
node.inputs = mockInputs
// Provide the common helpers some frontend code may call
node.findInputSlot = function (portName: string) {
return this.inputs?.findIndex((i: any) => i.name === portName) ?? -1
}
node.isInputConnected = function (idx: number) {
const port = this.inputs?.[idx]
if (!port) return false
if (typeof port.link !== 'undefined') return port.link != null
if (Array.isArray(port.links)) return port.links.length > 0
return false
}
}
return node as LGraphNode
}
describe('useNodePricing', () => {
describe('static pricing', () => {
it('should return static price for FluxProCannyNode', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('FluxProCannyNode')
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.05/Run')
})
it('should return static price for StabilityStableImageUltraNode', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('StabilityStableImageUltraNode')
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.08/Run')
})
it('should return empty string for non-API nodes', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('RegularNode', [], false)
const price = getNodeDisplayPrice(node)
expect(price).toBe('')
})
it('should return empty string for unknown node types', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('UnknownAPINode')
const price = getNodeDisplayPrice(node)
expect(price).toBe('')
})
})
describe('dynamic pricing - KlingTextToVideoNode', () => {
it('should return high price for kling-v2-1-master model', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('KlingTextToVideoNode', [
{ name: 'mode', value: 'standard / 5s / v2-1-master' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$1.40/Run')
})
it('should return high price for kling-v2-master model', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('KlingTextToVideoNode', [
{ name: 'mode', value: 'standard / 5s / v2-master' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$1.40/Run')
})
it('should return low price for kling-v2-turbo model', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('KlingTextToVideoNode', [
{ name: 'mode', value: 'pro / 5s / v2-5-turbo' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.35/Run')
})
it('should return high price for kling-v2-turbo model', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('KlingTextToVideoNode', [
{ name: 'mode', value: 'pro / 10s / v2-5-turbo' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.70/Run')
})
it('should return standard price for kling-v1-6 model', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('KlingTextToVideoNode', [
{ name: 'mode', value: 'standard / 5s / v1-6' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.28/Run')
})
it('should return range when mode widget is missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('KlingTextToVideoNode', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.14-2.80/Run (varies with model, mode & duration)')
})
})
describe('dynamic pricing - KlingImage2VideoNode', () => {
it('should return high price for kling-v2-master model', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('KlingImage2VideoNode', [
{ name: 'model_name', value: 'v2-master' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$1.40/Run')
})
it('should return high price for kling-v2-1-master model', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('KlingImage2VideoNode', [
{ name: 'model_name', value: 'v2-1-master' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$1.40/Run')
})
it('should return high price for kling-v2-5-turbo model', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('KlingImage2VideoNode', [
{ name: 'model_name', value: 'v2-5-turbo' },
{ name: 'mode', value: 'pro mode / 10s duration / kling-v2-5-turbo' },
{ name: 'duration', value: '10' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.70/Run')
})
it('should return standard price for kling-v1-6 model', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('KlingImage2VideoNode', [
{ name: 'model_name', value: 'v1-6' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.28/Run')
})
it('should return range when model_name widget is missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('KlingImage2VideoNode', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.14-2.80/Run (varies with model, mode & duration)')
})
})
describe('dynamic pricing - OpenAIDalle3', () => {
it('should return $0.04 for 1024x1024 standard quality', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIDalle3', [
{ name: 'size', value: '1024x1024' },
{ name: 'quality', value: 'standard' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.04/Run')
})
it('should return $0.08 for 1024x1024 hd quality', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIDalle3', [
{ name: 'size', value: '1024x1024' },
{ name: 'quality', value: 'hd' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.08/Run')
})
it('should return $0.08 for 1792x1024 standard quality', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIDalle3', [
{ name: 'size', value: '1792x1024' },
{ name: 'quality', value: 'standard' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.08/Run')
})
it('should return $0.16 for 1792x1024 hd quality', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIDalle3', [
{ name: 'size', value: '1792x1024' },
{ name: 'quality', value: 'hd' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.12/Run')
})
it('should return $0.08 for 1024x1792 standard quality', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIDalle3', [
{ name: 'size', value: '1024x1792' },
{ name: 'quality', value: 'standard' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.08/Run')
})
it('should return $0.16 for 1024x1792 hd quality', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIDalle3', [
{ name: 'size', value: '1024x1792' },
{ name: 'quality', value: 'hd' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.12/Run')
})
it('should return range when widgets are missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIDalle3', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.04-0.12/Run (varies with size & quality)')
})
it('should return range when size widget is missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIDalle3', [
{ name: 'quality', value: 'hd' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.04-0.12/Run (varies with size & quality)')
})
it('should return range when quality widget is missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIDalle3', [
{ name: 'size', value: '1024x1024' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.04-0.12/Run (varies with size & quality)')
})
})
// ============================== OpenAIVideoSora2 ==============================
describe('dynamic pricing - OpenAIVideoSora2', () => {
it('should require model, duration & size when widgets are missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [])
expect(getNodeDisplayPrice(node)).toBe('Set model, duration & size')
})
it('should require duration when duration is invalid or zero', () => {
const { getNodeDisplayPrice } = useNodePricing()
const nodeNaN = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2-pro' },
{ name: 'duration', value: 'oops' },
{ name: 'size', value: '720x1280' }
])
expect(getNodeDisplayPrice(nodeNaN)).toBe('Set model, duration & size')
const nodeZero = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2-pro' },
{ name: 'duration', value: 0 },
{ name: 'size', value: '720x1280' }
])
expect(getNodeDisplayPrice(nodeZero)).toBe('Set model, duration & size')
})
it('should require size when size is missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2-pro' },
{ name: 'duration', value: 8 }
])
expect(getNodeDisplayPrice(node)).toBe('Set model, duration & size')
})
it('should compute pricing for sora-2-pro with 1024x1792', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2-pro' },
{ name: 'duration', value: 8 },
{ name: 'size', value: '1024x1792' }
])
expect(getNodeDisplayPrice(node)).toBe('$4.00/Run') // 0.5 * 8
})
it('should compute pricing for sora-2-pro with 720x1280', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2-pro' },
{ name: 'duration', value: 12 },
{ name: 'size', value: '720x1280' }
])
expect(getNodeDisplayPrice(node)).toBe('$3.60/Run') // 0.3 * 12
})
it('should reject unsupported size for sora-2-pro', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2-pro' },
{ name: 'duration', value: 8 },
{ name: 'size', value: '640x640' }
])
expect(getNodeDisplayPrice(node)).toBe(
'Invalid size. Must be 720x1280, 1280x720, 1024x1792, or 1792x1024.'
)
})
it('should compute pricing for sora-2 (720x1280 only)', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2' },
{ name: 'duration', value: 10 },
{ name: 'size', value: '720x1280' }
])
expect(getNodeDisplayPrice(node)).toBe('$1.00/Run') // 0.1 * 10
})
it('should reject non-720 sizes for sora-2', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2' },
{ name: 'duration', value: 8 },
{ name: 'size', value: '1024x1792' }
])
expect(getNodeDisplayPrice(node)).toBe(
'sora-2 supports only 720x1280 or 1280x720'
)
})
it('should accept duration_s alias for duration', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'sora-2-pro' },
{ name: 'duration_s', value: 4 },
{ name: 'size', value: '1792x1024' }
])
expect(getNodeDisplayPrice(node)).toBe('$2.00/Run') // 0.5 * 4
})
it('should be case-insensitive for model and size', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIVideoSora2', [
{ name: 'model', value: 'SoRa-2-PrO' },
{ name: 'duration', value: 12 },
{ name: 'size', value: '1280x720' }
])
expect(getNodeDisplayPrice(node)).toBe('$3.60/Run') // 0.3 * 12
})
})
// ============================== MinimaxHailuoVideoNode ==============================
describe('dynamic pricing - MinimaxHailuoVideoNode', () => {
it('should return $0.28 for 6s duration and 768P resolution', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('MinimaxHailuoVideoNode', [
{ name: 'duration', value: '6' },
{ name: 'resolution', value: '768P' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.28/Run')
})
it('should return $0.60 for 10s duration and 768P resolution', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('MinimaxHailuoVideoNode', [
{ name: 'duration', value: '10' },
{ name: 'resolution', value: '768P' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.56/Run')
})
it('should return $0.49 for 6s duration and 1080P resolution', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('MinimaxHailuoVideoNode', [
{ name: 'duration', value: '6' },
{ name: 'resolution', value: '1080P' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.49/Run')
})
it('should return range when duration widget is missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('MinimaxHailuoVideoNode', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.28-0.56/Run (varies with resolution & duration)')
})
})
describe('dynamic pricing - OpenAIDalle2', () => {
it('should return $0.02 for 1024x1024 size', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIDalle2', [
{ name: 'size', value: '1024x1024' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.020/Run')
})
it('should return $0.018 for 512x512 size', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIDalle2', [
{ name: 'size', value: '512x512' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.018/Run')
})
it('should return $0.016 for 256x256 size', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIDalle2', [
{ name: 'size', value: '256x256' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.016/Run')
})
it('should return range when size widget is missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIDalle2', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.016-0.02 x n/Run (varies with size & n)')
})
})
describe('dynamic pricing - OpenAIGPTImage1', () => {
it('should return high price range for high quality', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIGPTImage1', [
{ name: 'quality', value: 'high' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.167-0.30/Run')
})
it('should return medium price range for medium quality', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIGPTImage1', [
{ name: 'quality', value: 'medium' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.046-0.07/Run')
})
it('should return low price range for low quality', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIGPTImage1', [
{ name: 'quality', value: 'low' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.011-0.02/Run')
})
it('should return range when quality widget is missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIGPTImage1', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.011-0.30 x n/Run (varies with quality & n)')
})
})
describe('dynamic pricing - IdeogramV3', () => {
it('should return correct prices for IdeogramV3 node', () => {
const { getNodeDisplayPrice } = useNodePricing()
const testCases = [
{
rendering_speed: 'Quality',
character_image: false,
expected: '$0.09/Run'
},
{
rendering_speed: 'Quality',
character_image: true,
expected: '$0.20/Run'
},
{
rendering_speed: 'Default',
character_image: false,
expected: '$0.06/Run'
},
{
rendering_speed: 'Default',
character_image: true,
expected: '$0.15/Run'
},
{
rendering_speed: 'Turbo',
character_image: false,
expected: '$0.03/Run'
},
{
rendering_speed: 'Turbo',
character_image: true,
expected: '$0.10/Run'
}
]
testCases.forEach(({ rendering_speed, character_image, expected }) => {
const node = createMockNode(
'IdeogramV3',
[{ name: 'rendering_speed', value: rendering_speed }],
true,
[{ name: 'character_image', connected: character_image }]
)
expect(getNodeDisplayPrice(node)).toBe(expected)
})
})
it('should return range when rendering_speed widget is missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('IdeogramV3', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe(
'$0.03-0.08 x num_images/Run (varies with rendering speed & num_images)'
)
})
it('should multiply price by num_images for Quality rendering speed', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('IdeogramV3', [
{ name: 'rendering_speed', value: 'Quality' },
{ name: 'num_images', value: 3 }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.27/Run') // 0.09 * 3
})
it('should multiply price by num_images for Turbo rendering speed', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('IdeogramV3', [
{ name: 'rendering_speed', value: 'Turbo' },
{ name: 'num_images', value: 5 }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.15/Run') // 0.03 * 5
})
})
describe('dynamic pricing - VeoVideoGenerationNode', () => {
it('should return $5.00 for 10s duration', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('VeoVideoGenerationNode', [
{ name: 'duration_seconds', value: '10' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$5.00/Run')
})
it('should return $2.50 for 5s duration', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('VeoVideoGenerationNode', [
{ name: 'duration_seconds', value: '5' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$2.50/Run')
})
it('should return range when duration widget is missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('VeoVideoGenerationNode', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$2.50-5.0/Run (varies with duration)')
})
})
describe('dynamic pricing - Veo3VideoGenerationNode', () => {
it('should return $0.80 for veo-3.0-fast-generate-001 without audio', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('Veo3VideoGenerationNode', [
{ name: 'model', value: 'veo-3.0-fast-generate-001' },
{ name: 'generate_audio', value: false }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.80/Run')
})
it('should return $1.20 for veo-3.0-fast-generate-001 with audio', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('Veo3VideoGenerationNode', [
{ name: 'model', value: 'veo-3.0-fast-generate-001' },
{ name: 'generate_audio', value: true }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$1.20/Run')
})
it('should return $1.60 for veo-3.0-generate-001 without audio', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('Veo3VideoGenerationNode', [
{ name: 'model', value: 'veo-3.0-generate-001' },
{ name: 'generate_audio', value: false }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$1.60/Run')
})
it('should return $3.20 for veo-3.0-generate-001 with audio', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('Veo3VideoGenerationNode', [
{ name: 'model', value: 'veo-3.0-generate-001' },
{ name: 'generate_audio', value: true }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$3.20/Run')
})
it('should return range when widgets are missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('Veo3VideoGenerationNode', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe(
'$0.80-3.20/Run (varies with model & audio generation)'
)
})
it('should return range when only model widget is present', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('Veo3VideoGenerationNode', [
{ name: 'model', value: 'veo-3.0-generate-001' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe(
'$0.80-3.20/Run (varies with model & audio generation)'
)
})
it('should return range when only generate_audio widget is present', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('Veo3VideoGenerationNode', [
{ name: 'generate_audio', value: true }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe(
'$0.80-3.20/Run (varies with model & audio generation)'
)
})
})
describe('dynamic pricing - LumaVideoNode', () => {
it('should return $2.19 for ray-flash-2 4K 5s', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('LumaVideoNode', [
{ name: 'model', value: 'ray-flash-2' },
{ name: 'resolution', value: '4K' },
{ name: 'duration', value: '5s' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$2.19/Run')
})
it('should return $6.37 for ray-2 4K 5s', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('LumaVideoNode', [
{ name: 'model', value: 'ray-2' },
{ name: 'resolution', value: '4K' },
{ name: 'duration', value: '5s' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$6.37/Run')
})
it('should return $0.35 for ray-1-6 model', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('LumaVideoNode', [
{ name: 'model', value: 'ray-1-6' },
{ name: 'resolution', value: '1080p' },
{ name: 'duration', value: '5s' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.35/Run')
})
it('should return range when widgets are missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('LumaVideoNode', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe(
'$0.14-11.47/Run (varies with model, resolution & duration)'
)
})
})
describe('dynamic pricing - PixverseTextToVideoNode', () => {
it('should return range for 5s 1080p quality', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('PixverseTextToVideoNode', [
{ name: 'duration', value: '5s' },
{ name: 'quality', value: '1080p' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe(
'$0.45-1.2/Run (varies with duration, quality & motion mode)'
)
})
it('should return range for 5s 540p normal quality', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('PixverseTextToVideoNode', [
{ name: 'duration', value: '5s' },
{ name: 'quality', value: '540p' },
{ name: 'motion_mode', value: 'normal' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe(
'$0.45-1.2/Run (varies with duration, quality & motion mode)'
)
})
it('should return range when widgets are missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('PixverseTextToVideoNode', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe(
'$0.45-1.2/Run (varies with duration, quality & motion mode)'
)
})
})
describe('dynamic pricing - KlingDualCharacterVideoEffectNode', () => {
it('should return range for v2-master 5s mode', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('KlingDualCharacterVideoEffectNode', [
{ name: 'mode', value: 'standard / 5s / v2-master' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.14-2.80/Run (varies with model, mode & duration)')
})
it('should return range for v1-6 5s mode', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('KlingDualCharacterVideoEffectNode', [
{ name: 'mode', value: 'standard / 5s / v1-6' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.14-2.80/Run (varies with model, mode & duration)')
})
it('should return range when mode widget is missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('KlingDualCharacterVideoEffectNode', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.14-2.80/Run (varies with model, mode & duration)')
})
})
describe('dynamic pricing - KlingSingleImageVideoEffectNode', () => {
it('should return $0.28 for fuzzyfuzzy effect', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('KlingSingleImageVideoEffectNode', [
{ name: 'effect_scene', value: 'fuzzyfuzzy' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.28/Run')
})
it('should return $0.49 for dizzydizzy effect', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('KlingSingleImageVideoEffectNode', [
{ name: 'effect_scene', value: 'dizzydizzy' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.49/Run')
})
it('should return range when effect_scene widget is missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('KlingSingleImageVideoEffectNode', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.28-0.49/Run (varies with effect scene)')
})
})
describe('dynamic pricing - PikaImageToVideoNode2_2', () => {
it('should return $0.45 for 5s 1080p', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('PikaImageToVideoNode2_2', [
{ name: 'duration', value: '5s' },
{ name: 'resolution', value: '1080p' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.45/Run')
})
it('should return $0.2 for 5s 720p', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('PikaImageToVideoNode2_2', [
{ name: 'duration', value: '5s' },
{ name: 'resolution', value: '720p' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.2/Run')
})
it('should return $1.0 for 10s 1080p', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('PikaImageToVideoNode2_2', [
{ name: 'duration', value: '10s' },
{ name: 'resolution', value: '1080p' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$1.0/Run')
})
it('should return range when widgets are missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('PikaImageToVideoNode2_2', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.2-1.0/Run (varies with duration & resolution)')
})
})
describe('dynamic pricing - PikaScenesV2_2', () => {
it('should return $0.3 for 5s 720p', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('PikaScenesV2_2', [
{ name: 'duration', value: '5s' },
{ name: 'resolution', value: '720p' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.3/Run')
})
it('should return $0.25 for 10s 720p', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('PikaScenesV2_2', [
{ name: 'duration', value: '10s' },
{ name: 'resolution', value: '720p' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.4/Run')
})
it('should return range when widgets are missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('PikaScenesV2_2', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.2-1.0/Run (varies with duration & resolution)')
})
})
describe('dynamic pricing - PikaStartEndFrameNode2_2', () => {
it('should return $0.2 for 5s 720p', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('PikaStartEndFrameNode2_2', [
{ name: 'duration', value: '5s' },
{ name: 'resolution', value: '720p' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.2/Run')
})
it('should return $1.0 for 10s 1080p', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('PikaStartEndFrameNode2_2', [
{ name: 'duration', value: '10s' },
{ name: 'resolution', value: '1080p' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$1.0/Run')
})
it('should return range when widgets are missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('PikaStartEndFrameNode2_2', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.2-1.0/Run (varies with duration & resolution)')
})
})
describe('error handling', () => {
it('should gracefully handle errors in dynamic pricing functions', () => {
const { getNodeDisplayPrice } = useNodePricing()
// Create a node with malformed widget data that could cause errors
const node = {
id: 'test-node',
widgets: null, // This could cause errors when accessing .find()
constructor: {
nodeData: {
name: 'KlingTextToVideoNode',
api_node: true
}
}
} as unknown as LGraphNode
// Should not throw an error and return empty string as fallback
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.14-2.80/Run (varies with model, mode & duration)')
})
it('should handle completely broken widget structure', () => {
const { getNodeDisplayPrice } = useNodePricing()
// Create a node with no widgets property at all
const node = {
id: 'test-node',
// No widgets property
constructor: {
nodeData: {
name: 'OpenAIDalle3',
api_node: true
}
}
} as unknown as LGraphNode
// Should gracefully fall back to the default range
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.04-0.12/Run (varies with size & quality)')
})
})
describe('helper methods', () => {
describe('getNodePricingConfig', () => {
it('should return pricing config for known API nodes', () => {
const { getNodePricingConfig } = useNodePricing()
const node = createMockNode('KlingTextToVideoNode')
const config = getNodePricingConfig(node)
expect(config).toBeDefined()
expect(typeof config.displayPrice).toBe('function')
})
it('should return undefined for unknown nodes', () => {
const { getNodePricingConfig } = useNodePricing()
const node = createMockNode('UnknownNode')
const config = getNodePricingConfig(node)
expect(config).toBeUndefined()
})
})
describe('getRelevantWidgetNames', () => {
it('should return correct widget names for KlingTextToVideoNode', () => {
const { getRelevantWidgetNames } = useNodePricing()
const widgetNames = getRelevantWidgetNames('KlingTextToVideoNode')
expect(widgetNames).toEqual(['mode', 'model_name', 'duration'])
})
it('should return correct widget names for KlingImage2VideoNode', () => {
const { getRelevantWidgetNames } = useNodePricing()
const widgetNames = getRelevantWidgetNames('KlingImage2VideoNode')
expect(widgetNames).toEqual(['mode', 'model_name', 'duration'])
})
it('should return correct widget names for OpenAIDalle3', () => {
const { getRelevantWidgetNames } = useNodePricing()
const widgetNames = getRelevantWidgetNames('OpenAIDalle3')
expect(widgetNames).toEqual(['size', 'quality'])
})
it('should return correct widget names for VeoVideoGenerationNode', () => {
const { getRelevantWidgetNames } = useNodePricing()
const widgetNames = getRelevantWidgetNames('VeoVideoGenerationNode')
expect(widgetNames).toEqual(['duration_seconds'])
})
it('should return correct widget names for Veo3VideoGenerationNode', () => {
const { getRelevantWidgetNames } = useNodePricing()
const widgetNames = getRelevantWidgetNames('Veo3VideoGenerationNode')
expect(widgetNames).toEqual(['model', 'generate_audio'])
})
it('should return correct widget names for LumaVideoNode', () => {
const { getRelevantWidgetNames } = useNodePricing()
const widgetNames = getRelevantWidgetNames('LumaVideoNode')
expect(widgetNames).toEqual(['model', 'resolution', 'duration'])
})
it('should return correct widget names for KlingSingleImageVideoEffectNode', () => {
const { getRelevantWidgetNames } = useNodePricing()
const widgetNames = getRelevantWidgetNames(
'KlingSingleImageVideoEffectNode'
)
expect(widgetNames).toEqual(['effect_scene'])
})
it('should return correct widget names for PikaImageToVideoNode2_2', () => {
const { getRelevantWidgetNames } = useNodePricing()
const widgetNames = getRelevantWidgetNames('PikaImageToVideoNode2_2')
expect(widgetNames).toEqual(['duration', 'resolution'])
})
it('should return empty array for unknown node types', () => {
const { getRelevantWidgetNames } = useNodePricing()
const widgetNames = getRelevantWidgetNames('UnknownNode')
expect(widgetNames).toEqual([])
})
describe('Ideogram nodes with num_images parameter', () => {
it('should return correct widget names for IdeogramV1', () => {
const { getRelevantWidgetNames } = useNodePricing()
const widgetNames = getRelevantWidgetNames('IdeogramV1')
expect(widgetNames).toEqual(['num_images', 'turbo'])
})
it('should return correct widget names for IdeogramV2', () => {
const { getRelevantWidgetNames } = useNodePricing()
const widgetNames = getRelevantWidgetNames('IdeogramV2')
expect(widgetNames).toEqual(['num_images', 'turbo'])
})
it('should return correct widget names for IdeogramV3', () => {
const { getRelevantWidgetNames } = useNodePricing()
const widgetNames = getRelevantWidgetNames('IdeogramV3')
expect(widgetNames).toEqual([
'rendering_speed',
'num_images',
'character_image'
])
})
})
describe('Recraft nodes with n parameter', () => {
it('should return correct widget names for RecraftTextToImageNode', () => {
const { getRelevantWidgetNames } = useNodePricing()
const widgetNames = getRelevantWidgetNames('RecraftTextToImageNode')
expect(widgetNames).toEqual(['n'])
})
it('should return correct widget names for RecraftTextToVectorNode', () => {
const { getRelevantWidgetNames } = useNodePricing()
const widgetNames = getRelevantWidgetNames('RecraftTextToVectorNode')
expect(widgetNames).toEqual(['n'])
})
})
})
describe('Ideogram nodes dynamic pricing', () => {
it('should calculate dynamic pricing for IdeogramV1 based on num_images value', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('IdeogramV1', [
{ name: 'num_images', value: 3 }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.18/Run') // 0.06 * 3
})
it('should calculate dynamic pricing for IdeogramV2 based on num_images value', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('IdeogramV2', [
{ name: 'num_images', value: 4 }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.32/Run') // 0.08 * 4
})
it('should fall back to static display when num_images widget is missing for IdeogramV1', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('IdeogramV1', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.02-0.06 x num_images/Run')
})
it('should fall back to static display when num_images widget is missing for IdeogramV2', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('IdeogramV2', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.05-0.08 x num_images/Run')
})
it('should handle edge case when num_images value is 1 for IdeogramV1', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('IdeogramV1', [
{ name: 'num_images', value: 1 }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.06/Run') // 0.06 * 1 (turbo=false by default)
})
})
describe('Recraft nodes dynamic pricing', () => {
it('should calculate dynamic pricing for RecraftTextToImageNode based on n value', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('RecraftTextToImageNode', [
{ name: 'n', value: 3 }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.12/Run') // 0.04 * 3
})
it('should calculate dynamic pricing for RecraftTextToVectorNode based on n value', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('RecraftTextToVectorNode', [
{ name: 'n', value: 2 }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.16/Run') // 0.08 * 2
})
it('should fall back to static display when n widget is missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('RecraftTextToImageNode', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.04 x n/Run')
})
it('should handle edge case when n value is 1', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('RecraftImageInpaintingNode', [
{ name: 'n', value: 1 }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.04/Run') // 0.04 * 1
})
})
})
describe('OpenAI nodes dynamic pricing with n parameter', () => {
it('should calculate dynamic pricing for OpenAIDalle2 based on size and n', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIDalle2', [
{ name: 'size', value: '1024x1024' },
{ name: 'n', value: 3 }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.060/Run') // 0.02 * 3
})
it('should calculate dynamic pricing for OpenAIGPTImage1 based on quality and n', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIGPTImage1', [
{ name: 'quality', value: 'low' },
{ name: 'n', value: 2 }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.011-0.02 x 2/Run')
})
it('should fall back to static display when n widget is missing for OpenAIDalle2', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIDalle2', [
{ name: 'size', value: '512x512' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.018/Run') // n defaults to 1
})
})
describe('KlingImageGenerationNode dynamic pricing with n parameter', () => {
it('should calculate dynamic pricing for text-to-image with kling-v1', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('KlingImageGenerationNode', [
{ name: 'model_name', value: 'kling-v1' },
{ name: 'n', value: 4 }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.0140/Run') // 0.0035 * 4
})
it('should calculate dynamic pricing for text-to-image with kling-v1-5', () => {
const { getNodeDisplayPrice } = useNodePricing()
// Mock node without image input (text-to-image mode)
const node = createMockNode('KlingImageGenerationNode', [
{ name: 'model_name', value: 'kling-v1-5' },
{ name: 'n', value: 2 }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.0280/Run') // For kling-v1-5 text-to-image: 0.014 * 2
})
it('should fall back to static display when model widget is missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('KlingImageGenerationNode', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.0035-0.028 x n/Run (varies with modality & model)')
})
})
describe('New Recraft nodes dynamic pricing', () => {
it('should calculate dynamic pricing for RecraftGenerateImageNode', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('RecraftGenerateImageNode', [
{ name: 'n', value: 3 }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.12/Run') // 0.04 * 3
})
it('should calculate dynamic pricing for RecraftVectorizeImageNode', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('RecraftVectorizeImageNode', [
{ name: 'n', value: 5 }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.05/Run') // 0.01 * 5
})
it('should calculate dynamic pricing for RecraftGenerateVectorImageNode', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('RecraftGenerateVectorImageNode', [
{ name: 'n', value: 2 }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.16/Run') // 0.08 * 2
})
})
describe('Widget names for reactive updates', () => {
it('should include n parameter for OpenAI nodes', () => {
const { getRelevantWidgetNames } = useNodePricing()
expect(getRelevantWidgetNames('OpenAIDalle2')).toEqual(['size', 'n'])
expect(getRelevantWidgetNames('OpenAIGPTImage1')).toEqual([
'quality',
'n'
])
})
it('should include n parameter for Kling and new Recraft nodes', () => {
const { getRelevantWidgetNames } = useNodePricing()
expect(getRelevantWidgetNames('KlingImageGenerationNode')).toEqual([
'modality',
'model_name',
'n'
])
expect(getRelevantWidgetNames('RecraftVectorizeImageNode')).toEqual(['n'])
expect(getRelevantWidgetNames('RecraftGenerateImageNode')).toEqual(['n'])
expect(getRelevantWidgetNames('RecraftGenerateVectorImageNode')).toEqual([
'n'
])
expect(
getRelevantWidgetNames('RecraftGenerateColorFromImageNode')
).toEqual(['n'])
})
it('should include relevant widget names for new nodes', () => {
const { getRelevantWidgetNames } = useNodePricing()
expect(getRelevantWidgetNames('RunwayImageToVideoNodeGen3a')).toEqual([
'duration'
])
expect(getRelevantWidgetNames('RunwayImageToVideoNodeGen4')).toEqual([
'duration'
])
expect(getRelevantWidgetNames('RunwayFirstLastFrameNode')).toEqual([
'duration'
])
expect(getRelevantWidgetNames('TripoTextToModelNode')).toEqual([
'quad',
'style',
'texture',
'texture_quality'
])
expect(getRelevantWidgetNames('TripoImageToModelNode')).toEqual([
'quad',
'style',
'texture',
'texture_quality'
])
})
})
describe('New API nodes pricing', () => {
describe('RunwayML nodes', () => {
it('should return static price for RunwayTextToImageNode', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('RunwayTextToImageNode')
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.08/Run')
})
it('should calculate dynamic pricing for RunwayImageToVideoNodeGen3a', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('RunwayImageToVideoNodeGen3a', [
{ name: 'duration', value: 10 }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.50/Run') // 0.05 * 10
})
it('should return fallback for RunwayImageToVideoNodeGen3a without duration', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('RunwayImageToVideoNodeGen3a', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.05/second')
})
it('should handle zero duration for RunwayImageToVideoNodeGen3a', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('RunwayImageToVideoNodeGen3a', [
{ name: 'duration', value: 0 }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.00/Run') // 0.05 * 0 = 0
})
it('should handle NaN duration for RunwayImageToVideoNodeGen3a', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('RunwayImageToVideoNodeGen3a', [
{ name: 'duration', value: 'invalid' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.25/Run') // Falls back to 5 seconds: 0.05 * 5
})
})
describe('Rodin nodes', () => {
it('should return base price for Rodin3D_Regular', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('Rodin3D_Regular')
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.4/Run')
})
it('should return addon price for Rodin3D_Detail', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('Rodin3D_Detail')
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.4/Run')
})
it('should return addon price for Rodin3D_Smooth', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('Rodin3D_Smooth')
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.4/Run')
})
})
describe('Tripo nodes', () => {
it('should return v2.5 standard pricing for TripoTextToModelNode', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('TripoTextToModelNode', [
{ name: 'quad', value: false },
{ name: 'style', value: 'any style' },
{ name: 'texture', value: false },
{ name: 'texture_quality', value: 'standard' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.15/Run') // any style, no quad, no texture
})
it('should return v2.5 detailed pricing for TripoTextToModelNode', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('TripoTextToModelNode', [
{ name: 'quad', value: true },
{ name: 'style', value: 'any style' },
{ name: 'texture', value: false },
{ name: 'texture_quality', value: 'detailed' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.35/Run') // any style, quad, no texture, detailed
})
it('should return v2.0 detailed pricing for TripoImageToModelNode', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('TripoImageToModelNode', [
{ name: 'quad', value: true },
{ name: 'style', value: 'any style' },
{ name: 'texture', value: false },
{ name: 'texture_quality', value: 'detailed' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.45/Run') // any style, quad, no texture, detailed
})
it('should return legacy pricing for TripoTextToModelNode', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('TripoTextToModelNode', [
{ name: 'quad', value: false },
{ name: 'style', value: 'none' },
{ name: 'texture', value: false },
{ name: 'texture_quality', value: 'standard' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.10/Run') // none style, no quad, no texture
})
it('should return static price for TripoRefineNode', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('TripoRefineNode')
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.3/Run')
})
it('should return fallback for TripoTextToModelNode without model', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('TripoTextToModelNode', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe(
'$0.1-0.4/Run (varies with quad, style, texture & quality)'
)
})
it('should return texture-based pricing for TripoTextureNode', () => {
const { getNodeDisplayPrice } = useNodePricing()
const standardNode = createMockNode('TripoTextureNode', [
{ name: 'texture_quality', value: 'standard' }
])
const detailedNode = createMockNode('TripoTextureNode', [
{ name: 'texture_quality', value: 'detailed' }
])
expect(getNodeDisplayPrice(standardNode)).toBe('$0.1/Run')
expect(getNodeDisplayPrice(detailedNode)).toBe('$0.2/Run')
})
it('should handle various Tripo parameter combinations', () => {
const { getNodeDisplayPrice } = useNodePricing()
// Test different parameter combinations
const testCases = [
{ quad: false, style: 'none', texture: false, expected: '$0.10/Run' },
{
quad: false,
style: 'any style',
texture: false,
expected: '$0.15/Run'
},
{ quad: true, style: 'none', texture: false, expected: '$0.20/Run' },
{
quad: true,
style: 'any style',
texture: false,
expected: '$0.25/Run'
}
]
testCases.forEach(({ quad, style, texture, expected }) => {
const node = createMockNode('TripoTextToModelNode', [
{ name: 'quad', value: quad },
{ name: 'style', value: style },
{ name: 'texture', value: texture },
{ name: 'texture_quality', value: 'standard' }
])
expect(getNodeDisplayPrice(node)).toBe(expected)
})
})
it('should return static price for TripoConvertModelNode', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('TripoConvertModelNode')
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.10/Run')
})
it('should return static price for TripoRetargetRiggedModelNode', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('TripoRetargetRiggedModelNode')
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.10/Run')
})
it('should return dynamic pricing for TripoMultiviewToModelNode', () => {
const { getNodeDisplayPrice } = useNodePricing()
// Test basic case - no style, no quad, no texture
const basicNode = createMockNode('TripoMultiviewToModelNode', [
{ name: 'quad', value: false },
{ name: 'style', value: 'none' },
{ name: 'texture', value: false },
{ name: 'texture_quality', value: 'standard' }
])
expect(getNodeDisplayPrice(basicNode)).toBe('$0.20/Run')
// Test high-end case - any style, quad, texture, detailed
const highEndNode = createMockNode('TripoMultiviewToModelNode', [
{ name: 'quad', value: true },
{ name: 'style', value: 'stylized' },
{ name: 'texture', value: true },
{ name: 'texture_quality', value: 'detailed' }
])
expect(getNodeDisplayPrice(highEndNode)).toBe('$0.50/Run')
})
it('should return fallback for TripoMultiviewToModelNode without widgets', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('TripoMultiviewToModelNode', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe(
'$0.2-0.5/Run (varies with quad, style, texture & quality)'
)
})
})
describe('Gemini and OpenAI Chat nodes', () => {
it('should return specific pricing for supported Gemini models', () => {
const { getNodeDisplayPrice } = useNodePricing()
const testCases = [
{
model: 'gemini-2.5-pro-preview-05-06',
expected: '$0.00125/$0.01 per 1K tokens'
},
{
model: 'gemini-2.5-pro',
expected: '$0.00125/$0.01 per 1K tokens'
},
{
model: 'gemini-3-pro-preview',
expected: '$0.002/$0.012 per 1K tokens'
},
{
model: 'gemini-2.5-flash-preview-04-17',
expected: '$0.0003/$0.0025 per 1K tokens'
},
{
model: 'gemini-2.5-flash',
expected: '$0.0003/$0.0025 per 1K tokens'
},
{ model: 'unknown-gemini-model', expected: 'Token-based' }
]
testCases.forEach(({ model, expected }) => {
const node = createMockNode('GeminiNode', [
{ name: 'model', value: model }
])
expect(getNodeDisplayPrice(node)).toBe(expected)
})
})
it('should return per-second pricing for Gemini Veo models', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('GeminiNode', [
{ name: 'model', value: 'veo-2.0-generate-001' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.5/second')
})
it('should return fallback for GeminiNode without model widget', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('GeminiNode', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe('Token-based')
})
it('should return token-based pricing for OpenAIChatNode', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIChatNode', [
{ name: 'model', value: 'unknown-model' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('Token-based')
})
it('should return correct pricing for all exposed OpenAI models', () => {
const { getNodeDisplayPrice } = useNodePricing()
const testCases = [
{ model: 'o4-mini', expected: '$0.0011/$0.0044 per 1K tokens' },
{ model: 'o1-pro', expected: '$0.15/$0.60 per 1K tokens' },
{ model: 'o1', expected: '$0.015/$0.06 per 1K tokens' },
{ model: 'o3-mini', expected: '$0.0011/$0.0044 per 1K tokens' },
{ model: 'o3', expected: '$0.01/$0.04 per 1K tokens' },
{ model: 'gpt-4o', expected: '$0.0025/$0.01 per 1K tokens' },
{ model: 'gpt-4.1-nano', expected: '$0.0001/$0.0004 per 1K tokens' },
{ model: 'gpt-4.1-mini', expected: '$0.0004/$0.0016 per 1K tokens' },
{ model: 'gpt-4.1', expected: '$0.002/$0.008 per 1K tokens' },
{ model: 'gpt-5-nano', expected: '$0.00005/$0.0004 per 1K tokens' },
{ model: 'gpt-5-mini', expected: '$0.00025/$0.002 per 1K tokens' },
{ model: 'gpt-5', expected: '$0.00125/$0.01 per 1K tokens' }
]
testCases.forEach(({ model, expected }) => {
const node = createMockNode('OpenAIChatNode', [
{ name: 'model', value: model }
])
expect(getNodeDisplayPrice(node)).toBe(expected)
})
})
it('should handle model ordering correctly (specific before general)', () => {
const { getNodeDisplayPrice } = useNodePricing()
// Test that more specific patterns are matched before general ones
const testCases = [
{
model: 'gpt-4.1-nano-test',
expected: '$0.0001/$0.0004 per 1K tokens'
},
{
model: 'gpt-4.1-mini-test',
expected: '$0.0004/$0.0016 per 1K tokens'
},
{ model: 'gpt-4.1-test', expected: '$0.002/$0.008 per 1K tokens' },
{ model: 'o1-pro-test', expected: '$0.15/$0.60 per 1K tokens' },
{ model: 'o1-test', expected: '$0.015/$0.06 per 1K tokens' },
{ model: 'o3-mini-test', expected: '$0.0011/$0.0044 per 1K tokens' },
{ model: 'unknown-model', expected: 'Token-based' }
]
testCases.forEach(({ model, expected }) => {
const node = createMockNode('OpenAIChatNode', [
{ name: 'model', value: model }
])
expect(getNodeDisplayPrice(node)).toBe(expected)
})
})
it('should return fallback for OpenAIChatNode without model widget', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('OpenAIChatNode', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe('Token-based')
})
it('should return static price for GeminiImageNode', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('GeminiImageNode')
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.03 per 1K tokens')
})
})
describe('Additional RunwayML edge cases', () => {
it('should handle edge cases for RunwayML duration-based pricing', () => {
const { getNodeDisplayPrice } = useNodePricing()
// Test edge cases
const testCases = [
{ duration: 0, expected: '$0.00/Run' }, // Now correctly handles 0 duration
{ duration: 1, expected: '$0.05/Run' },
{ duration: 30, expected: '$1.50/Run' }
]
testCases.forEach(({ duration, expected }) => {
const node = createMockNode('RunwayImageToVideoNodeGen3a', [
{ name: 'duration', value: duration }
])
expect(getNodeDisplayPrice(node)).toBe(expected)
})
})
it('should handle invalid duration values gracefully', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('RunwayImageToVideoNodeGen3a', [
{ name: 'duration', value: 'invalid-string' }
])
// When Number('invalid-string') returns NaN, it falls back to 5 seconds
expect(getNodeDisplayPrice(node)).toBe('$0.25/Run')
})
it('should handle missing duration widget gracefully', () => {
const { getNodeDisplayPrice } = useNodePricing()
const nodes = [
'RunwayImageToVideoNodeGen3a',
'RunwayImageToVideoNodeGen4',
'RunwayFirstLastFrameNode'
]
nodes.forEach((nodeType) => {
const node = createMockNode(nodeType, [])
expect(getNodeDisplayPrice(node)).toBe('$0.05/second')
})
})
})
describe('Complete Rodin node coverage', () => {
it('should return correct pricing for all Rodin variants', () => {
const { getNodeDisplayPrice } = useNodePricing()
const testCases = [
{ nodeType: 'Rodin3D_Regular', expected: '$0.4/Run' },
{ nodeType: 'Rodin3D_Sketch', expected: '$0.4/Run' },
{ nodeType: 'Rodin3D_Detail', expected: '$0.4/Run' },
{ nodeType: 'Rodin3D_Smooth', expected: '$0.4/Run' }
]
testCases.forEach(({ nodeType, expected }) => {
const node = createMockNode(nodeType)
expect(getNodeDisplayPrice(node)).toBe(expected)
})
})
})
describe('Comprehensive Tripo edge case testing', () => {
it('should handle TripoImageToModelNode with various parameter combinations', () => {
const { getNodeDisplayPrice } = useNodePricing()
const testCases = [
{ quad: false, style: 'none', texture: false, expected: '$0.20/Run' },
{ quad: false, style: 'none', texture: true, expected: '$0.25/Run' },
{
quad: true,
style: 'any style',
texture: true,
textureQuality: 'detailed',
expected: '$0.50/Run'
},
{
quad: true,
style: 'any style',
texture: false,
textureQuality: 'standard',
expected: '$0.35/Run'
}
]
testCases.forEach(
({ quad, style, texture, textureQuality, expected }) => {
const widgets = [
{ name: 'quad', value: quad },
{ name: 'style', value: style },
{ name: 'texture', value: texture }
]
if (textureQuality) {
widgets.push({ name: 'texture_quality', value: textureQuality })
}
const node = createMockNode('TripoImageToModelNode', widgets)
expect(getNodeDisplayPrice(node)).toBe(expected)
}
)
})
it('should return correct fallback for TripoImageToModelNode', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('TripoImageToModelNode', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe(
'$0.2-0.5/Run (varies with quad, style, texture & quality)'
)
})
it('should handle missing texture quality widget', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('TripoTextToModelNode', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe(
'$0.1-0.4/Run (varies with quad, style, texture & quality)'
)
})
it('should handle missing model version widget', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('TripoTextToModelNode', [
{ name: 'texture_quality', value: 'detailed' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe(
'$0.1-0.4/Run (varies with quad, style, texture & quality)'
)
})
it('should return correct pricing for exposed ByteDance models', () => {
const { getNodeDisplayPrice } = useNodePricing()
const testCases = [
{
node_name: 'ByteDanceImageNode',
model: 'seedream-3-0-t2i-250415',
expected: '$0.03/Run'
},
{
node_name: 'ByteDanceImageEditNode',
model: 'seededit-3-0-i2i-250628',
expected: '$0.03/Run'
}
]
testCases.forEach(({ node_name, model, expected }) => {
const node = createMockNode(node_name, [
{ name: 'model', value: model }
])
expect(getNodeDisplayPrice(node)).toBe(expected)
})
})
})
})
describe('dynamic pricing - ByteDanceSeedreamNode', () => {
it('should return fallback when widgets are missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('ByteDanceSeedreamNode', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.03/Run ($0.03 for one output image)')
})
it('should return $0.03/Run when sequential generation is disabled', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('ByteDanceSeedreamNode', [
{ name: 'sequential_image_generation', value: 'disabled' },
{ name: 'max_images', value: 5 }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.03/Run')
})
it('should multiply by max_images when sequential generation is enabled', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('ByteDanceSeedreamNode', [
{ name: 'sequential_image_generation', value: 'enabled' },
{ name: 'max_images', value: 4 }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.12/Run ($0.03 for one output image)')
})
})
describe('dynamic pricing - ByteDance Seedance video nodes', () => {
it('should return base 10s range for PRO 1080p on ByteDanceTextToVideoNode', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('ByteDanceTextToVideoNode', [
{ name: 'model', value: 'seedance-1-0-pro' },
{ name: 'duration', value: '10' },
{ name: 'resolution', value: '1080p' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$1.18-$1.22/Run')
})
it('should scale to half for 5s PRO 1080p on ByteDanceTextToVideoNode', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('ByteDanceTextToVideoNode', [
{ name: 'model', value: 'seedance-1-0-pro' },
{ name: 'duration', value: '5' },
{ name: 'resolution', value: '1080p' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.59-$0.61/Run')
})
it('should scale for 8s PRO 480p on ByteDanceImageToVideoNode', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('ByteDanceImageToVideoNode', [
{ name: 'model', value: 'seedance-1-0-pro' },
{ name: 'duration', value: '8' },
{ name: 'resolution', value: '480p' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.18-$0.19/Run')
})
it('should scale correctly for 12s PRO 720p on ByteDanceFirstLastFrameNode', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('ByteDanceFirstLastFrameNode', [
{ name: 'model', value: 'seedance-1-0-pro' },
{ name: 'duration', value: '12' },
{ name: 'resolution', value: '720p' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.61-$0.67/Run')
})
it('should collapse to a single value when min and max round equal for LITE 480p 3s on ByteDanceImageReferenceNode', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('ByteDanceImageReferenceNode', [
{ name: 'model', value: 'seedance-1-0-lite' },
{ name: 'duration', value: '3' },
{ name: 'resolution', value: '480p' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.05/Run') // 0.17..0.18 scaled by 0.3 both round to 0.05
})
it('should return Token-based when required widgets are missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const missingModel = createMockNode('ByteDanceFirstLastFrameNode', [
{ name: 'duration', value: '10' },
{ name: 'resolution', value: '1080p' }
])
const missingResolution = createMockNode('ByteDanceImageToVideoNode', [
{ name: 'model', value: 'seedance-1-0-pro' },
{ name: 'duration', value: '10' }
])
const missingDuration = createMockNode('ByteDanceTextToVideoNode', [
{ name: 'model', value: 'seedance-1-0-lite' },
{ name: 'resolution', value: '720p' }
])
expect(getNodeDisplayPrice(missingModel)).toBe('Token-based')
expect(getNodeDisplayPrice(missingResolution)).toBe('Token-based')
expect(getNodeDisplayPrice(missingDuration)).toBe('Token-based')
})
})
describe('dynamic pricing - WanTextToVideoApi', () => {
it('should return $1.50 for 10s at 1080p', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanTextToVideoApi', [
{ name: 'duration', value: '10' },
{ name: 'size', value: '1080p: 4:3 (1632x1248)' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$1.50/Run') // 0.15 * 10
})
it('should return $0.50 for 5s at 720p', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanTextToVideoApi', [
{ name: 'duration', value: 5 },
{ name: 'size', value: '720p: 16:9 (1280x720)' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.50/Run') // 0.10 * 5
})
it('should return $0.15 for 3s at 480p', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanTextToVideoApi', [
{ name: 'duration', value: '3' },
{ name: 'size', value: '480p: 1:1 (624x624)' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.15/Run') // 0.05 * 3
})
it('should fall back when widgets are missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const missingBoth = createMockNode('WanTextToVideoApi', [])
const missingSize = createMockNode('WanTextToVideoApi', [
{ name: 'duration', value: '5' }
])
const missingDuration = createMockNode('WanTextToVideoApi', [
{ name: 'size', value: '1080p' }
])
expect(getNodeDisplayPrice(missingBoth)).toBe('$0.05-0.15/second')
expect(getNodeDisplayPrice(missingSize)).toBe('$0.05-0.15/second')
expect(getNodeDisplayPrice(missingDuration)).toBe('$0.05-0.15/second')
})
it('should fall back on invalid duration', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanTextToVideoApi', [
{ name: 'duration', value: 'invalid' },
{ name: 'size', value: '1080p' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.05-0.15/second')
})
it('should fall back on unknown resolution', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanTextToVideoApi', [
{ name: 'duration', value: '10' },
{ name: 'size', value: '2K' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.05-0.15/second')
})
})
describe('dynamic pricing - WanImageToVideoApi', () => {
it('should return $0.80 for 8s at 720p', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanImageToVideoApi', [
{ name: 'duration', value: 8 },
{ name: 'resolution', value: '720p' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.80/Run') // 0.10 * 8
})
it('should return $0.60 for 12s at 480P', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanImageToVideoApi', [
{ name: 'duration', value: '12' },
{ name: 'resolution', value: '480P' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.60/Run') // 0.05 * 12
})
it('should return $1.50 for 10s at 1080p', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanImageToVideoApi', [
{ name: 'duration', value: '10' },
{ name: 'resolution', value: '1080p' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$1.50/Run') // 0.15 * 10
})
it('should handle "5s" string duration at 1080P', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanImageToVideoApi', [
{ name: 'duration', value: '5s' },
{ name: 'resolution', value: '1080P' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.75/Run') // 0.15 * 5
})
it('should fall back when widgets are missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const missingBoth = createMockNode('WanImageToVideoApi', [])
const missingRes = createMockNode('WanImageToVideoApi', [
{ name: 'duration', value: '5' }
])
const missingDuration = createMockNode('WanImageToVideoApi', [
{ name: 'resolution', value: '1080p' }
])
expect(getNodeDisplayPrice(missingBoth)).toBe('$0.05-0.15/second')
expect(getNodeDisplayPrice(missingRes)).toBe('$0.05-0.15/second')
expect(getNodeDisplayPrice(missingDuration)).toBe('$0.05-0.15/second')
})
it('should fall back on invalid duration', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanImageToVideoApi', [
{ name: 'duration', value: 'invalid' },
{ name: 'resolution', value: '720p' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.05-0.15/second')
})
it('should fall back on unknown resolution', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanImageToVideoApi', [
{ name: 'duration', value: '10' },
{ name: 'resolution', value: 'weird-res' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.05-0.15/second')
})
})
describe('dynamic pricing - LtxvApiTextToVideo', () => {
it('should return $0.30 for Pro 1080p 5s', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('LtxvApiTextToVideo', [
{ name: 'model', value: 'LTX-2 (Pro)' },
{ name: 'duration', value: '5' },
{ name: 'resolution', value: '1920x1080' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.30/Run') // 0.06 * 5
})
it('should parse "10s" duration strings', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('LtxvApiTextToVideo', [
{ name: 'model', value: 'LTX-2 (Fast)' },
{ name: 'duration', value: '10' },
{ name: 'resolution', value: '3840x2160' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$1.60/Run') // 0.16 * 10
})
it('should fall back when a required widget is missing (no resolution)', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('LtxvApiTextToVideo', [
{ name: 'model', value: 'LTX-2 (Pro)' },
{ name: 'duration', value: '5' }
// missing resolution
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.04-0.24/second')
})
it('should fall back for unknown model', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('LtxvApiTextToVideo', [
{ name: 'model', value: 'LTX-3 (Pro)' },
{ name: 'duration', value: 5 },
{ name: 'resolution', value: '1920x1080' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.04-0.24/second')
})
})
})