chore: snapshot workflow migration progress

This commit is contained in:
2026-04-12 11:49:04 +02:00
parent 0cd02513d5
commit 3e810c74a3
163 changed files with 31773 additions and 2752 deletions
@@ -0,0 +1,307 @@
import { useState } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render, screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { beforeEach, describe, expect, test, vi } from 'vitest'
import type { RenderTemplate } from '../../api/renderTemplates'
import type { WorkflowNodeDefinition, WorkflowParams } from '../../api/workflows'
import { WorkflowNodeInspector } from '../../components/workflows/WorkflowNodeInspector'
const listRenderTemplates = vi.fn<() => Promise<RenderTemplate[]>>()
vi.mock('../../api/renderTemplates', () => ({
listRenderTemplates: () => listRenderTemplates(),
}))
const resolveTemplateDefinition: WorkflowNodeDefinition = {
step: 'resolve_template',
label: 'Resolve Template',
family: 'order_line',
module_key: 'rendering.resolve_template',
category: 'processing',
description: 'Resolve a template for order-line rendering.',
node_type: 'processNode',
icon: 'layers',
defaults: {},
fields: [
{
key: 'template_id_override',
label: 'Template Override',
type: 'text',
description: 'Manual template override.',
section: 'General',
default: '',
min: null,
max: null,
step: null,
unit: null,
options: [],
allow_blank: true,
max_length: null,
text_format: 'uuid',
},
],
execution_kind: 'bridge',
legacy_compatible: true,
input_contract: { context: 'order_line', requires: ['order_line_context'] },
output_contract: { context: 'order_line', provides: ['render_template'] },
artifact_roles_consumed: ['order_line_context'],
artifact_roles_produced: ['render_template'],
legacy_source: 'legacy.resolve_template',
}
const notifyDefinition: WorkflowNodeDefinition = {
step: 'notify',
label: 'Notify',
family: 'order_line',
module_key: 'notifications.emit',
category: 'output',
description: 'Emit the workflow result.',
node_type: 'outputNode',
icon: 'bell',
defaults: {},
fields: [],
execution_kind: 'bridge',
legacy_compatible: true,
input_contract: { context: 'order_line', requires: ['workflow_result'] },
output_contract: { context: 'order_line', provides: ['notification_event'] },
artifact_roles_consumed: ['workflow_result'],
artifact_roles_produced: ['notification_event'],
legacy_source: 'legacy.notify',
}
function createRenderTemplate(overrides: Partial<RenderTemplate> = {}): RenderTemplate {
return {
id: '0d87b85f-c454-4d61-a124-d5b59e6a43a2',
name: 'Bearing Studio',
category_key: 'bearings',
output_type_id: null,
output_type_name: null,
output_type_ids: [],
output_type_names: ['Still'],
blend_file_path: '/templates/bearing.blend',
original_filename: 'bearing.blend',
target_collection: 'Product',
material_replace_enabled: true,
lighting_only: false,
shadow_catcher_enabled: false,
camera_orbit: true,
workflow_input_schema: [],
is_active: true,
created_at: '2026-04-11T00:00:00Z',
updated_at: '2026-04-11T00:00:00Z',
...overrides,
}
}
function renderInspector(
params: WorkflowParams,
onChange = vi.fn(),
options: { stateful?: boolean } = {},
) {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
function StatefulInspector() {
const [currentParams, setCurrentParams] = useState(params)
return (
<WorkflowNodeInspector
params={currentParams}
onChange={nextParams => {
setCurrentParams(nextParams)
onChange(nextParams)
}}
nodeDefinition={resolveTemplateDefinition}
step="resolve_template"
nodeDefinitions={[resolveTemplateDefinition]}
graphFamily="order_line"
onStepChange={vi.fn()}
/>
)
}
render(
<QueryClientProvider client={queryClient}>
{options.stateful ? (
<StatefulInspector />
) : (
<WorkflowNodeInspector
params={params}
onChange={onChange}
nodeDefinition={resolveTemplateDefinition}
step="resolve_template"
nodeDefinitions={[resolveTemplateDefinition]}
graphFamily="order_line"
onStepChange={vi.fn()}
/>
)}
</QueryClientProvider>,
)
return { onChange, queryClient }
}
describe('WorkflowNodeInspector', () => {
beforeEach(() => {
listRenderTemplates.mockReset()
})
test('renders template-defined workflow input fields for resolve_template nodes', async () => {
listRenderTemplates.mockResolvedValue([
createRenderTemplate({
workflow_input_schema: [
{
key: 'studio_variant',
label: 'Studio Variant',
type: 'select',
section: 'Template Inputs',
description: 'Choose the blend lighting preset.',
default: 'default',
min: null,
max: null,
step: null,
unit: null,
options: [
{ value: 'default', label: 'Default' },
{ value: 'warm', label: 'Warm' },
],
allow_blank: false,
},
],
}),
])
renderInspector({
template_id_override: '0d87b85f-c454-4d61-a124-d5b59e6a43a2',
template_input__studio_variant: 'warm',
})
expect(await screen.findByLabelText('Template Override')).toHaveValue(
'0d87b85f-c454-4d61-a124-d5b59e6a43a2',
)
expect(await screen.findByLabelText('Studio Variant')).toHaveValue('warm')
expect(screen.getByText('Bearing Studio')).toBeInTheDocument()
})
test('clears dynamic template inputs when template override is removed', async () => {
listRenderTemplates.mockResolvedValue([
createRenderTemplate({
workflow_input_schema: [
{
key: 'studio_variant',
label: 'Studio Variant',
type: 'select',
section: 'Template Inputs',
description: 'Choose the blend lighting preset.',
default: 'default',
min: null,
max: null,
step: null,
unit: null,
options: [
{ value: 'default', label: 'Default' },
{ value: 'warm', label: 'Warm' },
],
allow_blank: false,
},
],
}),
])
const user = userEvent.setup()
const { onChange } = renderInspector({
template_id_override: '0d87b85f-c454-4d61-a124-d5b59e6a43a2',
template_input__studio_variant: 'warm',
template_input__camera_profile: 'macro',
})
const templateOverride = await screen.findByLabelText('Template Override')
await user.selectOptions(templateOverride, '')
await waitFor(() => {
expect(onChange).toHaveBeenCalledWith({})
})
})
test('explains connection-driven nodes when no editor fields are available', async () => {
listRenderTemplates.mockResolvedValue([])
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
render(
<QueryClientProvider client={queryClient}>
<WorkflowNodeInspector
params={{}}
onChange={vi.fn()}
nodeDefinition={notifyDefinition}
step="notify"
nodeDefinitions={[notifyDefinition]}
graphFamily="order_line"
onStepChange={vi.fn()}
/>
</QueryClientProvider>,
)
expect(screen.getByText('This node has no editor settings.')).toBeInTheDocument()
expect(screen.getByText(/each required upstream input gets its own socket/i)).toBeInTheDocument()
expect(screen.getByText(/0 local variables by design/i)).toBeInTheDocument()
expect(screen.getByText('Socket 1')).toBeInTheDocument()
expect(
screen.getAllByText('Any of: Rendered Image / Rendered Frames / Rendered Video / Workflow Result / Blend Asset').length,
).toBeGreaterThan(0)
})
test('summarizes wired inputs and inspector variables separately', async () => {
listRenderTemplates.mockResolvedValue([
createRenderTemplate({
workflow_input_schema: [
{
key: 'studio_variant',
label: 'Studio Variant',
type: 'select',
section: 'Template Inputs',
description: 'Choose the blend lighting preset.',
default: 'default',
min: null,
max: null,
step: null,
unit: null,
options: [
{ value: 'default', label: 'Default' },
{ value: 'warm', label: 'Warm' },
],
allow_blank: false,
},
],
}),
])
const user = userEvent.setup()
renderInspector({}, vi.fn(), { stateful: true })
const templateOverride = await screen.findByLabelText('Template Override')
await waitFor(() => {
expect(within(templateOverride).getByRole('option', { name: /bearing studio/i })).toBeInTheDocument()
})
await user.selectOptions(templateOverride, '0d87b85f-c454-4d61-a124-d5b59e6a43a2')
expect(await screen.findByText(/1 canvas socket is required/i)).toBeInTheDocument()
expect(screen.getByText('Socket 1')).toBeInTheDocument()
expect(await screen.findByText(/2 local variables are edited in the inspector/i)).toBeInTheDocument()
expect(screen.getByText(/Static: Template Override/i)).toBeInTheDocument()
expect(screen.getByText(/Template-driven: Studio Variant/i)).toBeInTheDocument()
})
})