UNPKG

@nocobase/flow-engine

Version:

A standalone flow engine for NocoBase, managing workflows, models, and actions.

190 lines (162 loc) 6.11 kB
/** * This file is part of the NocoBase (R) project. * Copyright (c) 2020-2024 NocoBase Co., Ltd. * Authors: NocoBase Team. * * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. * For more information, please refer to: https://www.nocobase.com/agreement. */ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { FlowEngine } from '../flowEngine'; import { FlowModel } from '../models'; import type { IFlowModelRepository } from '../types'; class BaseModel extends FlowModel {} class SubModelA extends BaseModel {} class SubModelB extends BaseModel {} class SubModelC extends SubModelA {} describe('FlowEngine', () => { let engine: FlowEngine; beforeEach(() => { engine = new FlowEngine(); engine.registerModels({ BaseModel, SubModelA, SubModelB, SubModelC, }); }); it('getModelClass should return correct class', () => { expect(engine.getModelClass('BaseModel')).toBe(BaseModel); expect(engine.getModelClass('SubModelA')).toBe(SubModelA); expect(engine.getModelClass('NotExist')).toBeUndefined(); }); it('getSubclassesOf should return all subclasses', () => { const result = engine.getSubclassesOf(BaseModel); expect(result.has('BaseModel')).toBe(false); expect(result.has('SubModelA')).toBe(true); expect(result.has('SubModelB')).toBe(true); expect(result.has('SubModelC')).toBe(true); }); it('getSubclassesOf should support filter', () => { const result = engine.getSubclassesOf(BaseModel, (ModelClass, name) => name.startsWith('SubModel')); expect(result.has('BaseModel')).toBe(false); expect(result.has('SubModelA')).toBe(true); expect(result.has('SubModelB')).toBe(true); expect(result.has('SubModelC')).toBe(true); }); it('findModelClass should find by predicate', () => { const found = engine.findModelClass((name) => name === 'SubModelB'); expect(found).toBeDefined(); expect(found?.[0]).toBe('SubModelB'); expect(found?.[1]).toBe(SubModelB); }); it('filterModelClassByParent should return correct subclasses', () => { const result = engine.filterModelClassByParent(SubModelA); expect(result.has('SubModelA')).toBe(false); expect(result.has('SubModelC')).toBe(true); expect(result.has('BaseModel')).toBe(false); expect(result.has('SubModelB')).toBe(false); }); it('registerAction/getAction should work', () => { engine.registerActions({ testAction: { name: 'testAction', handler: vi.fn(), }, }); const action = engine.getAction('testAction'); expect(action).toBeDefined(); expect(action?.name).toBe('testAction'); }); it('registerModels should overwrite existing model', () => { class NewBaseModel extends FlowModel {} engine.registerModels({ BaseModel: NewBaseModel }); expect(engine.getModelClass('BaseModel')).toBe(NewBaseModel); }); describe('loadOrCreateModel', () => { class MockFlowModelRepository implements IFlowModelRepository { // 使用可配置返回值,便于不同用例控制 findOne 行为 findOneResult: any = null; save = vi.fn(async (model: FlowModel) => ({ success: true, uid: model.uid })); async findOne() { // 返回深拷贝,避免被测试过程修改 return this.findOneResult ? JSON.parse(JSON.stringify(this.findOneResult)) : null; } async destroy() { return true; } async move() {} } let repo: MockFlowModelRepository; beforeEach(() => { repo = new MockFlowModelRepository(); engine.setModelRepository(repo); }); it('should set as object subModel when subType=object even if subKey="array" (regression for subType check)', async () => { // 父模型 const parent = engine.createModel({ uid: 'p1', use: 'FlowModel' }); // 仓库返回数据:subKey 恰好为 'array',但 subType 是 'object' repo.findOneResult = { uid: 'c1', use: 'FlowModel', parentId: parent.uid, subKey: 'array', subType: 'object', }; const child = await engine.loadOrCreateModel({ uid: 'c1', use: 'FlowModel', parentId: parent.uid, subKey: 'array', subType: 'object', }); expect(child).toBeTruthy(); // 关键断言:应按 object 模式挂载,而不是数组 const mounted = (parent.subModels as any)['array']; expect(Array.isArray(mounted)).toBe(false); expect(mounted?.uid).toBe('c1'); expect(mounted).toBe(child); }); it('should add to array subModels when subType=array', async () => { const parent = engine.createModel({ uid: 'p2', use: 'FlowModel' }); repo.findOneResult = { uid: 'c2', use: 'FlowModel', parentId: parent.uid, subKey: 'items', subType: 'array', }; const child = await engine.loadOrCreateModel({ uid: 'c2', use: 'FlowModel', parentId: parent.uid, subKey: 'items', subType: 'array', }); expect(child).toBeTruthy(); const arr = (parent.subModels as any)['items']; expect(Array.isArray(arr)).toBe(true); expect(arr.length).toBe(1); expect(arr[0].uid).toBe('c2'); }); it('should create and save when repo has no data, then mount under parent accordingly (object case)', async () => { const parent = engine.createModel({ uid: 'p3', use: 'FlowModel' }); // 仓库查无数据 repo.findOneResult = null; const child = await engine.loadOrCreateModel({ uid: 'c3', use: 'FlowModel', parentId: parent.uid, subKey: 'info', subType: 'object', }); expect(child).toBeTruthy(); // 应触发一次保存(create 后会调用 save) expect(repo.save).toHaveBeenCalledTimes(1); // 正确以对象形式挂载 const mounted = (parent.subModels as any)['info']; expect(Array.isArray(mounted)).toBe(false); expect(mounted?.uid).toBe('c3'); }); }); });