diff --git a/packages/demo/build.plugin.js b/packages/demo/build.plugin.js index 1e50c137b..44698c569 100644 --- a/packages/demo/build.plugin.js +++ b/packages/demo/build.plugin.js @@ -1,7 +1,7 @@ const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin'); -module.exports = ({ onGetWebpackConfig }) => { +module.exports = ({ context, onGetWebpackConfig }) => { onGetWebpackConfig((config) => { config.resolve.plugin('tsconfigpaths').use(TsconfigPathsPlugin, [ { @@ -23,5 +23,8 @@ module.exports = ({ onGetWebpackConfig }) => { config.plugins.delete('hot'); config.devServer.hot(false); + if (context.command === 'start') { + config.devtool('inline-source-map'); + } }); }; diff --git a/packages/designer/package.json b/packages/designer/package.json index d8dd526f8..54d91ab75 100644 --- a/packages/designer/package.json +++ b/packages/designer/package.json @@ -33,8 +33,7 @@ "build-plugin-component": "^0.2.10", "build-scripts-config": "^0.1.8", "jest": "^26.5.2", - "lodash.clonedeep": "^4.5.0", - "lodash.set": "^4.3.2", + "lodash": "^4.17.20", "ts-jest": "^26.4.1", "typescript": "^4.0.3" }, diff --git a/packages/designer/src/document/history.ts b/packages/designer/src/document/history.ts index 972fced01..61f1539d0 100644 --- a/packages/designer/src/document/history.ts +++ b/packages/designer/src/document/history.ts @@ -222,7 +222,7 @@ class Session { end() { if (this.isActive()) { this.clearTimer(); - console.info('session end'); + // console.info('session end'); } } diff --git a/packages/designer/src/document/node/node.ts b/packages/designer/src/document/node/node.ts index df0ebef6e..01ee191a2 100644 --- a/packages/designer/src/document/node/node.ts +++ b/packages/designer/src/document/node/node.ts @@ -23,6 +23,7 @@ import { ReactElement } from 'react'; import { SettingTopEntry } from 'designer/src/designer'; import { EventEmitter } from 'events'; import { includeSlot, removeSlot } from '../../utils/slot'; +import { foreachReverse } from '../../utils/tree'; /** * 基础节点 @@ -594,7 +595,11 @@ export class Node { import(data: Schema, checkId = false) { const { componentName, id, children, props, ...extras } = data; - + if (this.isSlot()) { + foreachReverse(this.children, (subNode: Node) => { + subNode.remove(true, true); + }, (iterable, idx) => (iterable as NodeChildren).get(idx)); + } if (this.isParental()) { this.props.import(props, extras); (this._children as NodeChildren).import(children, checkId); @@ -709,12 +714,12 @@ export class Node { } addSlot(slotNode: Node) { - slotNode.internalSetParent(this as ParentalNode, true); const slotName = slotNode?.getExtraProp('name')?.getAsString(); // 一个组件下的所有 slot,相同 slotName 的 slot 应该是唯一的 if (includeSlot(this, slotName)) { removeSlot(this, slotName); } + slotNode.internalSetParent(this as ParentalNode, true); this._slots.push(slotNode); } @@ -756,7 +761,7 @@ export class Node { this.purged = true; this.autoruns?.forEach((dispose) => dispose()); this.props.purge(); - this.document.destroyNode(this); + // this.document.destroyNode(this); } /** diff --git a/packages/designer/src/document/node/props/prop.ts b/packages/designer/src/document/node/props/prop.ts index 704a99ba6..8fdea7d66 100644 --- a/packages/designer/src/document/node/props/prop.ts +++ b/packages/designer/src/document/node/props/prop.ts @@ -269,7 +269,7 @@ export class Prop implements IPropParent { this.stash.clear(); } if (this._type !== 'slot' && this._slotNode) { - this._slotNode.purge(); + this._slotNode.remove(); this._slotNode = undefined; } } diff --git a/packages/designer/src/utils/slot.ts b/packages/designer/src/utils/slot.ts index 9fe1ef5f7..9061326b1 100644 --- a/packages/designer/src/utils/slot.ts +++ b/packages/designer/src/utils/slot.ts @@ -11,6 +11,7 @@ export function removeSlot(node: Node, slotName: string | undefined): boolean { const { slots = [] } = node; return slots.some((slot, idx) => { if (slotName && slotName === slot?.getExtraProp('name')?.getAsString()) { + slot.remove(); slots.splice(idx, 1); return true; } diff --git a/packages/designer/tests/bugs/misc.ts b/packages/designer/tests/bugs/misc.ts new file mode 100644 index 000000000..e101d868f --- /dev/null +++ b/packages/designer/tests/bugs/misc.ts @@ -0,0 +1,55 @@ +import set from 'lodash/set'; +import cloneDeep from 'lodash/cloneDeep'; +import '../fixtures/window'; +import { Project } from '../../src/project/project'; +// import { Node } from '../../../src/document/node/node'; +import { Designer } from '../../src/designer/designer'; +import formSchema from '../fixtures/schema/form'; +import { getIdsFromSchema, getNodeFromSchemaById } from '../utils'; + +const mockCreateSettingEntry = jest.fn(); +jest.mock('../../src/designer/designer', () => { + return { + Designer: jest.fn().mockImplementation(() => { + return { + getComponentMeta() { + return { + getMetadata() { + return { experimental: null }; + }, + }; + }, + transformProps(props) { return props; }, + createSettingEntry: mockCreateSettingEntry, + postEvent() {}, + }; + }), + }; +}); + +let designer = null; +beforeAll(() => { + designer = new Designer({}); +}); + +it.todo('在同一个节点下,相同名称的 slot 只能有一个', () => { + const project = new Project(designer, { + componentsTree: [ + formSchema, + ], + }); + project.open(); + expect(project).toBeTruthy(); + const { currentDocument } = project; + const { nodesMap } = currentDocument; + const ids = getIdsFromSchema(formSchema); + const expectedNodeCnt = ids.length; + expect(nodesMap.size).toBe(expectedNodeCnt); + ids.forEach(id => { + expect(nodesMap.get(id).componentName).toBe(getNodeFromSchemaById(formSchema, id).componentName); + }); + + const exportSchema = currentDocument?.export(1); + expect(getIdsFromSchema(exportSchema).length).toBe(expectedNodeCnt); + expect(mockCreateSettingEntry).toBeCalledTimes(expectedNodeCnt); +}); \ No newline at end of file diff --git a/packages/designer/tests/document/document-model/document-model.test.ts b/packages/designer/tests/document/document-model/document-model.test.ts index ca0c9f173..94eda1871 100644 --- a/packages/designer/tests/document/document-model/document-model.test.ts +++ b/packages/designer/tests/document/document-model/document-model.test.ts @@ -1,12 +1,11 @@ import '../../fixtures/window'; -console.log('window.matchMedia', window.matchMedia); window.matchMedia('width=600px'); import { DocumentModel } from '../../../src/document/document-model'; // const { DocumentModel } = require('../../../src/document/document-model'); // const { Node } = require('../__mocks__/node'); -describe('basic utility', () => { - test.only('delegateMethod - useOriginMethodName', () => { +describe.skip('basic utility', () => { + test('delegateMethod - useOriginMethodName', () => { const node = new DocumentModel({}, { componentName: 'Component', diff --git a/packages/designer/tests/document/document-model/node.test.ts b/packages/designer/tests/document/document-model/node.test.ts index e183aa6bd..29ea34123 100644 --- a/packages/designer/tests/document/document-model/node.test.ts +++ b/packages/designer/tests/document/document-model/node.test.ts @@ -17,7 +17,7 @@ jest.mock('../../../src/document/document-model', () => { }; }); -describe('basic utility', () => { +describe.skip('basic utility', () => { test('delegateMethod - useOriginMethodName', () => { const dm = new DocumentModel({} as any, {} as any); console.log(dm.nextId); diff --git a/packages/designer/tests/document/selection.test.ts b/packages/designer/tests/document/selection.test.ts new file mode 100644 index 000000000..4d81e935e --- /dev/null +++ b/packages/designer/tests/document/selection.test.ts @@ -0,0 +1,245 @@ +import set from 'lodash/set'; +import cloneDeep from 'lodash/cloneDeep'; +import '../fixtures/window'; +import { Project } from '../../src/project/project'; +import { Node } from '../../src/document/node/node'; +import { Designer } from '../../src/designer/designer'; +import formSchema from '../fixtures/schema/form'; +import { getIdsFromSchema, getNodeFromSchemaById } from '../utils'; + +const mockCreateSettingEntry = jest.fn(); +jest.mock('../../src/designer/designer', () => { + return { + Designer: jest.fn().mockImplementation(() => { + return { + getComponentMeta() { + return { + getMetadata() { + return { experimental: null }; + }, + }; + }, + transformProps(props) { return props; }, + createSettingEntry: mockCreateSettingEntry, + postEvent() {}, + }; + }), + }; +}); + +let designer = null; +beforeAll(() => { + designer = new Designer({}); +}); + +describe('选择区测试', () => { + it('常规方法', () => { + const project = new Project(designer, { + componentsTree: [ + formSchema, + ], + }); + project.open(); + expect(project).toBeTruthy(); + const { currentDocument } = project; + const { nodesMap, selection } = currentDocument!; + const selectionChangeHandler = jest.fn(); + selection.onSelectionChange(selectionChangeHandler); + + selection.select('form'); + expect(selectionChangeHandler).toHaveBeenCalledTimes(1); + expect(selection.selected).toEqual(['form']); + selectionChangeHandler.mockClear(); + + selection.select('form'); + expect(selectionChangeHandler).toHaveBeenCalledTimes(0); + expect(selection.selected).toEqual(['form']); + + selection.select('node_k1ow3cbj'); + expect(selectionChangeHandler).toHaveBeenCalledTimes(1); + expect(selectionChangeHandler.mock.calls[0][0]).toEqual(['node_k1ow3cbj']); + expect(selection.selected).toEqual(['node_k1ow3cbj']); + selectionChangeHandler.mockClear(); + + selection.selectAll(['node_k1ow3cbj', 'form']); + expect(selectionChangeHandler).toHaveBeenCalledTimes(1); + expect(selectionChangeHandler.mock.calls[0][0]).toEqual(['node_k1ow3cbj', 'form']); + expect(selection.selected).toEqual(['node_k1ow3cbj', 'form']); + selectionChangeHandler.mockClear(); + + selection.remove('node_k1ow3cbj'); + expect(selectionChangeHandler).toHaveBeenCalledTimes(1); + expect(selectionChangeHandler.mock.calls[0][0]).toEqual(['form']); + expect(selection.selected).toEqual(['form']); + selectionChangeHandler.mockClear(); + + selection.clear(); + expect(selectionChangeHandler).toHaveBeenCalledTimes(1); + expect(selectionChangeHandler.mock.calls[0][0]).toEqual([]); + expect(selection.selected).toEqual([]); + selectionChangeHandler.mockClear(); + + // 无选中时调用 clear,不再触发事件 + selection.clear(); + expect(selectionChangeHandler).toHaveBeenCalledTimes(0); + expect(selection.selected).toEqual([]); + selectionChangeHandler.mockClear(); + }); + + it('add 方法', () => { + const project = new Project(designer, { + componentsTree: [ + formSchema, + ], + }); + project.open(); + expect(project).toBeTruthy(); + const { currentDocument } = project; + const { nodesMap, selection } = currentDocument!; + const selectionChangeHandler = jest.fn(); + selection.onSelectionChange(selectionChangeHandler); + + selection.add('form'); + expect(selectionChangeHandler).toHaveBeenCalledTimes(1); + expect(selectionChangeHandler.mock.calls[0][0]).toEqual(['form']); + expect(selection.selected).toEqual(['form']); + selectionChangeHandler.mockClear(); + + // 再加一次相同的节点,不触发事件 + selection.add('form'); + expect(selectionChangeHandler).toHaveBeenCalledTimes(0); + expect(selection.selected).toEqual(['form']); + selectionChangeHandler.mockClear(); + + selection.add('form2'); + expect(selectionChangeHandler).toHaveBeenCalledTimes(1); + expect(selectionChangeHandler.mock.calls[0][0]).toEqual(['form', 'form2']); + expect(selection.selected).toEqual(['form', 'form2']); + selectionChangeHandler.mockClear(); + }); + + it('dispose 方法', () => { + const project = new Project(designer, { + componentsTree: [ + formSchema, + ], + }); + project.open(); + expect(project).toBeTruthy(); + const { currentDocument } = project; + const { nodesMap, selection } = currentDocument!; + + selection.selectAll(['form', 'node_k1ow3cbj', 'form2']); + + const selectionChangeHandler = jest.fn(); + selection.onSelectionChange(selectionChangeHandler); + selection.dispose(); + + expect(selectionChangeHandler).toHaveBeenCalledTimes(1); + expect(selectionChangeHandler.mock.calls[0][0]).toEqual(['form', 'node_k1ow3cbj']); + expect(selection.selected).toEqual(['form', 'node_k1ow3cbj']); + selectionChangeHandler.mockClear(); + }); + + it('dispose 方法', () => { + const project = new Project(designer, { + componentsTree: [ + formSchema, + ], + }); + project.open(); + expect(project).toBeTruthy(); + const { currentDocument } = project; + const { nodesMap, selection } = currentDocument!; + + selection.selectAll(['form', 'node_k1ow3cbj', 'form2']); + + const selectionChangeHandler = jest.fn(); + selection.onSelectionChange(selectionChangeHandler); + selection.dispose(); + + expect(selectionChangeHandler).toHaveBeenCalledTimes(1); + expect(selectionChangeHandler.mock.calls[0][0]).toEqual(['form', 'node_k1ow3cbj']); + expect(selection.selected).toEqual(['form', 'node_k1ow3cbj']); + selectionChangeHandler.mockClear(); + }); + + it('containsNode 方法', () => { + const project = new Project(designer, { + componentsTree: [ + formSchema, + ], + }); + project.open(); + expect(project).toBeTruthy(); + const { currentDocument } = project; + const { nodesMap, selection } = currentDocument!; + const selectionChangeHandler = jest.fn(); + selection.onSelectionChange(selectionChangeHandler); + + selection.select('form'); + expect(selectionChangeHandler).toHaveBeenCalledTimes(1); + expect(selectionChangeHandler.mock.calls[0][0]).toEqual(['form']); + expect(selection.selected).toEqual(['form']); + expect(selection.has('form')).toBe(true); + expect(selection.containsNode(currentDocument?.getNode('form'))).toBe(true); + expect(selection.containsNode(currentDocument?.getNode('node_k1ow3cbj'))).toBe(true); + expect(selection.containsNode(currentDocument?.getNode('node_k1ow3cb9'))).toBe(false); + expect(selection.getNodes()).toEqual([currentDocument?.getNode('form')]); + selectionChangeHandler.mockClear(); + + selection.add('node_k1ow3cbj'); + expect(selection.selected).toEqual(['form', 'node_k1ow3cbj']); + expect(selection.getTopNodes()).toEqual([currentDocument?.getNode('form')]); + expect(selection.getTopNodes(true)).toEqual([currentDocument?.getNode('form')]); + }); + + it('containsNode 方法 - excludeRoot: true', () => { + const project = new Project(designer, { + componentsTree: [ + formSchema, + ], + }); + project.open(); + expect(project).toBeTruthy(); + const { currentDocument } = project; + const { nodesMap, selection } = currentDocument!; + const selectionChangeHandler = jest.fn(); + selection.onSelectionChange(selectionChangeHandler); + + selection.select('node_k1ow3cb9'); + expect(selectionChangeHandler).toHaveBeenCalledTimes(1); + expect(selectionChangeHandler.mock.calls[0][0]).toEqual(['node_k1ow3cb9']); + expect(selection.selected).toEqual(['node_k1ow3cb9']); + expect(selection.has('node_k1ow3cb9')).toBe(true); + expect(selection.containsNode(currentDocument?.getNode('form'))).toBe(true); + expect(selection.containsNode(currentDocument?.getNode('form'), true)).toBe(false); + selectionChangeHandler.mockClear(); + }); + + it('containsNode 方法 - excludeRoot: true', () => { + const project = new Project(designer, { + componentsTree: [ + formSchema, + ], + }); + project.open(); + expect(project).toBeTruthy(); + const { currentDocument } = project; + const { nodesMap, selection } = currentDocument!; + const selectionChangeHandler = jest.fn(); + const dispose = selection.onSelectionChange(selectionChangeHandler); + + selection.select('form'); + expect(selectionChangeHandler).toHaveBeenCalledTimes(1); + expect(selectionChangeHandler.mock.calls[0][0]).toEqual(['form']); + selectionChangeHandler.mockClear(); + + // dispose 后,selected 会被赋值,但是变更事件不会被触发 + dispose(); + selection.select('node_k1ow3cb9'); + expect(selectionChangeHandler).toHaveBeenCalledTimes(0); + expect(selection.selected).toEqual(['node_k1ow3cb9']); + selectionChangeHandler.mockClear(); + }); +}); \ No newline at end of file diff --git a/packages/designer/tests/fixtures/component-metadata/div.ts b/packages/designer/tests/fixtures/component-metadata/div.ts new file mode 100644 index 000000000..27bcd18fc --- /dev/null +++ b/packages/designer/tests/fixtures/component-metadata/div.ts @@ -0,0 +1,272 @@ +export default { + componentName: 'Div', + title: '容器', + docUrl: 'http://gitlab.alibaba-inc.com/vision-components/vc-block/blob/master/README.md', + devMode: 'procode', + tags: ['布局'], + configure: { + props: [ + { + type: 'field', + name: 'behavior', + title: '默认状态', + extraProps: { + display: 'inline', + defaultValue: 'NORMAL', + }, + setter: { + componentName: 'MixedSetter', + props: { + setters: [ + { + key: null, + ref: null, + props: { + options: [ + { + title: '普通', + value: 'NORMAL', + }, + { + title: '隐藏', + value: 'HIDDEN', + }, + ], + loose: false, + cancelable: false, + }, + _owner: null, + }, + 'VariableSetter', + ], + }, + }, + }, + { + type: 'field', + name: '__style__', + title: { + label: '样式设置', + tip: '点击 ? 查看样式设置器用法指南', + docUrl: 'https://lark.alipay.com/legao/help/design-tool-style', + }, + extraProps: { + display: 'accordion', + defaultValue: {}, + }, + setter: { + key: null, + ref: null, + props: { + advanced: true, + }, + _owner: null, + }, + }, + { + type: 'group', + name: 'groupkgzzeo41', + title: '高级', + extraProps: { + display: 'accordion', + }, + items: [ + { + type: 'field', + name: 'fieldId', + title: { + label: '唯一标识', + }, + extraProps: { + display: 'block', + }, + setter: { + key: null, + ref: null, + props: { + placeholder: '请输入唯一标识', + multiline: false, + rows: 10, + required: false, + pattern: null, + maxLength: null, + }, + _owner: null, + }, + }, + { + type: 'field', + name: 'useFieldIdAsDomId', + title: { + label: '将唯一标识用作 DOM ID', + }, + extraProps: { + display: 'block', + defaultValue: false, + }, + setter: { + key: null, + ref: null, + props: {}, + _owner: null, + }, + }, + { + type: 'field', + name: 'customClassName', + title: '自定义样式类', + extraProps: { + display: 'block', + defaultValue: '', + }, + setter: { + componentName: 'MixedSetter', + props: { + setters: [ + { + key: null, + ref: null, + props: { + placeholder: null, + multiline: false, + rows: 10, + required: false, + pattern: null, + maxLength: null, + }, + _owner: null, + }, + 'VariableSetter', + ], + }, + }, + }, + { + type: 'field', + name: 'events', + title: { + label: '动作设置', + tip: '点击 ? 查看如何设置组件的事件响应动作', + docUrl: 'https://lark.alipay.com/legao/legao/events-call', + }, + extraProps: { + display: 'accordion', + defaultValue: { + ignored: true, + }, + }, + setter: { + key: null, + ref: null, + props: { + events: [ + { + name: 'onClick', + title: '当点击时', + initialValue: + "/**\n * 容器 当点击时\n */\nfunction onClick(event) {\n console.log('onClick', event);\n}", + }, + { + name: 'onMouseEnter', + title: '当鼠标进入时', + initialValue: + "/**\n * 容器 当鼠标进入时\n */\nfunction onMouseEnter(event) {\n console.log('onMouseEnter', event);\n}", + }, + { + name: 'onMouseLeave', + title: '当鼠标离开时', + initialValue: + "/**\n * 容器 当鼠标离开时\n */\nfunction onMouseLeave(event) {\n console.log('onMouseLeave', event);\n}", + }, + ], + }, + _owner: null, + }, + }, + { + type: 'field', + name: 'onClick', + extraProps: { + defaultValue: { + ignored: true, + }, + }, + setter: 'I18nSetter', + }, + { + type: 'field', + name: 'onMouseEnter', + extraProps: { + defaultValue: { + ignored: true, + }, + }, + setter: 'I18nSetter', + }, + { + type: 'field', + name: 'onMouseLeave', + extraProps: { + defaultValue: { + ignored: true, + }, + }, + setter: 'I18nSetter', + }, + ], + }, + ], + component: { + isContainer: true, + nestingRule: {}, + }, + supports: {}, + }, + experimental: { + callbacks: {}, + initials: [ + { + name: 'behavior', + }, + { + name: '__style__', + }, + { + name: 'fieldId', + }, + { + name: 'useFieldIdAsDomId', + }, + { + name: 'customClassName', + }, + { + name: 'events', + }, + { + name: 'onClick', + }, + { + name: 'onMouseEnter', + }, + { + name: 'onMouseLeave', + }, + ], + filters: [ + { + name: 'events', + }, + { + name: 'onClick', + }, + { + name: 'onMouseEnter', + }, + { + name: 'onMouseLeave', + }, + ], + autoruns: [], + }, +}; diff --git a/packages/designer/tests/fixtures/schema/form.ts b/packages/designer/tests/fixtures/schema/form.ts index 8438c8b39..903b21756 100644 --- a/packages/designer/tests/fixtures/schema/form.ts +++ b/packages/designer/tests/fixtures/schema/form.ts @@ -1,6 +1,7 @@ export default { componentName: 'Page', id: 'node_k1ow3cb9', + title: 'hey, i\' a page!', props: { extensions: { 启用页头: true, @@ -111,7 +112,8 @@ export default { children: [ { componentName: 'Form', - id: 'node_k1ow3cbq', + id: 'form', + extraPropA: 'extraPropA', props: { size: 'medium', labelAlign: 'top', @@ -123,9 +125,15 @@ export default { type: 'variable', variable: 'state.formData', }, + obj: { + a: 1, + b: false, + c: 'string', + }, __style__: {}, fieldId: 'form', fieldOptions: {}, + slotA: '', }, condition: true, children: [ @@ -949,6 +957,13 @@ export default { }, __style__: ':root {\n width: 80px;\n}', fieldId: 'button_k1ow3h1p', + greeting: { + // type: 'JSSlot', + value: [{ + componentName: 'Text', + props: {}, + }] + } }, condition: true, }, diff --git a/packages/designer/tests/meta/component-meta.test.ts b/packages/designer/tests/meta/component-meta.test.ts new file mode 100644 index 000000000..cec01ba5f --- /dev/null +++ b/packages/designer/tests/meta/component-meta.test.ts @@ -0,0 +1,30 @@ +import set from 'lodash/set'; +import cloneDeep from 'lodash/cloneDeep'; +import '../fixtures/window'; +import { Node } from '../../src/document/node/node'; +import { Designer } from '../../src/designer/designer'; +import divMeta from '../fixtures/component-metadata/div'; +import { ComponentMeta } from '../../src/component-meta'; + +const mockCreateSettingEntry = jest.fn(); +jest.mock('../../src/designer/designer', () => { + return { + Designer: jest.fn().mockImplementation(() => { + return { + getGlobalComponentActions: () => [], + }; + }), + }; +}); + +let designer = null; +beforeAll(() => { + designer = new Designer({}); +}); + +describe('组件元数据处理', () => { + it('构造函数', () => { + const meta = new ComponentMeta(designer, divMeta); + console.log(meta); + }); +}); \ No newline at end of file diff --git a/packages/designer/tests/node/node.add.test.ts b/packages/designer/tests/node/node.add.test.ts new file mode 100644 index 000000000..9da7a3473 --- /dev/null +++ b/packages/designer/tests/node/node.add.test.ts @@ -0,0 +1,564 @@ +import set from 'lodash/set'; +import cloneDeep from 'lodash/cloneDeep'; +import '../fixtures/window'; +import { Project } from '../../src/project/project'; +import { Node } from '../../src/document/node/node'; +import { Designer } from '../../src/designer/designer'; +import formSchema from '../fixtures/schema/form'; +import { getIdsFromSchema, getNodeFromSchemaById } from '../utils'; +import { EBADF } from 'constants'; + +const mockCreateSettingEntry = jest.fn(); +jest.mock('../../src/designer/designer', () => { + return { + Designer: jest.fn().mockImplementation(() => { + return { + getComponentMeta() { + return { + getMetadata() { + return { experimental: null }; + }, + }; + }, + transformProps(props) { return props; }, + createSettingEntry: mockCreateSettingEntry, + postEvent() {}, + }; + }), + }; +}); + +let designer = null; +beforeAll(() => { + designer = new Designer({}); +}); + +describe('schema 生成节点模型测试', () => { + describe('block ❌ | component ❌ | slot ❌', () => { + let project: Project; + beforeEach(() => { + project = new Project(designer, { + componentsTree: [ + formSchema, + ], + }); + project.open(); + }); + afterEach(() => { + project.unload(); + }); + it('基本的节点模型初始化,模型导出', () => { + expect(project).toBeTruthy(); + const { currentDocument } = project; + const { nodesMap } = currentDocument; + const ids = getIdsFromSchema(formSchema); + const expectedNodeCnt = ids.length; + expect(nodesMap.size).toBe(expectedNodeCnt); + ids.forEach(id => { + expect(nodesMap.get(id).componentName).toBe(getNodeFromSchemaById(formSchema, id).componentName); + }); + + const pageNode = currentDocument?.getNode('node_k1ow3cb9'); + expect(pageNode?.getComponentName()).toBe('Page'); + expect(pageNode?.getIcon()).toBeUndefined; + + const exportSchema = currentDocument?.export(1); + expect(getIdsFromSchema(exportSchema).length).toBe(expectedNodeCnt); + expect(mockCreateSettingEntry).toBeCalledTimes(expectedNodeCnt); + }); + + it('基本的节点模型初始化,节点深度', () => { + expect(project).toBeTruthy(); + const { currentDocument } = project; + const getNode = currentDocument.getNode.bind(currentDocument); + + const pageNode = getNode('node_k1ow3cb9'); + const rootHeaderNode = getNode('node_k1ow3cba'); + const rootContentNode = getNode('node_k1ow3cbb'); + const rootFooterNode = getNode('node_k1ow3cbc'); + const formNode = getNode('form'); + const cardNode = getNode('node_k1ow3cbj'); + const cardContentNode = getNode('node_k1ow3cbk'); + const columnsLayoutNode = getNode('node_k1ow3cbw'); + const columnNode = getNode('node_k1ow3cbx'); + const textFieldNode = getNode('node_k1ow3cbz'); + + expect(pageNode?.zLevel).toBe(0); + expect(rootHeaderNode?.zLevel).toBe(1); + expect(rootContentNode?.zLevel).toBe(1); + expect(rootFooterNode?.zLevel).toBe(1); + expect(formNode?.zLevel).toBe(2); + expect(cardNode?.zLevel).toBe(3); + expect(cardContentNode?.zLevel).toBe(4); + expect(columnsLayoutNode?.zLevel).toBe(5); + expect(columnNode?.zLevel).toBe(6); + expect(textFieldNode?.zLevel).toBe(7); + + expect(textFieldNode?.getZLevelTop(7)).toEqual(textFieldNode); + expect(textFieldNode?.getZLevelTop(6)).toEqual(columnNode); + expect(textFieldNode?.getZLevelTop(5)).toEqual(columnsLayoutNode); + expect(textFieldNode?.getZLevelTop(4)).toEqual(cardContentNode); + expect(textFieldNode?.getZLevelTop(3)).toEqual(cardNode); + expect(textFieldNode?.getZLevelTop(2)).toEqual(formNode); + expect(textFieldNode?.getZLevelTop(1)).toEqual(rootContentNode); + expect(textFieldNode?.getZLevelTop(0)).toEqual(pageNode); + + // 异常情况 + expect(textFieldNode?.getZLevelTop(8)).toBeNull; + expect(textFieldNode?.getZLevelTop(-1)).toBeNull; + }); + + it('基本的节点模型初始化,节点父子、兄弟相关方法', () => { + expect(project).toBeTruthy(); + const { currentDocument } = project; + const getNode = currentDocument.getNode.bind(currentDocument); + + const pageNode = getNode('node_k1ow3cb9'); + const rootHeaderNode = getNode('node_k1ow3cba'); + const rootContentNode = getNode('node_k1ow3cbb'); + const rootFooterNode = getNode('node_k1ow3cbc'); + const formNode = getNode('form'); + const cardNode = getNode('node_k1ow3cbj'); + const cardContentNode = getNode('node_k1ow3cbk'); + const columnsLayoutNode = getNode('node_k1ow3cbw'); + const columnNode = getNode('node_k1ow3cbx'); + const textFieldNode = getNode('node_k1ow3cbz'); + + expect(pageNode?.index).toBe(-1); + expect(pageNode?.children.toString()).toBe('[object Array]'); + expect(pageNode?.children?.get(1)).toBe(rootContentNode); + expect(pageNode?.getChildren()?.get(1)).toBe(rootContentNode); + expect(pageNode?.getNode()).toBe(pageNode); + + expect(rootFooterNode?.index).toBe(2); + + expect(textFieldNode?.getParent()).toBe(columnNode); + expect(columnNode?.getParent()).toBe(columnsLayoutNode); + expect(columnsLayoutNode?.getParent()).toBe(cardContentNode); + expect(cardContentNode?.getParent()).toBe(cardNode); + expect(cardNode?.getParent()).toBe(formNode); + expect(formNode?.getParent()).toBe(rootContentNode); + expect(rootContentNode?.getParent()).toBe(pageNode); + expect(rootContentNode?.prevSibling).toBe(rootHeaderNode); + expect(rootContentNode?.nextSibling).toBe(rootFooterNode); + + expect(pageNode?.isRoot()).toBe(true); + expect(pageNode?.contains(textFieldNode)).toBe(true); + expect(textFieldNode?.getRoot()).toBe(pageNode); + expect(columnNode?.getRoot()).toBe(pageNode); + expect(columnsLayoutNode?.getRoot()).toBe(pageNode); + expect(cardContentNode?.getRoot()).toBe(pageNode); + expect(cardNode?.getRoot()).toBe(pageNode); + expect(formNode?.getRoot()).toBe(pageNode); + expect(rootContentNode?.getRoot()).toBe(pageNode); + }); + + it('基本的节点模型初始化,节点新建、删除等事件', () => { + expect(project).toBeTruthy(); + const { currentDocument } = project; + const getNode = currentDocument.getNode.bind(currentDocument); + const createNode = currentDocument.createNode.bind(currentDocument); + + const pageNode = getNode('node_k1ow3cb9'); + const nodeCreateHandler = jest.fn(); + currentDocument?.onNodeCreate(nodeCreateHandler); + + const node = createNode({ + componentName: 'TextInput', + props: { + propA: 'haha', + } + }); + currentDocument?.insertNode(pageNode, node); + + expect(nodeCreateHandler).toHaveBeenCalledTimes(1); + expect(nodeCreateHandler.mock.calls[0][0]).toBe(node); + expect(nodeCreateHandler.mock.calls[0][0].componentName).toBe('TextInput'); + expect(nodeCreateHandler.mock.calls[0][0].getPropValue('propA')).toBe('haha'); + + const nodeDestroyHandler = jest.fn(); + currentDocument?.onNodeDestroy(nodeDestroyHandler); + node.remove(); + expect(nodeDestroyHandler).toHaveBeenCalledTimes(1); + expect(nodeDestroyHandler.mock.calls[0][0]).toBe(node); + expect(nodeDestroyHandler.mock.calls[0][0].componentName).toBe('TextInput'); + expect(nodeDestroyHandler.mock.calls[0][0].getPropValue('propA')).toBe('haha'); + }); + + it.skip('基本的节点模型初始化,节点插入等方法', () => { + expect(project).toBeTruthy(); + const { currentDocument } = project; + const getNode = currentDocument.getNode.bind(currentDocument); + + const formNode = getNode('form'); + const node1 = currentDocument.createNode({ + componentName: 'TextInput', + props: { + propA: 'haha', + } + }); + const node2 = currentDocument.createNode({ + componentName: 'TextInput', + props: { + propA: 'heihei', + } + }); + const node3 = currentDocument.createNode({ + componentName: 'TextInput', + props: { + propA: 'heihei2', + } + }); + const node4 = currentDocument.createNode({ + componentName: 'TextInput', + props: { + propA: 'heihei3', + } + }); + + formNode?.insertBefore(node2); + // formNode?.insertBefore(node1, node2); + // formNode?.insertAfter(node3); + // formNode?.insertAfter(node4, node3); + + expect(formNode?.children?.get(0)).toBe(node1); + expect(formNode?.children?.get(1)).toBe(node2); + // expect(formNode?.children?.get(5)).toBe(node3); + // expect(formNode?.children?.get(6)).toBe(node4); + }); + + it('基本的节点模型初始化,节点其他方法', () => { + expect(project).toBeTruthy(); + const { currentDocument } = project; + const getNode = currentDocument.getNode.bind(currentDocument); + + const pageNode = getNode('node_k1ow3cb9'); + expect(pageNode?.isPage()).toBe(true); + expect(pageNode?.isComponent()).toBe(false); + expect(pageNode?.isSlot()).toBe(false); + expect(pageNode?.title).toBe('hey, i\' a page!'); + }); + + describe('节点新增(insertNode)', () => { + let project: Project; + beforeEach(() => { + project = new Project(designer, { + componentsTree: [ + formSchema, + ], + }); + project.open(); + }); + it('场景一:插入 NodeSchema,不指定 index', () => { + expect(project).toBeTruthy(); + const ids = getIdsFromSchema(formSchema); + const { currentDocument } = project; + const { nodesMap } = currentDocument; + const formNode = nodesMap.get('form') as Node; + const formNode2 = currentDocument?.getNode('form'); + expect(formNode).toEqual(formNode2); + currentDocument?.insertNode(formNode, { + componentName: 'TextInput', + id: 'nodeschema-id1', + props: { + propA: 'haha', + propB: 3 + } + }); + expect(nodesMap.size).toBe(ids.length + 1); + expect(formNode.children?.length).toBe(4); + const insertedNode = formNode.children.get(formNode.children.length - 1); + expect(insertedNode.componentName).toBe('TextInput'); + expect(insertedNode.propsData).toEqual({ + propA: 'haha', + propB: 3 + }); + // TODO: 把 checkId 的 commit pick 过来 + // expect(nodesMap.get('nodeschema-id1').componentName).toBe('TextInput'); + }); + + it('场景一:插入 NodeSchema,指定 index: 0', () => { + expect(project).toBeTruthy(); + const ids = getIdsFromSchema(formSchema); + const { currentDocument } = project; + const nodesMap = currentDocument.nodesMap; + const formNode = nodesMap.get('form'); + currentDocument?.insertNode(formNode, { + componentName: 'TextInput', + id: 'nodeschema-id1', + props: { + propA: 'haha', + propB: 3 + } + }, 0); + expect(nodesMap.size).toBe(ids.length + 1); + expect(formNode.children.length).toBe(4); + const insertedNode = formNode.children.get(0); + expect(insertedNode.componentName).toBe('TextInput'); + expect(insertedNode.propsData).toEqual({ + propA: 'haha', + propB: 3 + }); + // TODO: 把 checkId 的 commit pick 过来 + // expect(nodesMap.get('nodeschema-id1').componentName).toBe('TextInput'); + }); + + it('场景一:插入 NodeSchema,指定 index: 1', () => { + expect(project).toBeTruthy(); + const ids = getIdsFromSchema(formSchema); + const { currentDocument } = project; + const { nodesMap } = currentDocument; + const formNode = nodesMap.get('form'); + currentDocument?.insertNode(formNode, { + componentName: 'TextInput', + id: 'nodeschema-id1', + props: { + propA: 'haha', + propB: 3 + } + }, 1); + expect(nodesMap.size).toBe(ids.length + 1); + expect(formNode.children.length).toBe(4); + const insertedNode = formNode.children.get(1); + expect(insertedNode.componentName).toBe('TextInput'); + expect(insertedNode.propsData).toEqual({ + propA: 'haha', + propB: 3 + }); + // TODO: 把 checkId 的 commit pick 过来 + // expect(nodesMap.get('nodeschema-id1').componentName).toBe('TextInput'); + }); + + it('场景一:插入 NodeSchema,有 children', () => { + expect(project).toBeTruthy(); + const ids = getIdsFromSchema(formSchema); + const { currentDocument } = project; + const { nodesMap } = currentDocument; + const formNode = nodesMap.get('form') as Node; + currentDocument?.insertNode(formNode, { + componentName: 'ParentNode', + props: { + propA: 'haha', + propB: 3 + }, + children: [ + { + componentName: 'SubNode', + props: { + propA: 'haha', + propB: 3 + } + }, + { + componentName: 'SubNode2', + props: { + propA: 'haha', + propB: 3 + } + } + ] + }); + expect(nodesMap.size).toBe(ids.length + 3); + expect(formNode.children.length).toBe(4); + expect(formNode.children?.get(3)?.componentName).toBe('ParentNode'); + expect(formNode.children?.get(3)?.children?.get(0)?.componentName).toBe('SubNode'); + expect(formNode.children?.get(3)?.children?.get(1)?.componentName).toBe('SubNode2'); + }); + + it.skip('场景一:插入 NodeSchema,id 与现有 schema 里的 id 重复', () => { + expect(project).toBeTruthy(); + const ids = getIdsFromSchema(formSchema); + const { currentDocument } = project; + const { nodesMap } = currentDocument; + const formNode = nodesMap.get('form'); + currentDocument?.insertNode(formNode, { + componentName: 'TextInput', + id: 'nodeschema-id1', + props: { + propA: 'haha', + propB: 3 + } + }); + expect(nodesMap.get('nodeschema-id1').componentName).toBe('TextInput'); + expect(nodesMap.size).toBe(ids.length + 1); + }); + + it.skip('场景一:插入 NodeSchema,id 与现有 schema 里的 id 重复,但关闭了 id 检测器', () => { + expect(project).toBeTruthy(); + const ids = getIdsFromSchema(formSchema); + const { currentDocument } = project; + const { nodesMap } = currentDocument; + const formNode = nodesMap.get('form'); + currentDocument?.insertNode(formNode, { + componentName: 'TextInput', + id: 'nodeschema-id1', + props: { + propA: 'haha', + propB: 3 + } + }); + expect(nodesMap.get('nodeschema-id1').componentName).toBe('TextInput'); + expect(nodesMap.size).toBe(ids.length + 1); + }); + + it('场景二:插入 Node 实例', () => { + expect(project).toBeTruthy(); + const ids = getIdsFromSchema(formSchema); + const { currentDocument } = project; + const { nodesMap } = currentDocument; + const formNode = nodesMap.get('form'); + const inputNode = currentDocument?.createNode({ + componentName: 'TextInput', + id: 'nodeschema-id2', + props: { + propA: 'haha', + propB: 3 + } + }); + currentDocument?.insertNode(formNode, inputNode); + expect(formNode.children?.get(3)?.componentName).toBe('TextInput'); + expect(nodesMap.size).toBe(ids.length + 1); + }); + + it('场景三:插入 JSExpression', () => { + expect(project).toBeTruthy(); + const ids = getIdsFromSchema(formSchema); + const { currentDocument } = project; + const { nodesMap } = currentDocument; + const formNode = nodesMap.get('form') as Node; + currentDocument?.insertNode(formNode, { + type: 'JSExpression', + value: 'just a expression' + }); + expect(nodesMap.size).toBe(ids.length + 1); + expect(formNode.children?.get(3)?.componentName).toBe('Leaf'); + // expect(formNode.children?.get(3)?.children).toEqual({ + // type: 'JSExpression', + // value: 'just a expression' + // }); + }); + it('场景四:插入 string', () => { + expect(project).toBeTruthy(); + const ids = getIdsFromSchema(formSchema); + const { currentDocument } = project; + const { nodesMap } = currentDocument; + const formNode = nodesMap.get('form') as Node; + currentDocument?.insertNode(formNode, 'just a string'); + expect(nodesMap.size).toBe(ids.length + 1); + expect(formNode.children?.get(3)?.componentName).toBe('Leaf'); + // expect(formNode.children?.get(3)?.children).toBe('just a string'); + }); + }); + + describe('节点新增(insertNodes)', () => { + let project: Project; + beforeEach(() => { + project = new Project(designer, { + componentsTree: [ + formSchema, + ], + }); + project.open(); + }); + it('场景一:插入 NodeSchema,指定 index', () => { + expect(project).toBeTruthy(); + const ids = getIdsFromSchema(formSchema); + const { currentDocument } = project; + const { nodesMap } = currentDocument; + const formNode = nodesMap.get('form') as Node; + const formNode2 = currentDocument?.getNode('form'); + expect(formNode).toEqual(formNode2); + currentDocument?.insertNodes(formNode, [ + { + componentName: 'TextInput', + props: { + propA: 'haha2', + propB: 3 + } + }, + { + componentName: 'TextInput2', + props: { + propA: 'haha', + propB: 3 + } + } + ], 1); + expect(nodesMap.size).toBe(ids.length + 2); + expect(formNode.children?.length).toBe(5); + const insertedNode1 = formNode.children.get(1); + const insertedNode2 = formNode.children.get(2); + expect(insertedNode1.componentName).toBe('TextInput'); + expect(insertedNode1.propsData).toEqual({ + propA: 'haha2', + propB: 3 + }); + expect(insertedNode2.componentName).toBe('TextInput2'); + expect(insertedNode2.propsData).toEqual({ + propA: 'haha', + propB: 3 + }); + }); + + it('场景二:插入 Node 实例,指定 index', () => { + expect(project).toBeTruthy(); + const ids = getIdsFromSchema(formSchema); + const { currentDocument } = project; + const { nodesMap } = currentDocument; + const formNode = nodesMap.get('form') as Node; + const formNode2 = currentDocument?.getNode('form'); + expect(formNode).toEqual(formNode2); + const createdNode1 = currentDocument?.createNode({ + componentName: 'TextInput', + props: { + propA: 'haha2', + propB: 3 + } + }); + const createdNode2 = currentDocument?.createNode({ + componentName: 'TextInput2', + props: { + propA: 'haha', + propB: 3 + } + }); + currentDocument?.insertNodes(formNode, [ createdNode1, createdNode2 ], 1); + expect(nodesMap.size).toBe(ids.length + 2); + expect(formNode.children?.length).toBe(5); + const insertedNode1 = formNode.children.get(1); + const insertedNode2 = formNode.children.get(2); + expect(insertedNode1.componentName).toBe('TextInput'); + expect(insertedNode1.propsData).toEqual({ + propA: 'haha2', + propB: 3 + }); + expect(insertedNode2.componentName).toBe('TextInput2'); + expect(insertedNode2.propsData).toEqual({ + propA: 'haha', + propB: 3 + }); + }); + }); + }) + + describe('block ❌ | component ❌ | slot ✅', () => { + it('基本的 slot 创建', () => { + const formSchemaWithSlot = set(cloneDeep(formSchema), 'children[0].children[0].props.title.type', 'JSSlot'); + const project = new Project(designer, { + componentsTree: [ + formSchemaWithSlot, + ], + }); + project.open(); + expect(project).toBeTruthy(); + const { currentDocument } = project; + const { nodesMap } = currentDocument; + const ids = getIdsFromSchema(formSchema); + // 目前每个 slot 会新增(1 + children.length)个节点 + const expectedNodeCnt = ids.length + 2; + expect(nodesMap.size).toBe(expectedNodeCnt); + // PageHeader + expect(nodesMap.get('node_k1ow3cbd').slots).toHaveLength(1); + }); + }); +}); \ No newline at end of file diff --git a/packages/designer/tests/node/node.dragdrop.test.ts b/packages/designer/tests/node/node.dragdrop.test.ts new file mode 100644 index 000000000..be15bdfa7 --- /dev/null +++ b/packages/designer/tests/node/node.dragdrop.test.ts @@ -0,0 +1,61 @@ +import set from 'lodash/set'; +import cloneDeep from 'lodash/cloneDeep'; +import '../fixtures/window'; +import { Project } from '../../src/project/project'; +import { Node } from '../../src/document/node/node'; +import { Designer } from '../../src/designer/designer'; +import formSchema from '../fixtures/schema/form'; +import { getIdsFromSchema, getNodeFromSchemaById } from '../utils'; + +const mockCreateSettingEntry = jest.fn(); +jest.mock('../../src/designer/designer', () => { + return { + Designer: jest.fn().mockImplementation(() => { + return { + getComponentMeta() { + return { + getMetadata() { + return { experimental: null }; + }, + }; + }, + transformProps(props) { return props; }, + createSettingEntry: mockCreateSettingEntry, + postEvent() {}, + }; + }), + }; +}); + +let designer = null; +beforeAll(() => { + designer = new Designer({}); +}); + +describe.skip('节点拖拽测试', () => { + describe('block ❌ | component ❌ | slot ❌', () => { + it('修改普通属性,string | number', () => { + const project = new Project(designer, { + componentsTree: [ + formSchema, + ], + }); + project.open(); + expect(project).toBeTruthy(); + const { currentDocument } = project; + const { nodesMap } = currentDocument; + const ids = getIdsFromSchema(formSchema); + const expectedNodeCnt = ids.length; + expect(nodesMap.size).toBe(expectedNodeCnt); + ids.forEach(id => { + expect(nodesMap.get(id).componentName).toBe(getNodeFromSchemaById(formSchema, id).componentName); + }); + + const exportSchema = currentDocument?.export(1); + expect(getIdsFromSchema(exportSchema).length).toBe(expectedNodeCnt); + expect(mockCreateSettingEntry).toBeCalledTimes(expectedNodeCnt); + }); + + + }); +}); \ No newline at end of file diff --git a/packages/designer/tests/node/node.modify.test.ts b/packages/designer/tests/node/node.modify.test.ts new file mode 100644 index 000000000..e064d273f --- /dev/null +++ b/packages/designer/tests/node/node.modify.test.ts @@ -0,0 +1,456 @@ +import set from 'lodash/set'; +import cloneDeep from 'lodash/cloneDeep'; +import '../fixtures/window'; +import { Project } from '../../src/project/project'; +import { Node } from '../../src/document/node/node'; +import { Designer } from '../../src/designer/designer'; +import formSchema from '../fixtures/schema/form'; +import { getIdsFromSchema, getNodeFromSchemaById } from '../utils'; + +const mockCreateSettingEntry = jest.fn(); +jest.mock('../../src/designer/designer', () => { + return { + Designer: jest.fn().mockImplementation(() => { + return { + getComponentMeta() { + return { + getMetadata() { + return { experimental: null }; + }, + }; + }, + transformProps(props) { return props; }, + createSettingEntry: mockCreateSettingEntry, + postEvent() {}, + }; + }), + }; +}); + +let designer = null; +beforeAll(() => { + designer = new Designer({}); +}); + +describe('schema 生成节点模型测试', () => { + describe('block ❌ | component ❌ | slot ❌', () => { + let project: Project; + beforeEach(() => { + project = new Project(designer, { + componentsTree: [ + formSchema, + ], + }); + project.open(); + }); + it('读取普通属性,string | number | object', () => { + expect(project).toBeTruthy(); + const { currentDocument } = project; + const { nodesMap } = currentDocument; + const ids = getIdsFromSchema(formSchema); + const expectedNodeCnt = ids.length; + const formNode = currentDocument?.getNode('form'); + /* + props: { + size: 'medium', + labelAlign: 'top', + autoValidate: true, + scrollToFirstError: true, + autoUnmount: true, + behavior: 'NORMAL', + dataSource: { + type: 'variable', + variable: 'state.formData', + }, + obj: { + a: 1, + b: false, + c: 'string', + }, + __style__: {}, + fieldId: 'form', + fieldOptions: {}, + }, + id: 'form', + condition: true, + */ + const sizeProp = formNode?.getProp('size'); + const sizeProp2 = formNode?.getProps().getProp('size'); + expect(sizeProp).toBe(sizeProp2); + expect(sizeProp?.getAsString()).toBe('medium'); + expect(sizeProp?.getValue()).toBe('medium'); + + const autoValidateProp = formNode?.getProp('autoValidate'); + expect(autoValidateProp?.getValue()).toBe(true); + + const objProp = formNode?.getProp('obj'); + expect(objProp?.getValue()).toEqual({ + a: 1, + b: false, + c: 'string', + }); + const objAProp = formNode?.getProp('obj.a'); + const objBProp = formNode?.getProp('obj.b'); + const objCProp = formNode?.getProp('obj.c'); + expect(objAProp?.getValue()).toBe(1); + expect(objBProp?.getValue()).toBe(false); + expect(objCProp?.getValue()).toBe('string'); + + const idProp = formNode?.getExtraProp('extraPropA'); + expect(idProp?.getValue()).toBe('extraPropA'); + }); + + it('修改普通属性,string | number | object,使用 Node 实例接口', () => { + expect(project).toBeTruthy(); + const { currentDocument } = project; + const { nodesMap } = currentDocument; + const ids = getIdsFromSchema(formSchema); + const expectedNodeCnt = ids.length; + const formNode = currentDocument?.getNode('form'); + /* + props: { + size: 'medium', + labelAlign: 'top', + autoValidate: true, + scrollToFirstError: true, + autoUnmount: true, + behavior: 'NORMAL', + dataSource: { + type: 'variable', + variable: 'state.formData', + }, + obj: { + a: 1, + b: false, + c: 'string', + }, + __style__: {}, + fieldId: 'form', + fieldOptions: {}, + }, + id: 'form', + condition: true, + */ + formNode?.setPropValue('size', 'large'); + const sizeProp = formNode?.getProp('size'); + expect(sizeProp?.getAsString()).toBe('large'); + expect(sizeProp?.getValue()).toBe('large'); + + formNode?.setPropValue('autoValidate', false); + const autoValidateProp = formNode?.getProp('autoValidate'); + expect(autoValidateProp?.getValue()).toBe(false); + + formNode?.setPropValue('obj', { + a: 2, + b: true, + c: 'another string' + }); + const objProp = formNode?.getProp('obj'); + expect(objProp?.getValue()).toEqual({ + a: 2, + b: true, + c: 'another string', + }); + formNode?.setPropValue('obj.a', 3); + formNode?.setPropValue('obj.b', false); + formNode?.setPropValue('obj.c', 'string'); + const objAProp = formNode?.getProp('obj.a'); + const objBProp = formNode?.getProp('obj.b'); + const objCProp = formNode?.getProp('obj.c'); + expect(objAProp?.getValue()).toBe(3); + expect(objBProp?.getValue()).toBe(false); + expect(objCProp?.getValue()).toBe('string'); + expect(objProp?.getValue()).toEqual({ + a: 3, + b: false, + c: 'string', + }); + }); + + it('修改普通属性,string | number | object,使用 Props 实例接口', () => { + expect(project).toBeTruthy(); + const { currentDocument } = project; + const { nodesMap } = currentDocument; + const ids = getIdsFromSchema(formSchema); + const expectedNodeCnt = ids.length; + const formNode = currentDocument?.getNode('form'); + /* + props: { + size: 'medium', + labelAlign: 'top', + autoValidate: true, + scrollToFirstError: true, + autoUnmount: true, + behavior: 'NORMAL', + dataSource: { + type: 'variable', + variable: 'state.formData', + }, + obj: { + a: 1, + b: false, + c: 'string', + }, + __style__: {}, + fieldId: 'form', + fieldOptions: {}, + }, + id: 'form', + condition: true, + */ + const props = formNode?.getProps(); + props?.setPropValue('size', 'large'); + const sizeProp = formNode?.getProp('size'); + expect(sizeProp?.getAsString()).toBe('large'); + expect(sizeProp?.getValue()).toBe('large'); + + props?.setPropValue('autoValidate', false); + const autoValidateProp = formNode?.getProp('autoValidate'); + expect(autoValidateProp?.getValue()).toBe(false); + + props?.setPropValue('obj', { + a: 2, + b: true, + c: 'another string' + }); + const objProp = formNode?.getProp('obj'); + expect(objProp?.getValue()).toEqual({ + a: 2, + b: true, + c: 'another string', + }); + props?.setPropValue('obj.a', 3); + props?.setPropValue('obj.b', false); + props?.setPropValue('obj.c', 'string'); + const objAProp = formNode?.getProp('obj.a'); + const objBProp = formNode?.getProp('obj.b'); + const objCProp = formNode?.getProp('obj.c'); + expect(objAProp?.getValue()).toBe(3); + expect(objBProp?.getValue()).toBe(false); + expect(objCProp?.getValue()).toBe('string'); + expect(objProp?.getValue()).toEqual({ + a: 3, + b: false, + c: 'string', + }); + }); + + it('修改普通属性,string | number | object,使用 Prop 实例接口', () => { + expect(project).toBeTruthy(); + const { currentDocument } = project; + const { nodesMap } = currentDocument; + const ids = getIdsFromSchema(formSchema); + const expectedNodeCnt = ids.length; + const formNode = currentDocument?.getNode('form'); + /* + props: { + size: 'medium', + labelAlign: 'top', + autoValidate: true, + scrollToFirstError: true, + autoUnmount: true, + behavior: 'NORMAL', + dataSource: { + type: 'variable', + variable: 'state.formData', + }, + obj: { + a: 1, + b: false, + c: 'string', + }, + __style__: {}, + fieldId: 'form', + fieldOptions: {}, + }, + id: 'form', + condition: true, + */ + const sizeProp = formNode?.getProp('size'); + sizeProp?.setValue('large'); + expect(sizeProp?.getAsString()).toBe('large'); + expect(sizeProp?.getValue()).toBe('large'); + + const autoValidateProp = formNode?.getProp('autoValidate'); + autoValidateProp?.setValue(false); + expect(autoValidateProp?.getValue()).toBe(false); + + + const objProp = formNode?.getProp('obj'); + objProp?.setValue({ + a: 2, + b: true, + c: 'another string' + }); + expect(objProp?.getValue()).toEqual({ + a: 2, + b: true, + c: 'another string', + }); + const objAProp = formNode?.getProp('obj.a'); + const objBProp = formNode?.getProp('obj.b'); + const objCProp = formNode?.getProp('obj.c'); + objAProp?.setValue(3); + objBProp?.setValue(false); + objCProp?.setValue('string'); + expect(objAProp?.getValue()).toBe(3); + expect(objBProp?.getValue()).toBe(false); + expect(objCProp?.getValue()).toBe('string'); + expect(objProp?.getValue()).toEqual({ + a: 3, + b: false, + c: 'string', + }); + }); + }); + + describe('block ❌ | component ❌ | slot ✅', () => { + let project: Project; + beforeEach(() => { + project = new Project(designer, { + componentsTree: [ + formSchema, + ], + }); + project.open(); + }); + it('修改 slot 属性,初始存在 slot 属性名,正常生成节点模型', () => { + expect(project).toBeTruthy(); + const { currentDocument } = project; + const { nodesMap } = currentDocument; + const ids = getIdsFromSchema(formSchema); + const expectedNodeCnt = ids.length; + const formNode = currentDocument?.getNode('form'); + + formNode?.setPropValue('slotA', { + type: 'JSSlot', + value: [{ + componentName: 'TextInput1', + props: { + txt: 'haha', + num: 1, + bool: true + } + }, { + componentName: 'TextInput2', + props: { + txt: 'heihei', + num: 2, + bool: false + } + }] + }); + + expect(nodesMap.size).toBe(ids.length + 3); + expect(formNode?.slots).toHaveLength(1); + expect(formNode?.slots[0].children).toHaveLength(2); + const firstChildNode = formNode?.slots[0].children?.get(0); + const secondChildNode = formNode?.slots[0].children?.get(1); + expect(firstChildNode?.componentName).toBe('TextInput1'); + expect(firstChildNode?.getPropValue('txt')).toBe('haha'); + expect(firstChildNode?.getPropValue('num')).toBe(1); + expect(firstChildNode?.getPropValue('bool')).toBe(true); + expect(secondChildNode?.componentName).toBe('TextInput2'); + expect(secondChildNode?.getPropValue('txt')).toBe('heihei'); + expect(secondChildNode?.getPropValue('num')).toBe(2); + expect(secondChildNode?.getPropValue('bool')).toBe(false); + }); + + it('修改 slot 属性,初始存在 slot 属性名,关闭 slot', () => { + expect(project).toBeTruthy(); + const { currentDocument } = project; + const { nodesMap } = currentDocument; + const ids = getIdsFromSchema(formSchema); + const expectedNodeCnt = ids.length; + const formNode = currentDocument?.getNode('form'); + + formNode?.setPropValue('slotA', { + type: 'JSSlot', + value: [{ + componentName: 'TextInput1', + props: { + txt: 'haha', + num: 1, + bool: true + } + }, { + componentName: 'TextInput2', + props: { + txt: 'heihei', + num: 2, + bool: false + } + }] + }); + + expect(nodesMap.size).toBe(ids.length + 3); + expect(formNode?.slots).toHaveLength(1); + + formNode?.setPropValue('slotA', ''); + + expect(nodesMap.size).toBe(ids.length); + expect(formNode?.slots).toHaveLength(0); + }); + + it('修改 slot 属性,初始存在 slot 属性名,同名覆盖 slot', () => { + expect(project).toBeTruthy(); + const { currentDocument } = project; + const { nodesMap } = currentDocument; + const ids = getIdsFromSchema(formSchema); + const expectedNodeCnt = ids.length; + const formNode = currentDocument?.getNode('form'); + + formNode?.setPropValue('slotA', { + type: 'JSSlot', + name: 'slotA', + value: [{ + componentName: 'TextInput1', + props: { + txt: 'haha', + num: 1, + bool: true + } + }, { + componentName: 'TextInput2', + props: { + txt: 'heihei', + num: 2, + bool: false + } + }] + }); + + expect(nodesMap.size).toBe(ids.length + 3); + expect(formNode?.slots).toHaveLength(1); + expect(formNode?.slots[0].children).toHaveLength(2); + + let firstChildNode = formNode?.slots[0].children?.get(0); + expect(firstChildNode?.componentName).toBe('TextInput1'); + expect(firstChildNode?.getPropValue('txt')).toBe('haha'); + expect(firstChildNode?.getPropValue('num')).toBe(1); + expect(firstChildNode?.getPropValue('bool')).toBe(true); + + formNode?.setPropValue('slotA', { + type: 'JSSlot', + name: 'slotA', + value: [{ + componentName: 'TextInput3', + props: { + txt: 'xixi', + num: 3, + bool: false + } + }] + }); + + expect(nodesMap.size).toBe(ids.length + 2); + expect(formNode?.slots).toHaveLength(1); + expect(formNode?.slots[0].children).toHaveLength(1); + firstChildNode = formNode?.slots[0].children?.get(0); + expect(firstChildNode?.componentName).toBe('TextInput3'); + expect(firstChildNode?.getPropValue('txt')).toBe('xixi'); + expect(firstChildNode?.getPropValue('num')).toBe(3); + expect(firstChildNode?.getPropValue('bool')).toBe(false); + }); + }); +}); \ No newline at end of file diff --git a/packages/designer/tests/node/node.remove.test.ts b/packages/designer/tests/node/node.remove.test.ts new file mode 100644 index 000000000..f68d57ca1 --- /dev/null +++ b/packages/designer/tests/node/node.remove.test.ts @@ -0,0 +1,122 @@ +import set from 'lodash/set'; +import cloneDeep from 'lodash/cloneDeep'; +import '../fixtures/window'; +import { Project } from '../../src/project/project'; +import { Node } from '../../src/document/node/node'; +import { Designer } from '../../src/designer/designer'; +import formSchema from '../fixtures/schema/form'; +import { getIdsFromSchema, getNodeFromSchemaById } from '../utils'; + +const mockCreateSettingEntry = jest.fn(); +jest.mock('../../src/designer/designer', () => { + return { + Designer: jest.fn().mockImplementation(() => { + return { + getComponentMeta() { + return { + getMetadata() { + return { experimental: null }; + }, + }; + }, + transformProps(props) { return props; }, + createSettingEntry: mockCreateSettingEntry, + postEvent() {}, + }; + }), + }; +}); + +let designer = null; +beforeAll(() => { + designer = new Designer({}); +}); + +describe('节点模型删除测试', () => { + it('删除叶子节点', () => { + const project = new Project(designer, { + componentsTree: [ + formSchema, + ], + }); + project.open(); + expect(project).toBeTruthy(); + const { currentDocument } = project; + const { nodesMap } = currentDocument; + const ids = getIdsFromSchema(formSchema); + const originalNodeCnt = ids.length; + expect(nodesMap.size).toBe(originalNodeCnt); + + currentDocument?.removeNode('node_k1ow3cbn'); + // Button#1 + expect(nodesMap.size).toBe(originalNodeCnt - 1); + + currentDocument?.removeNode(nodesMap.get('node_k1ow3cbp')); + // Button#2 + expect(nodesMap.size).toBe(originalNodeCnt - 2); + + currentDocument?.removeNode('unexisting_node'); + expect(nodesMap.size).toBe(originalNodeCnt - 2); + }); + + it('删除叶子节点,带有 slot', () => { + const formSchemaWithSlot = set(cloneDeep(formSchema), + 'children[1].children[0].children[2].children[1].props.greeting.type', 'JSSlot'); + const project = new Project(designer, { + componentsTree: [ + formSchemaWithSlot, + ], + }); + project.open(); + expect(project).toBeTruthy(); + const { currentDocument } = project; + const { nodesMap } = currentDocument; + const ids = getIdsFromSchema(formSchema); + const originalNodeCnt = ids.length + 2; + expect(nodesMap.size).toBe(originalNodeCnt); + + currentDocument?.removeNode('node_k1ow3cbp'); + // Button + Slot + Text + expect(nodesMap.size).toBe(originalNodeCnt - 3); + }); + + it('删除分支节点', () => { + const project = new Project(designer, { + componentsTree: [ + formSchema, + ], + }); + project.open(); + expect(project).toBeTruthy(); + const { currentDocument } = project; + const { nodesMap } = currentDocument; + const ids = getIdsFromSchema(formSchema); + const originalNodeCnt = ids.length; + expect(nodesMap.size).toBe(originalNodeCnt); + + currentDocument?.removeNode('node_k1ow3cbo'); + // Div + 2 * Button + expect(nodesMap.size).toBe(originalNodeCnt - 3); + }); + + it('删除分支节点,带有 slot', () => { + const formSchemaWithSlot = set(cloneDeep(formSchema), + 'children[1].children[0].children[2].children[1].props.greeting.type', 'JSSlot'); + const project = new Project(designer, { + componentsTree: [ + formSchemaWithSlot, + ], + }); + project.open(); + expect(project).toBeTruthy(); + const { currentDocument } = project; + const { nodesMap } = currentDocument; + const ids = getIdsFromSchema(formSchema); + const originalNodeCnt = ids.length + 2; + expect(nodesMap.size).toBe(originalNodeCnt); + + currentDocument?.removeNode('node_k1ow3cbo'); + // Div + 2 * Button + Slot + Text + expect(nodesMap.size).toBe(originalNodeCnt - 5); + }); +}); \ No newline at end of file diff --git a/packages/designer/tests/project/project.test.ts b/packages/designer/tests/project/project.test.ts index 54ccedf18..31ece4526 100644 --- a/packages/designer/tests/project/project.test.ts +++ b/packages/designer/tests/project/project.test.ts @@ -1,8 +1,8 @@ -import set from 'lodash.set'; -import cloneDeep from 'lodash.clonedeep'; +import set from 'lodash/set'; +import cloneDeep from 'lodash/clonedeep'; import '../fixtures/window'; import { Project } from '../../src/project/project'; -// import { Node } from '../../../src/document/node/node'; +import { Node } from '../../src/document/node/node'; import { Designer } from '../../src/designer/designer'; import formSchema from '../fixtures/schema/form'; import { getIdsFromSchema, getNodeFromSchemaById } from '../utils'; @@ -34,7 +34,11 @@ beforeAll(() => { describe('schema 生成节点模型测试', () => { describe('block ❌ | component ❌ | slot ❌', () => { - it('基本的节点模型初始化,模型导出', () => { + beforeEach(() => { + mockCreateSettingEntry.mockClear(); + }); + + it('基本的节点模型初始化,模型导出,初始化传入 schema', () => { const project = new Project(designer, { componentsTree: [ formSchema, @@ -56,129 +60,75 @@ describe('schema 生成节点模型测试', () => { expect(mockCreateSettingEntry).toBeCalledTimes(expectedNodeCnt); }); - describe('节点新增(insertNode)', () => { - let project; - beforeEach(() => { - project = new Project(designer, { - componentsTree: [ - formSchema, - ], - }); - project.open(); - }); - it.only('场景一:插入 NodeSchema', () => { - expect(project).toBeTruthy(); - const ids = getIdsFromSchema(formSchema); - const { currentDocument } = project; - const { nodesMap } = currentDocument; - const formNode = nodesMap.get('node_k1ow3cbq'); - currentDocument?.insertNode(formNode, { - componentName: 'TextInput', - id: 'nodeschema-id1', - props: { - propA: 'haha', - propB: 3 - } - }); - expect(nodesMap.get('nodeschema-id1').componentName).toBe('TextInput'); - expect(nodesMap.size).toBe(ids.length + 1); + it('基本的节点模型初始化,模型导出,project.open 传入 schema', () => { + const project = new Project(designer); + project.open(formSchema); + expect(project).toBeTruthy(); + const { currentDocument } = project; + const { nodesMap } = currentDocument; + const ids = getIdsFromSchema(formSchema); + const expectedNodeCnt = ids.length; + expect(nodesMap.size).toBe(expectedNodeCnt); + ids.forEach(id => { + expect(nodesMap.get(id).componentName).toBe(getNodeFromSchemaById(formSchema, id).componentName); }); - it.only('场景一:插入 NodeSchema,有 children', () => { - expect(project).toBeTruthy(); - const ids = getIdsFromSchema(formSchema); - const { currentDocument } = project; - const { nodesMap } = currentDocument; - const formNode = nodesMap.get('node_k1ow3cbq'); - currentDocument?.insertNode(formNode, { - componentName: 'TextInput', - id: 'nodeschema-id1', - props: { - propA: 'haha', - propB: 3 - } - }); - expect(nodesMap.get('nodeschema-id1').componentName).toBe('TextInput'); - expect(nodesMap.size).toBe(ids.length + 1); - }); - - it.only('场景一:插入 NodeSchema,id 与现有 schema 重复', () => { - expect(project).toBeTruthy(); - const ids = getIdsFromSchema(formSchema); - const { currentDocument } = project; - const { nodesMap } = currentDocument; - const formNode = nodesMap.get('node_k1ow3cbq'); - currentDocument?.insertNode(formNode, { - componentName: 'TextInput', - id: 'nodeschema-id1', - props: { - propA: 'haha', - propB: 3 - } - }); - expect(nodesMap.get('nodeschema-id1').componentName).toBe('TextInput'); - expect(nodesMap.size).toBe(ids.length + 1); - }); - - it.only('场景一:插入 NodeSchema,id 与现有 schema 重复,但关闭了 id 检测器', () => { - expect(project).toBeTruthy(); - const ids = getIdsFromSchema(formSchema); - const { currentDocument } = project; - const { nodesMap } = currentDocument; - const formNode = nodesMap.get('node_k1ow3cbq'); - currentDocument?.insertNode(formNode, { - componentName: 'TextInput', - id: 'nodeschema-id1', - props: { - propA: 'haha', - propB: 3 - } - }); - expect(nodesMap.get('nodeschema-id1').componentName).toBe('TextInput'); - expect(nodesMap.size).toBe(ids.length + 1); - }); - - it('场景二:插入 Node 实例', () => { - expect(project).toBeTruthy(); - const ids = getIdsFromSchema(formSchema); - const { currentDocument } = project; - const { nodesMap } = currentDocument; - const formNode = nodesMap.get('node_k1ow3cbq'); - const inputNode = currentDocument?.createNode({ - componentName: 'TextInput', - id: 'nodeschema-id2', - props: { - propA: 'haha', - propB: 3 - } - }); - expect(inputNode.id).toBe('nodeschema-id2'); - currentDocument?.insertNode(formNode, inputNode); - expect(nodesMap.get('nodeschema-id2').componentName).toBe('TextInput'); - expect(nodesMap.size).toBe(ids.length + 1); - }); - - it('场景二:插入 JSExpression', () => {}); + const exportSchema = currentDocument?.export(1); + expect(getIdsFromSchema(exportSchema).length).toBe(expectedNodeCnt); + expect(mockCreateSettingEntry).toBeCalledTimes(expectedNodeCnt); }); - }) + it('project 卸载所有 document - unload()', () => { + const project = new Project(designer); + project.open(formSchema); + expect(project).toBeTruthy(); + const { currentDocument, documents } = project; - it('block ❌ | component ❌ | slot ✅', () => { - const formSchemaWithSlot = set(cloneDeep(formSchema), 'children[0].children[0].props.title.type', 'JSSlot'); - const project = new Project(designer, { - componentsTree: [ - formSchemaWithSlot, - ], + expect(documents).toHaveLength(1); + expect(currentDocument).toBe(documents[0]); + + project.unload(); + + expect(documents).toHaveLength(0); }); - project.open(); - expect(project).toBeTruthy(); - const { currentDocument } = project; - const { nodesMap } = currentDocument; - const ids = getIdsFromSchema(formSchema); - // 目前每个 slot 会新增 1 + children.length 个节点 - const expectedNodeCnt = ids.length + 2; - expect(nodesMap.size).toBe(expectedNodeCnt); - // PageHeader - expect(nodesMap.get('node_k1ow3cbd').slots).toHaveLength(1); + + it('project 卸载指定 document - removeDocument()', () => { + const project = new Project(designer); + project.open(formSchema); + expect(project).toBeTruthy(); + const { currentDocument, documents } = project; + + expect(documents).toHaveLength(1); + expect(currentDocument).toBe(documents[0]); + + project.removeDocument(currentDocument); + + expect(documents).toHaveLength(0); + }); + }); + + describe('block ❌ | component ❌ | slot ✅', () => { + it('基本的节点模型初始化,模型导出,初始化传入 schema', () => { + const formSchemaWithSlot = set(cloneDeep(formSchema), 'children[0].children[0].props.title.type', 'JSSlot'); + const project = new Project(designer, { + componentsTree: [ + formSchemaWithSlot, + ], + }); + project.open(); + expect(project).toBeTruthy(); + const { currentDocument } = project; + const { nodesMap } = currentDocument!; + const ids = getIdsFromSchema(formSchema); + // 目前每个 slot 会新增(1 + children.length)个节点 + const expectedNodeCnt = ids.length + 2; + expect(nodesMap.size).toBe(expectedNodeCnt); + // PageHeader + expect(nodesMap.get('node_k1ow3cbd').slots).toHaveLength(1); + }); + }); + + describe.skip('多 document 测试', () => { + }); }); \ No newline at end of file diff --git a/packages/editor-preset-vision/src/editor.ts b/packages/editor-preset-vision/src/editor.ts index 6a1933c54..6d1cdd67d 100644 --- a/packages/editor-preset-vision/src/editor.ts +++ b/packages/editor-preset-vision/src/editor.ts @@ -127,7 +127,7 @@ designer.addPropsReducer((props, node) => { !isJSBlock(ov) && !isJSSlot(ov) && !isVariable(ov) && - isString(v)) { + (isString(v) || isI18NObject(v))) { newProps[item.name] = convertToI18NObject(v); } } catch (e) { diff --git a/packages/react-renderer/package.json b/packages/react-renderer/package.json index 9f46448c7..946612931 100644 --- a/packages/react-renderer/package.json +++ b/packages/react-renderer/package.json @@ -54,5 +54,5 @@ "publishConfig": { "registry": "http://registry.npm.alibaba-inc.com" }, - "homepage": "https://unpkg.alibaba-inc.com/@ali/lowcode-react-renderer@0.13.1-9/build/index.html" + "homepage": "https://unpkg.alibaba-inc.com/@ali/lowcode-react-renderer@0.13.1-10/build/index.html" } diff --git a/packages/react-simulator-renderer/.eslintrc.js b/packages/react-simulator-renderer/.eslintrc.js index 31d2e19c3..39a7d9067 100644 --- a/packages/react-simulator-renderer/.eslintrc.js +++ b/packages/react-simulator-renderer/.eslintrc.js @@ -11,6 +11,7 @@ module.exports = { 'no-shadow': 0, 'no-prototype-builtins': 0, 'array-callback-return': 0, - '@typescript-eslint/member-ordering': 0 + '@typescript-eslint/member-ordering': 0, + 'react/no-find-dom-node', 0 } } \ No newline at end of file diff --git a/packages/react-simulator-renderer/src/utils/react-find-dom-nodes.ts b/packages/react-simulator-renderer/src/utils/react-find-dom-nodes.ts index f7dde3925..36aa3bfb2 100644 --- a/packages/react-simulator-renderer/src/utils/react-find-dom-nodes.ts +++ b/packages/react-simulator-renderer/src/utils/react-find-dom-nodes.ts @@ -1,4 +1,5 @@ import { ReactInstance } from 'react'; +import { findDOMNode } from 'react-dom'; import { isElement } from '@ali/lowcode-utils'; import { isDOMNode } from './is-dom-node'; @@ -29,5 +30,5 @@ export function reactFindDOMNodes(elem: ReactInstance | null): Array = []; const fiberNode = (elem as any)[FIBER_KEY]; elementsFromFiber(fiberNode.child, elements); - return elements.length > 0 ? elements : null; + return elements.length > 0 ? elements : [findDOMNode(elem)]; }