/**
* 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 React from 'react';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, cleanup, fireEvent, waitFor } from '@testing-library/react';
import { App, ConfigProvider } from 'antd';
import { FlowEngine } from '../../flowEngine';
import { FlowModel, ModelRenderMode } from '../../models/flowModel';
import { FlowEngineProvider } from '../../provider';
import { FlowModelRenderer } from '../FlowModelRenderer';
// ---------------- Pure Mocks ----------------
// Intercept Dropdown (capture menu) and Modal.confirm (auto-confirm), stub others
const dropdownMenus: any[] = [];
vi.mock('antd', () => {
const Dropdown = (props: any) => {
(globalThis as any).__lastDropdownMenu = props.menu;
dropdownMenus.push(props.menu);
return React.createElement('span', { 'data-testid': 'dropdown' }, props.children);
};
const Modal = {
confirm: (opts: any) => {
if (opts && typeof opts.onOk === 'function') {
return opts.onOk();
}
return {} as any;
},
error: vi.fn(),
};
const App = Object.assign(({ children }: any) => React.createElement(React.Fragment, null, children), {
useApp: () => ({ message: { success: () => {}, error: () => {}, info: () => {} } }),
});
const ConfigProvider = ({ children }: any) => React.createElement(React.Fragment, null, children);
const Button = (props: any) => React.createElement('button', props, props.children ?? 'Button');
const Result = (props: any) => React.createElement('div', { 'data-testid': 'result' }, props.children ?? 'Result');
const Collapse = Object.assign((props: any) => React.createElement('div', null, props.children ?? 'Collapse'), {
Panel: (props: any) => React.createElement('div', null, props.children ?? 'Panel'),
});
const Space = ({ children }: any) => React.createElement('div', null, children);
const FormItem = (props: any) => React.createElement('div', null, props.children ?? 'FormItem');
const Form = Object.assign((props: any) => React.createElement('form', null, props.children ?? 'Form'), {
Item: FormItem,
useForm: () => [{ setFieldsValue: (_: any) => {} }],
});
const Input: any = (props: any) => React.createElement('input', props);
Input.TextArea = (props: any) => React.createElement('textarea', props);
const InputNumber = (props: any) => React.createElement('input', { ...props, type: 'number' });
const Select = (props: any) => React.createElement('select', props);
const Switch = (props: any) => React.createElement('input', { ...props, type: 'checkbox' });
const Alert = (props: any) => React.createElement('div', { role: 'alert' }, props.message ?? 'Alert');
const Skeleton: any = () => React.createElement('div', null, 'Skeleton');
Skeleton.Button = (props: any) => React.createElement('div', null, 'Skeleton.Button');
const Spin = (props: any) => React.createElement('div', null, props.children ?? 'Spin');
const Typography = {
Paragraph: ({ children }: any) => React.createElement('p', null, children ?? 'Paragraph'),
Text: ({ children }: any) => React.createElement('span', null, children ?? 'Text'),
};
return {
Dropdown,
Modal,
App,
ConfigProvider,
Button,
Result,
Typography,
Collapse,
Space,
Form,
Input,
InputNumber,
Select,
Switch,
Alert,
Skeleton,
Spin,
theme: { useToken: () => ({}) },
} as any;
});
// ---------------- Helpers ----------------
const clickDeleteFromLastDropdown = async () => {
await waitFor(() => {
const menu = (globalThis as any).__lastDropdownMenu;
expect(menu).toBeTruthy();
});
const menu = (globalThis as any).__lastDropdownMenu;
menu.onClick?.({ key: 'delete' });
};
const getHost = (element: HTMLElement) => element.closest('[data-has-float-menu="true"]') as HTMLDivElement;
const hoverHostAndClickDelete = async (element: HTMLElement) => {
const host = getHost(element);
if (host) {
fireEvent.mouseEnter(host);
}
await clickDeleteFromLastDropdown();
};
// ---------------- Tests ----------------
describe('Delete problematic model via FlowSettings menu', () => {
beforeEach(() => {
dropdownMenus.length = 0;
(globalThis as any).__lastDropdownMenu = undefined;
});
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
it('deletes a broken top-level model when rendering via FlowModelRenderer throws', async () => {
class BrokenModel extends FlowModel {
render() {
throw new Error('boom-top');
}
}
const engine = new FlowEngine();
await engine.flowSettings.forceEnable();
engine.registerModels({ BrokenModel });
const model = engine.createModel({ use: 'BrokenModel', uid: 'broken-top-2' }) as BrokenModel;
// satisfy FlowsFloatContextMenu styles
model.context.defineProperty('themeToken', { value: { borderRadiusLG: 8 } });
const { findByTestId } = render(