690615:1449 237 #01
This commit is contained in:
@@ -115,4 +115,79 @@ describe('DSLEditor (T054)', () => {
|
||||
});
|
||||
// ไม่ throw error
|
||||
});
|
||||
|
||||
it('calls onChange callback when editor value changes', async () => {
|
||||
const onChange = vi.fn();
|
||||
render(<DSLEditor initialValue="initial" onChange={onChange} />);
|
||||
|
||||
const editor = screen.getByTestId('monaco-editor');
|
||||
await userEvent.type(editor, ' updated');
|
||||
|
||||
// onChange ถูกเรียกแต่ละ character - check ว่าถูกเรียกและค่าสุดท้ายถูกต้อง
|
||||
expect(onChange).toHaveBeenCalled();
|
||||
expect(onChange).toHaveBeenLastCalledWith(' updated');
|
||||
});
|
||||
|
||||
it('disables Validate and Test buttons when readOnly=true', () => {
|
||||
render(<DSLEditor initialValue="test" readOnly={true} />);
|
||||
|
||||
const validateButton = screen.getByRole('button', { name: /validate/i });
|
||||
const testButton = screen.getByRole('button', { name: /test/i });
|
||||
|
||||
expect(validateButton).toBeDisabled();
|
||||
expect(testButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('enables Validate and Test buttons when readOnly=false', () => {
|
||||
render(<DSLEditor initialValue="test" readOnly={false} />);
|
||||
|
||||
const validateButton = screen.getByRole('button', { name: /validate/i });
|
||||
const testButton = screen.getByRole('button', { name: /test/i });
|
||||
|
||||
expect(validateButton).not.toBeDisabled();
|
||||
expect(testButton).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('clears validation result when editor value changes', async () => {
|
||||
mockValidateDSL.mockResolvedValue({ valid: true });
|
||||
const onChange = vi.fn();
|
||||
|
||||
render(<DSLEditor initialValue="test" onChange={onChange} onValidationChange={onValidationChange} />);
|
||||
|
||||
// Validate first
|
||||
await userEvent.click(screen.getByRole('button', { name: /validate/i }));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/valid and ready/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Change editor value
|
||||
const editor = screen.getByTestId('monaco-editor');
|
||||
await userEvent.type(editor, ' updated');
|
||||
|
||||
// Validation result should be cleared
|
||||
expect(screen.queryByText(/valid and ready/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows Test result when Test button is clicked', async () => {
|
||||
render(<DSLEditor initialValue="test" />);
|
||||
|
||||
const testButton = screen.getByRole('button', { name: /test/i });
|
||||
await userEvent.click(testButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Workflow simulation completed successfully/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('updates internal state when initialValue prop changes', () => {
|
||||
const { rerender } = render(<DSLEditor initialValue="initial" />);
|
||||
|
||||
// Mock Monaco editor ไม่ได้ update value เมื่อ initialValue เปลี่ยน
|
||||
// แต่เราสามารถ test ได้โดย render component ใหม่ด้วย initialValue ต่างกัน
|
||||
rerender(<DSLEditor initialValue="updated" />);
|
||||
|
||||
// Component ควร render ได้โดยไม่ throw error
|
||||
const editor = screen.getByTestId('monaco-editor');
|
||||
expect(editor).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
// File: components/workflows/__tests__/visual-builder.test.ts
|
||||
// Change Log:
|
||||
// - 2026-06-14: สร้างใหม่สำหรับ Phase 3 Coverage
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
// Mock ReactFlow to avoid dependency issues
|
||||
vi.mock('reactflow', () => ({
|
||||
ReactFlow: () => null,
|
||||
Controls: () => null,
|
||||
Background: () => null,
|
||||
Panel: () => null,
|
||||
useNodesState: () => [[], () => {}, () => {}],
|
||||
useEdgesState: () => [[], () => {}, () => {}],
|
||||
addEdge: (params: any, edges: any) => [...edges, params],
|
||||
useReactFlow: () => ({ fitView: () => {} }),
|
||||
MarkerType: { ArrowClosed: 'arrowclosed' },
|
||||
ReactFlowProvider: ({ children }: { children: React.ReactNode }) => children,
|
||||
}));
|
||||
|
||||
// Import helper functions after mocking
|
||||
import { createNode, createEdge, parseDSL } from '../visual-builder';
|
||||
|
||||
describe('visual-builder helper functions', () => {
|
||||
describe('createNode', () => {
|
||||
it('ควรสร้าง node ปกติ', () => {
|
||||
const node = createNode('TestNode', 100);
|
||||
|
||||
expect(node.id).toBe('TestNode');
|
||||
expect(node.type).toBe('default');
|
||||
expect(node.data.label).toBe('TestNode\n(No Role)');
|
||||
expect(node.data.name).toBe('TestNode');
|
||||
});
|
||||
|
||||
it('ควรสร้าง start node เมื่อ isStart=true', () => {
|
||||
const node = createNode('Start', 100, { isStart: true });
|
||||
|
||||
expect(node.type).toBe('input');
|
||||
expect(node.data.type).toBe('START');
|
||||
expect(node.style?.background).toBe('#10b981');
|
||||
});
|
||||
|
||||
it('ควรสร้าง end node เมื่อ isEnd=true', () => {
|
||||
const node = createNode('End', 100, { isEnd: true });
|
||||
|
||||
expect(node.type).toBe('output');
|
||||
expect(node.data.type).toBe('END');
|
||||
expect(node.style?.background).toBe('#ef4444');
|
||||
});
|
||||
|
||||
it('ควรสร้าง condition node เมื่อ isCondition=true', () => {
|
||||
const node = createNode('Condition', 100, { isCondition: true });
|
||||
|
||||
expect(node.style?.background).toBe('#fef3c7');
|
||||
expect(node.style?.borderStyle).toBe('dashed');
|
||||
});
|
||||
|
||||
it('ควรใส่ role ใน label เมื่อมี role', () => {
|
||||
const node = createNode('Task', 100, { role: 'Manager' });
|
||||
|
||||
expect(node.data.label).toBe('Task\n(Manager)');
|
||||
expect(node.data.role).toBe('Manager');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createEdge', () => {
|
||||
it('ควรสร้าง edge ระหว่าง source และ target', () => {
|
||||
const edge = createEdge('node1', 'node2', 'TRANSITION');
|
||||
|
||||
expect(edge.source).toBe('node1');
|
||||
expect(edge.target).toBe('node2');
|
||||
expect(edge.label).toBe('TRANSITION');
|
||||
expect(edge.id).toBe('e-node1-TRANSITION-node2');
|
||||
});
|
||||
|
||||
it('ควรมี markerEnd', () => {
|
||||
const edge = createEdge('node1', 'node2', 'TRANSITION');
|
||||
|
||||
expect(edge.markerEnd).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseDSL', () => {
|
||||
it('ควร return empty nodes/edges เมื่อ DSL เป็น empty string', () => {
|
||||
const result = parseDSL('');
|
||||
|
||||
expect(result.nodes).toEqual([]);
|
||||
expect(result.edges).toEqual([]);
|
||||
});
|
||||
|
||||
it('ควร return empty nodes/edges เมื่อ JSON parse fail', () => {
|
||||
const result = parseDSL('invalid json');
|
||||
|
||||
expect(result.nodes).toEqual([]);
|
||||
expect(result.edges).toEqual([]);
|
||||
});
|
||||
|
||||
it('ควร parse DSL ที่มี states array', () => {
|
||||
const dsl = JSON.stringify({
|
||||
states: [
|
||||
{ name: 'Start', type: 'START', initial: true },
|
||||
{ name: 'Review', role: 'Manager' },
|
||||
],
|
||||
});
|
||||
|
||||
const result = parseDSL(dsl);
|
||||
|
||||
expect(result.nodes.length).toBe(2);
|
||||
expect(result.nodes[0].data.name).toBe('Start');
|
||||
expect(result.nodes[1].data.name).toBe('Review');
|
||||
});
|
||||
|
||||
it('ควร parse DSL ที่มี states object', () => {
|
||||
const dsl = JSON.stringify({
|
||||
initialState: 'Start',
|
||||
states: {
|
||||
Start: { initial: true },
|
||||
End: { terminal: true },
|
||||
},
|
||||
});
|
||||
|
||||
const result = parseDSL(dsl);
|
||||
|
||||
expect(result.nodes.length).toBe(2);
|
||||
expect(result.nodes[0].data.name).toBe('Start');
|
||||
expect(result.nodes[1].data.name).toBe('End');
|
||||
});
|
||||
|
||||
it('ควรสร้าง edges จาก transitions', () => {
|
||||
const dsl = JSON.stringify({
|
||||
states: [
|
||||
{ name: 'Start', on: { SUBMIT: { to: 'Review' } } },
|
||||
{ name: 'Review' },
|
||||
],
|
||||
});
|
||||
|
||||
const result = parseDSL(dsl);
|
||||
|
||||
expect(result.edges.length).toBe(1);
|
||||
expect(result.edges[0].source).toBe('Start');
|
||||
expect(result.edges[0].target).toBe('Review');
|
||||
});
|
||||
|
||||
it('ควร handle dslDefinition field', () => {
|
||||
const dsl = JSON.stringify({
|
||||
dslDefinition: JSON.stringify({
|
||||
states: [{ name: 'Start' }],
|
||||
}),
|
||||
});
|
||||
|
||||
const result = parseDSL(dsl);
|
||||
|
||||
expect(result.nodes.length).toBe(1);
|
||||
});
|
||||
|
||||
it('ควร handle role จาก require.role', () => {
|
||||
const dsl = JSON.stringify({
|
||||
states: [
|
||||
{ name: 'Review', on: { SUBMIT: { require: { role: 'Manager' } } } },
|
||||
],
|
||||
});
|
||||
|
||||
const result = parseDSL(dsl);
|
||||
|
||||
expect(result.nodes[0].data.role).toBe('Manager');
|
||||
});
|
||||
|
||||
it('ควร handle role array จาก require.role', () => {
|
||||
const dsl = JSON.stringify({
|
||||
states: [
|
||||
{ name: 'Review', on: { SUBMIT: { require: { role: ['Manager', 'Lead'] } } } },
|
||||
],
|
||||
});
|
||||
|
||||
const result = parseDSL(dsl);
|
||||
|
||||
expect(result.nodes[0].data.role).toBe('Manager, Lead');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -106,7 +106,7 @@ interface VisualWorkflowBuilderProps {
|
||||
onDslChange?: (dsl: string) => void;
|
||||
}
|
||||
|
||||
const createNode = (
|
||||
export const createNode = (
|
||||
name: string,
|
||||
yOffset: number,
|
||||
options?: {
|
||||
@@ -148,7 +148,7 @@ const createNode = (
|
||||
};
|
||||
};
|
||||
|
||||
const createEdge = (source: string, target: string, label: string): Edge => ({
|
||||
export const createEdge = (source: string, target: string, label: string): Edge => ({
|
||||
id: `e-${source}-${label}-${target}`,
|
||||
source,
|
||||
target,
|
||||
@@ -156,7 +156,7 @@ const createEdge = (source: string, target: string, label: string): Edge => ({
|
||||
markerEnd: { type: MarkerType.ArrowClosed },
|
||||
});
|
||||
|
||||
function parseDSL(dsl: string): { nodes: Node[]; edges: Edge[] } {
|
||||
export function parseDSL(dsl: string): { nodes: Node[]; edges: Edge[] } {
|
||||
const nodes: Node[] = [];
|
||||
const edges: Edge[] = [];
|
||||
let yOffset = 50;
|
||||
|
||||
Reference in New Issue
Block a user