diff --git a/packages/designer/jest.config.js b/packages/designer/jest.config.js index 553eeb33f..b32307826 100644 --- a/packages/designer/jest.config.js +++ b/packages/designer/jest.config.js @@ -6,7 +6,7 @@ module.exports = { // // '^.+\\.(ts|tsx)$': 'ts-jest', // // '^.+\\.(js|jsx)$': 'babel-jest', // }, - // testMatch: ['**/builtin-hotkey.test.ts'], + testMatch: ['**/bugs/*.test.ts'], // testMatch: ['(/tests?/.*(test))\\.[jt]s$'], transformIgnorePatterns: [ `/node_modules/(?!${esModules})/`, diff --git a/packages/designer/tests/bugs/misc.ts b/packages/designer/tests/bugs/misc.ts.bak similarity index 100% rename from packages/designer/tests/bugs/misc.ts rename to packages/designer/tests/bugs/misc.ts.bak diff --git a/packages/designer/tests/bugs/prop-variable-jse.test.ts b/packages/designer/tests/bugs/prop-variable-jse.test.ts new file mode 100644 index 000000000..c830f8fbd --- /dev/null +++ b/packages/designer/tests/bugs/prop-variable-jse.test.ts @@ -0,0 +1,72 @@ +// @ts-nocheck +import { Editor } from '@ali/lowcode-editor-core'; +import { isJSBlock, TransformStage } from '@ali/lowcode-types'; +import { isPlainObject, isVariable } from '@ali/lowcode-utils'; +import '../fixtures/window'; +import { Designer } from '../../src/designer/designer'; +import { DocumentModel } from '../../src/document/document-model'; +import { Project } from '../../src/project/project'; +import formSchema from '../fixtures/schema/form'; + +/** + * bug 背景: + * Prop 在每次 setValue 时都会调用 dispose 方法用于重新计算子 Prop,我认为在 Node 未完成初始化之前的 dispose 都是 + * 无意义的,所以增加了判断条件来调用 dispose,结果导致了 variable 结果没有正确转成 JSExpression 结构。 + * + * 因为 propsReducer 的 Init / Upgrade 阶段依然可以更改 props,且此时的 Node 也未完成初始化,不调用 dispose 则导致新的 Prop 结构无法生效 + */ + +function upgradePropsReducer(props: any): any { + if (!props || !isPlainObject(props)) { + return props; + } + + if (isJSBlock(props)) { + if (props.value.componentName === 'Slot') { + return { + type: 'JSSlot', + title: (props.value.props as any)?.slotTitle, + name: (props.value.props as any)?.slotName, + value: props.value.children, + }; + } else { + return props.value; + } + } + if (isVariable(props)) { + return { + type: 'JSExpression', + value: props.variable, + mock: props.value, + }; + } + const newProps: any = {}; + Object.keys(props).forEach((key) => { + if (/^__slot__/.test(key) && props[key] === true) { + return; + } + newProps[key] = upgradePropsReducer(props[key]); + }); + return newProps; +} + +describe('Node 方法测试', () => { + let editor: Editor; + let designer: Designer; + let project: Project; + let doc: DocumentModel; + + it('原始 prop 值是 variable 结构,通过一个 propsReducer 转成了 JSExpression 结构', () => { + editor = new Editor(); + designer = new Designer({ editor }); + designer.addPropsReducer(upgradePropsReducer, TransformStage.Upgrade); + project = designer.project; + doc = new DocumentModel(project, formSchema); + + const form = doc.getNode('form'); + expect(form.getPropValue('dataSource')).toEqual({ + type: 'JSExpression', + value: 'state.formData', + }) + }); +}); diff --git a/packages/designer/tests/bugs/why.md b/packages/designer/tests/bugs/why.md new file mode 100644 index 000000000..519dee1b5 --- /dev/null +++ b/packages/designer/tests/bugs/why.md @@ -0,0 +1,6 @@ +背景: +在 UT 的基础上,希望借助一些 Bug 修复来完成场景测试,从而进一步增强稳定性。 +至少在真正的 E2E 测试来临之前,我们保证不会重复犯两次相同的错误。 + +做法: +Bugs 文件夹每个文件记录一个 bug 修复的场景测试~ \ No newline at end of file diff --git a/packages/designer/tests/document/node/node.test.ts b/packages/designer/tests/document/node/node.test.ts index 18f40e215..ee3928a64 100644 --- a/packages/designer/tests/document/node/node.test.ts +++ b/packages/designer/tests/document/node/node.test.ts @@ -1,4 +1,4 @@ -// @ts-ignore +// @ts-nocheck import '../../fixtures/window'; import { set, delayObxTick, delay } from '../../utils'; import { Editor } from '@ali/lowcode-editor-core'; @@ -24,7 +24,6 @@ import rootHeaderMetadata from '../../fixtures/component-metadata/root-header'; import rootContentMetadata from '../../fixtures/component-metadata/root-content'; import rootFooterMetadata from '../../fixtures/component-metadata/root-footer'; - describe('Node 方法测试', () => { let editor: Editor; let designer: Designer; @@ -185,12 +184,16 @@ describe('Node 方法测试', () => { it('null', () => { expect( - doc.rootNode?.getSuitablePlace.call({ contains: () => false, isContainer: () => false, isRoot: () => false }), + doc.rootNode?.getSuitablePlace.call({ + contains: () => false, + isContainer: () => false, + isRoot: () => false, + }), ).toBeNull(); }); }); - it('removeChild / replaceWith / replaceChild / onChildrenChange / mergeChildren', () => { + it('removeChild / replaceWith / replaceChild', () => { const firstBtn = doc.getNode('node_k1ow3cbn')!; firstBtn.select(); @@ -254,7 +257,7 @@ describe('Node 方法测试', () => { }); }); - it('setVisible / getVisible / onVisibleChange', async () => { + it('setVisible / getVisible / onVisibleChange', () => { const mockFn = jest.fn(); const firstBtn = doc.getNode('node_k1ow3cbn')!; const off = firstBtn.onVisibleChange(mockFn); @@ -265,7 +268,6 @@ describe('Node 方法测试', () => { firstBtn.setVisible(false); - await delayObxTick(); expect(firstBtn.getVisible()).toBeFalsy(); expect(mockFn).toHaveBeenCalledTimes(2); expect(mockFn).toHaveBeenCalledWith(false); @@ -273,7 +275,22 @@ describe('Node 方法测试', () => { off(); mockFn.mockClear(); firstBtn.setVisible(true); - await delayObxTick(); + expect(mockFn).not.toHaveBeenCalled(); + }); + + it('onPropChange', () => { + const mockFn = jest.fn(); + const firstBtn = doc.getNode('node_k1ow3cbn')!; + const off = firstBtn.onPropChange(mockFn); + + firstBtn.setPropValue('x', 1); + expect(mockFn).toHaveBeenCalledTimes(1); + firstBtn.setPropValue('x', 2); + expect(mockFn).toHaveBeenCalledTimes(2); + + off(); + mockFn.mockClear(); + firstBtn.setPropValue('x', 3); expect(mockFn).not.toHaveBeenCalled(); }); @@ -292,7 +309,9 @@ describe('Node 方法测试', () => { const pageMeta = designer.getComponentMeta('Page'); const autorunMockFn = jest.fn(); - set(pageMeta, '_transformedMetadata.experimental.autoruns', [{ name: 'a', autorun: autorunMockFn }]); + set(pageMeta, '_transformedMetadata.experimental.autoruns', [ + { name: 'a', autorun: autorunMockFn }, + ]); const initialChildrenMockFn = jest.fn(); set(pageMeta, '_transformedMetadata.experimental.initialChildren', initialChildrenMockFn); doc.createNode({ componentName: 'Page', props: { a: 1 } }); diff --git a/packages/designer/tests/document/node/props/prop.test.ts b/packages/designer/tests/document/node/props/prop.test.ts index 1b55890f1..76ed65af6 100644 --- a/packages/designer/tests/document/node/props/prop.test.ts +++ b/packages/designer/tests/document/node/props/prop.test.ts @@ -200,9 +200,11 @@ describe('Prop 类测试', () => { // 更新 slot slotProp.setValue({ type: 'JSSlot', - value: [{ - componentName: 'Form', - }] + value: [ + { + componentName: 'Form', + }, + ], }); expect(slotNodeImportMockFn).toBeCalled(); @@ -276,19 +278,20 @@ describe('Prop 类测试', () => { expect(prop.get('z.z1')?.getValue()).toBe(1); expect(prop.get('z.z2')?.getValue()).toBe('str'); - const fromStashProp = prop.get('l'); - const fromStashNestedProp = prop.get('m.m1'); - fromStashProp.setValue('fromStashProp'); - fromStashNestedProp?.setValue('fromStashNestedProp'); + const newlyCreatedProp = prop.get('l', true); + const newlyCreatedNestedProp = prop.get('m.m1', true); + newlyCreatedProp.setValue('newlyCreatedProp'); + newlyCreatedNestedProp?.setValue('newlyCreatedNestedProp'); - await delayObxTick(); - expect(prop.get('l').getValue()).toBe('fromStashProp'); - expect(prop.get('m.m1').getValue()).toBe('fromStashNestedProp'); + expect(prop.get('l').getValue()).toBe('newlyCreatedProp'); + expect(prop.get('m.m1').getValue()).toBe('newlyCreatedNestedProp'); + + const newlyCreatedNestedProp2 = prop.get('m.m2', true); + // .m2 的值为 undefined,导出时将会被移除 + expect(prop.get('m').getValue()).toEqual({ m1: 'newlyCreatedNestedProp' }); }); it('export', () => { - // TODO: 需要访问一下才能触发构造 _items - prop.items; expect(prop.export()).toEqual({ a: 1, b: 'str', diff --git a/packages/designer/tests/document/node/props/props.test.ts b/packages/designer/tests/document/node/props/props.test.ts index eb49568a5..360db47eb 100644 --- a/packages/designer/tests/document/node/props/props.test.ts +++ b/packages/designer/tests/document/node/props/props.test.ts @@ -2,32 +2,42 @@ import '../../../fixtures/window'; import { set, delayObxTick } from '../../../utils'; import { Editor } from '@ali/lowcode-editor-core'; -import { Props, getConvertedExtraKey, getOriginalExtraKey, Prop, isProp, isValidArrayIndex } from '../../../../src/document/node/props/props'; +import { + Props, + getConvertedExtraKey, + getOriginalExtraKey, + Prop, + isProp, + isValidArrayIndex, +} from '../../../../src/document/node/props/props'; import { Designer } from '../../../../src/designer/designer'; import { Project } from '../../../../src/project/project'; import { DocumentModel } from '../../../../src/document/document-model'; import { TransformStage } from '@ali/lowcode-types'; - const mockedOwner = { componentName: 'Page' }; describe('Props 类测试', () => { let props: Props; beforeEach(() => { - props = new Props(mockedOwner, { - a: 1, - b: 'str', - c: true, - d: { - type: 'JSExpression', - value: 'state.a', + props = new Props( + mockedOwner, + { + a: 1, + b: 'str', + c: true, + d: { + type: 'JSExpression', + value: 'state.a', + }, + z: { + z1: 1, + z2: 'str', + }, }, - z: { - z1: 1, - z2: 'str', - }, - }, { condition: true }); + { condition: true }, + ); }); afterEach(() => { props.purge(); @@ -49,7 +59,6 @@ describe('Props 类测试', () => { z2: 'str', }); - expect(props.getPropValue('a')).toBe(1); props.setPropValue('a', 2); expect(props.getPropValue('a')).toBe(2); @@ -59,14 +68,15 @@ describe('Props 类测试', () => { expect(props.get('z.z1')?.getValue()).toBe(1); expect(props.get('z.z2')?.getValue()).toBe('str'); - const fromStashProp = props.get('l', true); - const fromStashNestedProp = props.get('m.m1', true); - fromStashProp.setValue('fromStashProp'); - fromStashNestedProp?.setValue('fromStashNestedProp'); + const notCreatedProp = props.get('i'); + expect(notCreatedProp).toBeNull(); + const newlyCreatedProp = props.get('l', true); + const newlyCreatedNestedProp = props.get('m.m1', true); + newlyCreatedProp.setValue('newlyCreatedProp'); + newlyCreatedNestedProp?.setValue('newlyCreatedNestedProp'); - await delayObxTick(); - expect(props.get('l').getValue()).toBe('fromStashProp'); - expect(props.get('m.m1').getValue()).toBe('fromStashNestedProp'); + expect(props.get('l').getValue()).toBe('newlyCreatedProp'); + expect(props.get('m.m1').getValue()).toBe('newlyCreatedNestedProp'); }); it('export', () => { @@ -119,11 +129,66 @@ describe('Props 类测试', () => { }); }); + it('export - remove undefined items', () => { + props.import( + { + a: 1, + }, + { loop: false }, + ); + props.setPropValue('x', undefined); + expect(props.export()).toEqual({ + props: { + a: 1, + }, + extras: { + loop: false, + }, + }); + + props.setPropValue('x', 2); + expect(props.export()).toEqual({ + props: { + a: 1, + x: 2, + }, + extras: { + loop: false, + }, + }); + + props.setPropValue('y.z', undefined); + expect(props.export()).toEqual({ + props: { + a: 1, + x: 2, + }, + extras: { + loop: false, + }, + }); + + props.setPropValue('y.z', 2); + expect(props.export()).toEqual({ + props: { + a: 1, + x: 2, + y: { z: 2 }, + }, + extras: { + loop: false, + }, + }); + }); + it('import', () => { - props.import({ - x: 1, - y: true, - }, { loop: false }); + props.import( + { + x: 1, + y: true, + }, + { loop: false }, + ); expect(props.export()).toEqual({ props: { x: 1, @@ -173,19 +238,19 @@ describe('Props 类测试', () => { expect(mockedFn).toHaveBeenCalledTimes(6); mockedFn.mockClear(); - props.forEach(item => { + props.forEach((item) => { mockedFn(); }); expect(mockedFn).toHaveBeenCalledTimes(6); mockedFn.mockClear(); - props.map(item => { + props.map((item) => { return mockedFn(); }); expect(mockedFn).toHaveBeenCalledTimes(6); mockedFn.mockClear(); - props.filter(item => { + props.filter((item) => { return mockedFn(); }); expect(mockedFn).toHaveBeenCalledTimes(6); @@ -229,7 +294,6 @@ describe('Props 类测试', () => { }); }); - describe('其他函数', () => { it('getConvertedExtraKey', () => { expect(getConvertedExtraKey()).toBe('');