From 3dd0b6d0a86267e3029c176ff49aff793ce3e186 Mon Sep 17 00:00:00 2001 From: liujuping Date: Wed, 22 Jun 2022 12:49:48 +0800 Subject: [PATCH 1/4] fix: when designMode is not design, the hidden attribute does not take effect --- packages/renderer-core/src/hoc/leaf.tsx | 4 +- packages/renderer-core/src/renderer/base.tsx | 4 - .../__snapshots__/renderer.test.tsx.snap | 17 ++++ .../tests/renderer/renderer.test.tsx | 81 ++++++++++++++++++- 4 files changed, 99 insertions(+), 7 deletions(-) diff --git a/packages/renderer-core/src/hoc/leaf.tsx b/packages/renderer-core/src/hoc/leaf.tsx index cb09ec2e8..e6981e729 100644 --- a/packages/renderer-core/src/hoc/leaf.tsx +++ b/packages/renderer-core/src/hoc/leaf.tsx @@ -367,7 +367,7 @@ export function leafWrapper(Comp: types.IBaseRenderComponent, { return null; } - _leaf = _leaf || getNode(componentId); + _leaf = _leaf || getNode?.(componentId); if (_leaf && this.curEventLeaf && _leaf !== this.curEventLeaf) { this.disposeFunctions.forEach((fn) => fn()); this.disposeFunctions = []; @@ -513,7 +513,7 @@ export function leafWrapper(Comp: types.IBaseRenderComponent, { } get leaf(): Node | undefined { - return this.props._leaf || getNode(componentCacheId); + return this.props._leaf || getNode?.(componentCacheId); } render() { diff --git a/packages/renderer-core/src/renderer/base.tsx b/packages/renderer-core/src/renderer/base.tsx index 830c92dbb..6a6d622b1 100644 --- a/packages/renderer-core/src/renderer/base.tsx +++ b/packages/renderer-core/src/renderer/base.tsx @@ -475,10 +475,6 @@ export default function baseRendererFactory(): IBaseRenderComponent { // DesignMode 为 design 情况下,需要进入 leaf Hoc,进行相关事件注册 const displayInHook = engine?.props?.designMode === 'design'; - if (schema.hidden && !displayInHook) { - return null; - } - if (schema.loop != null) { const loop = parseData(schema.loop, scope); const useLoop = isUseLoop(loop, this._designModeIsDesign); diff --git a/packages/renderer-core/tests/renderer/__snapshots__/renderer.test.tsx.snap b/packages/renderer-core/tests/renderer/__snapshots__/renderer.test.tsx.snap index 5ef56f775..76ed01e02 100644 --- a/packages/renderer-core/tests/renderer/__snapshots__/renderer.test.tsx.snap +++ b/packages/renderer-core/tests/renderer/__snapshots__/renderer.test.tsx.snap @@ -1121,3 +1121,20 @@ exports[`JSExpression base props 1`] = ` /> `; + +exports[`designMode designMode:default 1`] = ` +
+
+
+
+
+`; diff --git a/packages/renderer-core/tests/renderer/renderer.test.tsx b/packages/renderer-core/tests/renderer/renderer.test.tsx index d1917c5a5..9847c6cc1 100644 --- a/packages/renderer-core/tests/renderer/renderer.test.tsx +++ b/packages/renderer-core/tests/renderer/renderer.test.tsx @@ -7,7 +7,7 @@ import components from '../utils/components'; const Renderer = rendererFactory(); -function getComp(schema, comp = null): Promise<{ +function getComp(schema, comp = null, others = {}): Promise<{ component, inst, }> { @@ -17,6 +17,7 @@ function getComp(schema, comp = null): Promise<{ ); const componentInstance = component.root; @@ -321,4 +322,82 @@ describe('JSExpression', () => { done(); }); }) +}); + +describe("designMode", () => { + it('designMode:default', (done) => { + const schema = { + componentName: 'Page', + props: {}, + children: [ + { + componentName: "Div", + props: { + className: 'div-ut', + children: [ + { + componentName: "Div", + visible: true, + props: { + className: 'div-ut-children', + } + } + ] + } + } + ] + }; + + getComp(schema, components.Div).then(({ component, inst }) => { + expect(inst.length).toBe(2); + expect(inst[0].props.className).toBe('div-ut'); + expect(inst[1].props.className).toBe('div-ut-children'); + componentSnapshot = component; + done(); + }); + }); + it('designMode:design', (done) => { + const schema = { + componentName: 'Page', + props: {}, + children: [ + { + componentName: "Div", + id: '0', + props: { + className: 'div-ut', + children: [ + { + componentName: "Div", + id: 'hiddenId', + hidden: true, + props: { + className: 'div-ut-children', + } + } + ] + } + } + ] + }; + + getComp(schema, components.Div, { + designMode: 'design', + getNode: (id) => { + if (id === 'hiddenId') { + return { + export() { + return { + hidden: true, + }; + } + } + } + } + }).then(({ component, inst }) => { + expect(inst.length).toBe(1); + expect(inst[0].props.className).toBe('div-ut'); + done(); + }); + }); }) \ No newline at end of file From 8f237b108953d69e953a3cd4e81d397053dddeb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?LeoYuan=20=E8=A2=81=E5=8A=9B=E7=9A=93?= Date: Tue, 21 Jun 2022 21:01:50 +0800 Subject: [PATCH 2/4] test: increase branch coverage percentage of designer to 81.66% --- package.json | 3 +- packages/designer/jest.config.js | 2 +- packages/designer/src/document/history.ts | 50 ++- .../designer/src/plugin/plugin-manager.ts | 3 +- .../tests/__mocks__/document-model.ts | 1 - .../__snapshots__/history.test.ts.snap | 9 + .../tests/document/history/history.test.ts | 353 +++++++++++++++++ .../tests/document/history/session.test.ts | 57 +++ .../tests/plugin/plugin-manager.test.ts | 358 +++++++++++++----- .../tests/plugin/plugin-utils.test.ts | 85 +++++ .../tests/project/project-methods.test.ts | 2 + 11 files changed, 807 insertions(+), 116 deletions(-) create mode 100644 packages/designer/tests/document/history/__snapshots__/history.test.ts.snap create mode 100644 packages/designer/tests/document/history/history.test.ts create mode 100644 packages/designer/tests/document/history/session.test.ts create mode 100644 packages/designer/tests/plugin/plugin-utils.test.ts diff --git a/package.json b/package.json index 90c099d15..38299c96e 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ ], "nohoist": [ "**/css-modules-typescript-loader", - "**/@alife/theme-lowcode-*" + "**/@alifc/theme-lowcode-*", + "**/jest" ] }, "scripts": { diff --git a/packages/designer/jest.config.js b/packages/designer/jest.config.js index 43bea9064..2ffc057fa 100644 --- a/packages/designer/jest.config.js +++ b/packages/designer/jest.config.js @@ -10,7 +10,7 @@ const jestConfig = { // // '^.+\\.(js|jsx)$': 'babel-jest', // }, // testMatch: ['**/document/node/node.test.ts'], - // testMatch: ['**/component-meta.test.ts'], + // testMatch: ['**/history/history.test.ts'], // testMatch: ['**/plugin/plugin-manager.test.ts'], // testMatch: ['(/tests?/.*(test))\\.[jt]s$'], transformIgnorePatterns: [ diff --git a/packages/designer/src/document/history.ts b/packages/designer/src/document/history.ts index 8828b8e13..5ac0d99cc 100644 --- a/packages/designer/src/document/history.ts +++ b/packages/designer/src/document/history.ts @@ -3,27 +3,12 @@ import { autorun, reaction, mobx, untracked, globalContext, Editor } from '@alil import { NodeSchema } from '@alilc/lowcode-types'; import { History as ShellHistory } from '@alilc/lowcode-shell'; -// TODO: cache to localStorage - -export interface Serialization { - serialize(data: NodeSchema): T; - unserialize(data: T): NodeSchema; +export interface Serialization { + serialize(data: K): T; + unserialize(data: T): K; } -let currentSerialization: Serialization = { - serialize(data: NodeSchema): string { - return JSON.stringify(data); - }, - unserialize(data: string) { - return JSON.parse(data); - }, -}; - -export function setSerialization(serialization: Serialization) { - currentSerialization = serialization; -} - -export class History { +export class History { private session: Session; private records: Session[]; @@ -34,16 +19,29 @@ export class History { private asleep = false; - constructor(logger: () => any, private redoer: (data: NodeSchema) => void, private timeGap: number = 1000) { + private currentSerialization: Serialization = { + serialize(data: T): string { + return JSON.stringify(data); + }, + unserialize(data: string) { + return JSON.parse(data); + }, + }; + + setSerialization(serialization: Serialization) { + this.currentSerialization = serialization; + } + + constructor(dataFn: () => T, private redoer: (data: T) => void, private timeGap: number = 1000) { this.session = new Session(0, null, this.timeGap); this.records = [this.session]; reaction(() => { - return logger(); - }, (data) => { + return dataFn(); + }, (data: T) => { if (this.asleep) return; untracked(() => { - const log = currentSerialization.serialize(data); + const log = this.currentSerialization.serialize(data); if (this.session.isActive()) { this.session.log(log); } else { @@ -98,9 +96,9 @@ export class History { this.sleep(); try { - this.redoer(currentSerialization.unserialize(hotData)); + this.redoer(this.currentSerialization.unserialize(hotData)); this.emitter.emit('cursor', hotData); - } catch (e) { + } catch (e) /* istanbul ignore next */ { console.error(e); } @@ -201,7 +199,7 @@ export class History { } } -class Session { +export class Session { private _data: any; private activeTimer: any; diff --git a/packages/designer/src/plugin/plugin-manager.ts b/packages/designer/src/plugin/plugin-manager.ts index e90b51cfe..dc803ddfe 100644 --- a/packages/designer/src/plugin/plugin-manager.ts +++ b/packages/designer/src/plugin/plugin-manager.ts @@ -148,7 +148,7 @@ export class LowCodePluginManager implements ILowCodePluginManager { for (const pluginName of sequence) { try { await this.pluginsMap.get(pluginName)!.init(); - } catch (e) { + } catch (e) /* istanbul ignore next */ { logger.error( `Failed to init plugin:${pluginName}, it maybe affect those plugins which depend on this.`, ); @@ -189,6 +189,7 @@ export class LowCodePluginManager implements ILowCodePluginManager { }); } + /* istanbul ignore next */ setDisabled(pluginName: string, flag = true) { logger.warn(`plugin:${pluginName} has been set disable:${flag}`); this.pluginsMap.get(pluginName)?.setDisabled(flag); diff --git a/packages/designer/tests/__mocks__/document-model.ts b/packages/designer/tests/__mocks__/document-model.ts index c5f4fef8b..0eb1910fc 100644 --- a/packages/designer/tests/__mocks__/document-model.ts +++ b/packages/designer/tests/__mocks__/document-model.ts @@ -2,7 +2,6 @@ export class DocumentModel { a = 1; c = {}; constructor() { - console.log('xxxxxxxxxxxxxxxxxxxx'); const b = { x: { y: 2 } }; const c: number = 2; this.a = b?.x?.y; diff --git a/packages/designer/tests/document/history/__snapshots__/history.test.ts.snap b/packages/designer/tests/document/history/__snapshots__/history.test.ts.snap new file mode 100644 index 000000000..1249a4165 --- /dev/null +++ b/packages/designer/tests/document/history/__snapshots__/history.test.ts.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`History data function & records 1`] = `"{\\"data\\":1,\\"children\\":[{\\"data\\":2,\\"children\\":[]}]}"`; + +exports[`History data function & records 2`] = `"{\\"data\\":3,\\"children\\":[{\\"data\\":2,\\"children\\":[]}]}"`; + +exports[`History data function & records 3`] = `"{\\"data\\":5,\\"children\\":[{\\"data\\":2,\\"children\\":[]}]}"`; + +exports[`History data function & records 4`] = `"{\\"data\\":7,\\"children\\":[{\\"data\\":2,\\"children\\":[]}]}"`; diff --git a/packages/designer/tests/document/history/history.test.ts b/packages/designer/tests/document/history/history.test.ts new file mode 100644 index 000000000..a4c6d3a66 --- /dev/null +++ b/packages/designer/tests/document/history/history.test.ts @@ -0,0 +1,353 @@ +import '../../fixtures/window'; +import { mobx, makeAutoObservable, globalContext, Editor } from '@alilc/lowcode-editor-core'; +import { History } from '../../../src/document/history'; +import { delay } from '../../utils/misc'; + +class Node { + data: number; + children: Node[] = []; + + constructor(data: number) { + makeAutoObservable(this); + this.data = data; + } + + addNode(node: Node) { + this.children.push(node); + } + + toObject() { + return { + data: this.data, + children: this.children.map((c) => c.toObject()), + }; + } +} + +let tree: Node = null; +beforeEach(() => { + tree = new Node(1); + tree.addNode(new Node(2)); +}); + +afterEach(() => { + tree = null; +}); + +describe('History', () => { + beforeAll(() => { + globalContext.register(new Editor(), Editor); + }); + + it('data function & records', async () => { + const mockRedoFn = jest.fn(); + const mockDataFn = jest.fn(); + const history = new History(() => { + const data = tree.toObject(); + mockDataFn(data); + return data; + }, mockRedoFn); + + expect(mockDataFn).toHaveBeenCalledTimes(1); + expect(mockDataFn).toHaveBeenCalledWith({ data: 1, children: [{ data: 2, children: [] }] }); + expect(history.hotData).toMatchSnapshot(); + // @ts-ignore + expect(history.session.cursor).toBe(0); + // @ts-ignore + expect(history.records).toHaveLength(1); + + tree.data = 3; + expect(mockDataFn).toHaveBeenCalledTimes(2); + expect(mockDataFn).toHaveBeenCalledWith({ data: 3, children: [{ data: 2, children: [] }] }); + expect(history.hotData).toMatchSnapshot(); + // @ts-ignore + expect(history.session.cursor).toBe(0); + // @ts-ignore + expect(history.records).toHaveLength(1); + + // modify data after timeGap + await delay(1200); + tree.data = 5; + expect(mockDataFn).toHaveBeenCalledTimes(3); + expect(mockDataFn).toHaveBeenCalledWith({ data: 5, children: [{ data: 2, children: [] }] }); + expect(history.hotData).toMatchSnapshot(); + // @ts-ignore + expect(history.session.cursor).toBe(1); + // @ts-ignore + expect(history.records).toHaveLength(2); + + history.setSerialization({ + serialize(data: Node): string { + return JSON.stringify(data); + }, + unserialize(data: string) { + return JSON.parse(data); + }, + }); + + // modify data after timeGap + await delay(1200); + tree.data = 7; + expect(mockDataFn).toHaveBeenCalledTimes(4); + expect(mockDataFn).toHaveBeenCalledWith({ data: 7, children: [{ data: 2, children: [] }] }); + expect(history.hotData).toMatchSnapshot(); + }); + + it('isSavePoint & savePoint', async () => { + const history = new History( + () => { + const data = tree.toObject(); + return data; + }, + () => {}, + ); + + expect(history.isSavePoint()).toBeFalsy(); + expect(history.isModified()).toBeFalsy(); + + await delay(1200); + tree.data = 3; + expect(history.isSavePoint()).toBeTruthy(); + + history.savePoint(); + expect(history.isSavePoint()).toBeFalsy(); + }); + + it('go & forward & back & onCursor', async () => { + const mockRedoFn = jest.fn(); + const mockCursorFn = jest.fn(); + const mockStateFn = jest.fn(); + const history = new History( + () => { + const data = tree.toObject(); + return data; + }, + (data) => { + mockRedoFn(data); + }, + ); + + // undoable ❌ & redoable ❌ & modified ❌ + expect(history.getState()).toBe(0); + + await delay(1200); + tree.data = 3; + + await delay(1200); + tree.data = 5; + + await delay(1200); + tree.data = 7; + + const dataCursor0 = { data: 1, children: [{ data: 2, children: [] }] }; + const dataCursor1 = { data: 3, children: [{ data: 2, children: [] }] }; + const dataCursor2 = { data: 5, children: [{ data: 2, children: [] }] }; + const dataCursor3 = { data: 7, children: [{ data: 2, children: [] }] }; + + // redoable ❌ + expect(history.getState()).toBe(7 - 2); + + const off1 = history.onCursor(mockCursorFn); + const off2 = history.onStateChange(mockStateFn); + + // @ts-ignore + expect(history.records).toHaveLength(4); + // @ts-ignore + expect(history.session.cursor).toBe(3); + + // step 1 + history.back(); + expect(mockCursorFn).toHaveBeenNthCalledWith( + 1, + JSON.stringify(dataCursor2), + ); + expect(mockStateFn).toHaveBeenNthCalledWith(1, 7); + expect(mockRedoFn).toHaveBeenNthCalledWith(1, dataCursor2); + + // step 2 + history.back(); + expect(mockCursorFn).toHaveBeenNthCalledWith( + 2, + JSON.stringify(dataCursor1), + ); + expect(mockStateFn).toHaveBeenNthCalledWith(2, 7); + expect(mockRedoFn).toHaveBeenNthCalledWith(2, dataCursor1); + + // step 3 + history.back(); + expect(mockCursorFn).toHaveBeenNthCalledWith( + 3, + JSON.stringify(dataCursor0), + ); + expect(mockStateFn).toHaveBeenNthCalledWith(3, 7 - 4 - 1); + expect(mockRedoFn).toHaveBeenNthCalledWith(3, dataCursor0); + + // step 4 + history.forward(); + expect(mockCursorFn).toHaveBeenNthCalledWith( + 4, + JSON.stringify(dataCursor1), + ); + expect(mockStateFn).toHaveBeenNthCalledWith(4, 7); + expect(mockRedoFn).toHaveBeenNthCalledWith(4, dataCursor1); + + // step 5 + history.forward(); + expect(mockCursorFn).toHaveBeenNthCalledWith( + 5, + JSON.stringify(dataCursor2), + ); + expect(mockStateFn).toHaveBeenNthCalledWith(5, 7); + expect(mockRedoFn).toHaveBeenNthCalledWith(5, dataCursor2); + + // step 6 + history.go(3); + expect(mockCursorFn).toHaveBeenNthCalledWith( + 6, + JSON.stringify(dataCursor3), + ); + expect(mockStateFn).toHaveBeenNthCalledWith(6, 7 - 2); + expect(mockRedoFn).toHaveBeenNthCalledWith(6, dataCursor3); + + // step 7 + history.go(0); + expect(mockCursorFn).toHaveBeenNthCalledWith( + 7, + JSON.stringify(dataCursor0), + ); + expect(mockStateFn).toHaveBeenNthCalledWith(7, 7 - 4 - 1); + expect(mockRedoFn).toHaveBeenNthCalledWith(7, dataCursor0); + + off1(); + off2(); + mockStateFn.mockClear(); + mockCursorFn.mockClear(); + history.go(1); + expect(mockStateFn).not.toHaveBeenCalled(); + expect(mockCursorFn).not.toHaveBeenCalled(); + }); + + it('go() - edge case of cursor', async () => { + const mockRedoFn = jest.fn(); + const mockCursorFn = jest.fn(); + const mockStateFn = jest.fn(); + const history = new History( + () => { + const data = tree.toObject(); + return data; + }, + (data) => { + mockRedoFn(data); + }, + ); + + await delay(1200); + tree.data = 3; + + await delay(1200); + tree.data = 5; + + history.go(-1); + // @ts-ignore + expect(history.session.cursor).toBe(0); + + history.go(3); + // @ts-ignore + expect(history.session.cursor).toBe(2); + }); + + it('destroy()', async () => { + const history = new History( + () => { + const data = tree.toObject(); + return data; + }, + (data) => { + mockRedoFn(data); + }, + ); + + history.destroy(); + // @ts-ignore + expect(history.records).toHaveLength(0); + }); + + it('internalToShellHistory()', async () => { + const history = new History( + () => { + const data = tree.toObject(); + return data; + }, + (data) => { + mockRedoFn(data); + }, + ); + + expect(history.internalToShellHistory().isModified).toBeUndefined(); + }); + + it('sleep & wakeup', async () => { + const mockRedoFn = jest.fn(); + const history = new History( + () => { + const data = tree.toObject(); + return data; + }, + (data) => { + mockRedoFn(data); + }, + ); + + // @ts-ignore + history.sleep(); + + await delay(1200); + tree.data = 3; + // no record has been pushed into records because of history is asleep. + // @ts-ignore + expect(history.records).toHaveLength(1); + + // @ts-ignore + history.wakeup(); + tree.data = 4; + // @ts-ignore + expect(history.records).toHaveLength(2); + }); +}); + +describe('History - errors', () => { + beforeAll(() => { + globalContext.replace(Editor, null); + }); + + it('no editor', () => { + const history = new History( + () => { + const data = tree.toObject(); + return data; + }, + (data) => { + }, + ); + + history.back(); + history.forward(); + }); + + it('no session', () => { + const history = new History( + () => { + const data = tree.toObject(); + return data; + }, + (data) => { + }, + ); + + // @ts-ignore + history.session = undefined; + history.back(); + history.forward(); + history.savePoint(); + }); +}); \ No newline at end of file diff --git a/packages/designer/tests/document/history/session.test.ts b/packages/designer/tests/document/history/session.test.ts new file mode 100644 index 000000000..3e9e8c628 --- /dev/null +++ b/packages/designer/tests/document/history/session.test.ts @@ -0,0 +1,57 @@ +import '../../fixtures/window'; +import { Session } from '../../../src/document/history'; +import { delay } from '../../utils/misc'; + +describe('Session', () => { + it('constructor', () => { + const session = new Session(1, { a: 1 }); + expect(session.cursor).toBe(1); + expect(session.data).toEqual({ a: 1 }); + // @ts-ignore + expect(session.timeGap).toBe(1000); + expect(session.isActive()).toBeTruthy(); + }); + + it('log()', () => { + const session = new Session(1, { a: 1 }); + + session.log({ a: 2 }); + session.log({ a: 3 }); + expect(session.data).toEqual({ a: 3 }); + }); + + it('end()', () => { + const session = new Session(1, { a: 1 }); + + session.end(); + expect(session.isActive()).toBeFalsy(); + session.log({ a: 2 }); + // log is not possible if current session is inactive + expect(session.data).toEqual({ a: 1 }); + }); + + + it('timeGap', async () => { + const session = new Session(1, { a: 1 }); + + expect(session.isActive()).toBeTruthy(); + await delay(1200); + expect(session.isActive()).toBeFalsy(); + session.log({ a: 2 }); + // log is not possible if current session is inactive + expect(session.data).toEqual({ a: 1 }); + }); + + it('custom timeGap', async () => { + const session = new Session(1, { a: 1 }, 2000); + + expect(session.isActive()).toBeTruthy(); + await delay(1200); + expect(session.isActive()).toBeTruthy(); + await delay(1000); + expect(session.isActive()).toBeFalsy(); + session.log({ a: 2 }); + // log is not possible if current session is inactive + expect(session.data).toEqual({ a: 1 }); + }); +}); \ No newline at end of file diff --git a/packages/designer/tests/plugin/plugin-manager.test.ts b/packages/designer/tests/plugin/plugin-manager.test.ts index 08bc61ee7..b0c40070a 100644 --- a/packages/designer/tests/plugin/plugin-manager.test.ts +++ b/packages/designer/tests/plugin/plugin-manager.test.ts @@ -2,6 +2,7 @@ import '../fixtures/window'; import { Editor, engineConfig } from '@alilc/lowcode-editor-core'; import { LowCodePluginManager } from '../../src/plugin/plugin-manager'; import { ILowCodePluginContext, ILowCodePluginManager } from '../../src/plugin/plugin-types'; + const editor = new Editor(); describe('plugin 测试', () => { @@ -15,14 +16,14 @@ describe('plugin 测试', () => { it('注册插件,插件参数生成函数能被调用,且能拿到正确的 ctx ', () => { const mockFn = jest.fn(); - const creater = (ctx: ILowCodePluginContext) => { + const creator2 = (ctx: ILowCodePluginContext) => { mockFn(ctx); return { init: jest.fn(), }; }; - creater.pluginName = 'demo1'; - pluginManager.register(creater); + creator2.pluginName = 'demo1'; + pluginManager.register(creator2); const [expectedCtx] = mockFn.mock.calls[0]; expect(expectedCtx).toHaveProperty('project'); @@ -39,7 +40,7 @@ describe('plugin 测试', () => { it('注册插件,调用插件 init 方法', async () => { const mockFn = jest.fn(); - const creater = (ctx: ILowCodePluginContext) => { + const creator2 = (ctx: ILowCodePluginContext) => { return { init: mockFn, exports() { @@ -50,8 +51,8 @@ describe('plugin 测试', () => { }, }; }; - creater.pluginName = 'demo1'; - pluginManager.register(creater); + creator2.pluginName = 'demo1'; + pluginManager.register(creator2); await pluginManager.init(); expect(pluginManager.size).toBe(1); expect(pluginManager.has('demo1')).toBeTruthy(); @@ -59,35 +60,52 @@ describe('plugin 测试', () => { expect(pluginManager.demo1).toBeTruthy(); expect(pluginManager.demo1.x).toBe(1); expect(pluginManager.demo1.y).toBe(2); + expect(pluginManager.demo1.z).toBeUndefined(); expect(mockFn).toHaveBeenCalled(); }); it('注册插件,调用 setDisabled 方法', async () => { const mockFn = jest.fn(); - const creater = (ctx: ILowCodePluginContext) => { + const creator2 = (ctx: ILowCodePluginContext) => { return { init: mockFn, }; }; - creater.pluginName = 'demo1'; + creator2.pluginName = 'demo1'; - pluginManager.register(creater); + pluginManager.register(creator2); await pluginManager.init(); expect(pluginManager.demo1).toBeTruthy(); pluginManager.setDisabled('demo1', true); expect(pluginManager.demo1).toBeUndefined(); }); + it('注册插件,调用 plugin.setDisabled 方法', async () => { + const mockFn = jest.fn(); + const creator2 = (ctx: ILowCodePluginContext) => { + return { + init: mockFn, + }; + }; + creator2.pluginName = 'demo1'; + + pluginManager.register(creator2); + await pluginManager.init(); + expect(pluginManager.demo1).toBeTruthy(); + pluginManager.get('demo1').setDisabled(); + expect(pluginManager.demo1).toBeUndefined(); + }); + it('删除插件,调用插件 destroy 方法', async () => { const mockFn = jest.fn(); - const creater = (ctx: ILowCodePluginContext) => { + const creator2 = (ctx: ILowCodePluginContext) => { return { init: jest.fn(), destroy: mockFn, }; }; - creater.pluginName = 'demo1'; - pluginManager.register(creater); + creator2.pluginName = 'demo1'; + pluginManager.register(creator2); await pluginManager.init(); await pluginManager.delete('demo1'); @@ -95,109 +113,185 @@ describe('plugin 测试', () => { await pluginManager.delete('non-existing'); }); - it('dep 依赖', async () => { - const mockFn = jest.fn(); - const creater1 = (ctx: ILowCodePluginContext) => { - return { - // dep: ['demo2'], - init: () => mockFn('demo1'), + describe('dependencies 依赖', () => { + it('dependencies 依赖', async () => { + const mockFn = jest.fn(); + const creator21 = (ctx: ILowCodePluginContext) => { + return { + init: () => mockFn('demo1'), + }; }; - }; - creater1.pluginName = 'demo1'; - creater1.meta = { - dependencies: ['demo2'], - }; - pluginManager.register(creater1); - const creater2 = (ctx: ILowCodePluginContext) => { - return { - init: () => mockFn('demo2'), + creator21.pluginName = 'demo1'; + creator21.meta = { + dependencies: ['demo2'], }; - }; - creater2.pluginName = 'demo2'; - pluginManager.register(creater2); + pluginManager.register(creator21); + const creator22 = (ctx: ILowCodePluginContext) => { + return { + init: () => mockFn('demo2'), + }; + }; + creator22.pluginName = 'demo2'; + pluginManager.register(creator22); - await pluginManager.init(); - expect(mockFn).toHaveBeenNthCalledWith(1, 'demo2'); - expect(mockFn).toHaveBeenNthCalledWith(2, 'demo1'); + await pluginManager.init(); + expect(mockFn).toHaveBeenNthCalledWith(1, 'demo2'); + expect(mockFn).toHaveBeenNthCalledWith(2, 'demo1'); + }); + + it('dependencies 依赖 - string', async () => { + const mockFn = jest.fn(); + const creator21 = (ctx: ILowCodePluginContext) => { + return { + init: () => mockFn('demo1'), + }; + }; + creator21.pluginName = 'demo1'; + creator21.meta = { + dependencies: 'demo2', + }; + pluginManager.register(creator21); + const creator22 = (ctx: ILowCodePluginContext) => { + return { + init: () => mockFn('demo2'), + }; + }; + creator22.pluginName = 'demo2'; + pluginManager.register(creator22); + + await pluginManager.init(); + expect(mockFn).toHaveBeenNthCalledWith(1, 'demo2'); + expect(mockFn).toHaveBeenNthCalledWith(2, 'demo1'); + }); + + it('dependencies 依赖 - 兼容 dep', async () => { + const mockFn = jest.fn(); + const creator21 = (ctx: ILowCodePluginContext) => { + return { + dep: ['demo4'], + init: () => mockFn('demo3'), + }; + }; + creator21.pluginName = 'demo3'; + pluginManager.register(creator21); + const creator22 = (ctx: ILowCodePluginContext) => { + return { + init: () => mockFn('demo4'), + }; + }; + creator22.pluginName = 'demo4'; + pluginManager.register(creator22); + + await pluginManager.init(); + expect(mockFn).toHaveBeenNthCalledWith(1, 'demo4'); + expect(mockFn).toHaveBeenNthCalledWith(2, 'demo3'); + }); + + it('dependencies 依赖 - 兼容 dep & string', async () => { + const mockFn = jest.fn(); + const creator21 = (ctx: ILowCodePluginContext) => { + return { + dep: 'demo4', + init: () => mockFn('demo3'), + }; + }; + creator21.pluginName = 'demo3'; + pluginManager.register(creator21); + const creator22 = (ctx: ILowCodePluginContext) => { + return { + init: () => mockFn('demo4'), + }; + }; + creator22.pluginName = 'demo4'; + pluginManager.register(creator22); + + await pluginManager.init(); + expect(mockFn).toHaveBeenNthCalledWith(1, 'demo4'); + expect(mockFn).toHaveBeenNthCalledWith(2, 'demo3'); + }); }); it('version 依赖', async () => { const mockFn = jest.fn(); - const creater1 = (ctx: ILowCodePluginContext) => { + const creator21 = (ctx: ILowCodePluginContext) => { return { init: () => mockFn('demo1'), }; }; - creater1.pluginName = 'demo1'; - creater1.meta = { + creator21.pluginName = 'demo1'; + creator21.meta = { engines: { lowcodeEngine: '^1.1.0', - } + }, }; engineConfig.set('ENGINE_VERSION', '1.0.1'); - + console.log('version: ', engineConfig.get('ENGINE_VERSION')); // not match should skip - pluginManager.register(creater1).catch(e => { - expect(e).toEqual(new Error('plugin demo1 skipped, engine check failed, current engine version is 1.0.1, meta.engines.lowcodeEngine is ^1.1.0')); + pluginManager.register(creator21).catch((e) => { + expect(e).toEqual( + new Error( + 'plugin demo1 skipped, engine check failed, current engine version is 1.0.1, meta.engines.lowcodeEngine is ^1.1.0', + ), + ); }); expect(pluginManager.plugins.length).toBe(0); - const creater2 = (ctx: ILowCodePluginContext) => { + const creator22 = (ctx: ILowCodePluginContext) => { return { init: () => mockFn('demo2'), }; }; - creater2.pluginName = 'demo2'; - creater2.meta = { + creator22.pluginName = 'demo2'; + creator22.meta = { engines: { lowcodeEngine: '^1.0.1', - } + }, }; engineConfig.set('ENGINE_VERSION', '1.0.3'); - pluginManager.register(creater2); + pluginManager.register(creator22); expect(pluginManager.plugins.length).toBe(1); - const creater3 = (ctx: ILowCodePluginContext) => { + const creator23 = (ctx: ILowCodePluginContext) => { return { init: () => mockFn('demo3'), }; }; - creater3.pluginName = 'demo3'; - creater3.meta = { + creator23.pluginName = 'demo3'; + creator23.meta = { engines: { lowcodeEngine: '1.x', - } + }, }; engineConfig.set('ENGINE_VERSION', '1.1.1'); - pluginManager.register(creater3); + pluginManager.register(creator23); expect(pluginManager.plugins.length).toBe(2); }); it('autoInit 功能', async () => { const mockFn = jest.fn(); - const creater = (ctx: ILowCodePluginContext) => { + const creator2 = (ctx: ILowCodePluginContext) => { return { init: mockFn, }; }; - creater.pluginName = 'demo1'; - await pluginManager.register(creater, { autoInit: true }); + creator2.pluginName = 'demo1'; + await pluginManager.register(creator2, { autoInit: true }); expect(mockFn).toHaveBeenCalled(); }); it('插件不会重复 init,除非强制重新 init', async () => { const mockFn = jest.fn(); - const creater = (ctx: ILowCodePluginContext) => { + const creator2 = (ctx: ILowCodePluginContext) => { return { name: 'demo1', init: mockFn, }; }; - creater.pluginName = 'demo1'; - pluginManager.register(creater); + creator2.pluginName = 'demo1'; + pluginManager.register(creator2); await pluginManager.init(); expect(mockFn).toHaveBeenCalledTimes(1); @@ -217,7 +311,7 @@ describe('plugin 测试', () => { }; mockPlugin.pluginName = 'demoDuplicated'; pluginManager.register(mockPlugin); - pluginManager.register(mockPlugin).catch(e => { + pluginManager.register(mockPlugin).catch((e) => { expect(e).toEqual(new Error('Plugin with name demoDuplicated exists')); }); await pluginManager.init(); @@ -255,33 +349,35 @@ describe('plugin 测试', () => { it('内部事件机制', async () => { const mockFn = jest.fn(); - const creater = (ctx: ILowCodePluginContext) => { - return { - }; - } - creater.pluginName = 'demo1'; - pluginManager.register(creater); + const creator2 = (ctx: ILowCodePluginContext) => { + return {}; + }; + creator2.pluginName = 'demo1'; + pluginManager.register(creator2); await pluginManager.init(); const plugin = pluginManager.get('demo1')!; - plugin.on('haha', mockFn); + const off = plugin.on('haha', mockFn); plugin.emit('haha', 1, 2, 3); expect(mockFn).toHaveBeenCalledTimes(1); expect(mockFn).toHaveBeenCalledWith(1, 2, 3); + off(); + plugin.emit('haha', 1, 2, 3); + expect(mockFn).toHaveBeenCalledTimes(1); + plugin.removeAllListeners('haha'); plugin.emit('haha', 1, 2, 3); expect(mockFn).toHaveBeenCalledTimes(1); }); it('dispose 方法', async () => { - const creater = (ctx: ILowCodePluginContext) => { - return { - }; - } - creater.pluginName = 'demo1'; - pluginManager.register(creater); + const creator2 = (ctx: ILowCodePluginContext) => { + return {}; + }; + creator2.pluginName = 'demo1'; + pluginManager.register(creator2); await pluginManager.init(); const plugin = pluginManager.get('demo1')!; await plugin.dispose(); @@ -289,30 +385,61 @@ describe('plugin 测试', () => { expect(pluginManager.has('demo1')).toBeFalsy(); }); - it('注册插件,调用插件 init 方法并传入preference,可以成功获取', async () => { + it('getAll 方法', async () => { + const creator2 = (ctx: ILowCodePluginContext) => { + return {}; + }; + creator2.pluginName = 'demo1'; + pluginManager.register(creator2); + await pluginManager.init(); + + expect(pluginManager.getAll()).toHaveLength(1); + }); + + it('getPluginPreference 方法 - null', async () => { + const creator2 = (ctx: ILowCodePluginContext) => { + return {}; + }; + creator2.pluginName = 'demo1'; + pluginManager.register(creator2); + await pluginManager.init(); + + expect(pluginManager.getPluginPreference()).toBeNull(); + }); + + it('getPluginPreference 方法', async () => { + const creator2 = (ctx: ILowCodePluginContext) => { + return {}; + }; + const preference = new Map(); + preference.set('demo1', { a: 1, b: 2 }); + creator2.pluginName = 'demo1'; + pluginManager.register(creator2); + await pluginManager.init(preference); + + expect(pluginManager.getPluginPreference('demo1')).toEqual({ a: 1, b: 2 }); + }); + + it('注册插件,调用插件 init 方法并传入 preference,可以成功获取', async () => { const mockFn = jest.fn(); const mockFnForCtx = jest.fn(); + const mockFnForCtx2 = jest.fn(); const mockPreference = new Map(); - mockPreference.set('demo1',{ + mockPreference.set('demo1', { key1: 'value for key1', key2: false, key3: 123, - key5: 'value for key5, but declared, should not work' - }); - mockPreference.set('demo2',{ - key1: 'value for demo2.key1', - key2: false, - key3: 123, + key5: 'value for key5, but declared, should not work', }); - const creater = (ctx: ILowCodePluginContext) => { + const creator2 = (ctx: ILowCodePluginContext) => { mockFnForCtx(ctx); return { init: jest.fn(), }; }; - creater.pluginName = 'demo1'; - creater.meta = { + creator2.pluginName = 'demo1'; + creator2.meta = { preferenceDeclaration: { title: 'demo1的的参数定义', properties: [ @@ -338,13 +465,34 @@ describe('plugin 测试', () => { }, ], }, - } - pluginManager.register(creater); + }; + const creator22 = (ctx: ILowCodePluginContext) => { + mockFnForCtx2(ctx); + return { + init: jest.fn(), + }; + }; + creator22.pluginName = 'demo2'; + creator22.meta = { + preferenceDeclaration: { + title: 'demo1的的参数定义', + properties: [ + { + key: 'key1', + type: 'string', + description: 'this is description for key1', + }, + ], + }, + }; + pluginManager.register(creator2); + pluginManager.register(creator22); expect(mockFnForCtx).toHaveBeenCalledTimes(1); + await pluginManager.init(mockPreference); - // creater only get excuted once + // creator2 only get excuted once expect(mockFnForCtx).toHaveBeenCalledTimes(1); - + const [expectedCtx, expectedOptions] = mockFnForCtx.mock.calls[0]; expect(expectedCtx).toHaveProperty('preference'); @@ -352,9 +500,47 @@ describe('plugin 测试', () => { expect(expectedCtx.preference.getPreferenceValue('key1', 'default')).toBe('value for key1'); // test default value logic - expect(expectedCtx.preference.getPreferenceValue('key4', 'default for key4')).toBe('default for key4'); + expect(expectedCtx.preference.getPreferenceValue('key4', 'default for key4')).toBe( + 'default for key4', + ); // test undeclared key expect(expectedCtx.preference.getPreferenceValue('key5', 'default for key5')).toBeUndefined(); + + // no preference defined + const [expectedCtx2] = mockFnForCtx2.mock.calls[0]; + expect(expectedCtx2.preference.getPreferenceValue('key1')).toBeUndefined(); + }); + + it('注册插件,没有填写 pluginName,默认值为 anonymous', async () => { + const mockFn = jest.fn(); + + const creator2 = (ctx: ILowCodePluginContext) => { + return { + name: 'xxx', + init: () => mockFn('anonymous'), + }; + }; + await pluginManager.register(creator2); + expect(pluginManager.get('anonymous')).toBeUndefined(); + }); + + it('自定义/扩展 plugin context', async () => { + const mockFn = jest.fn(); + const mockFn2 = jest.fn(); + + const creator2 = (ctx: ILowCodePluginContext) => { + mockFn2(ctx); + return { + init: () => mockFn('anonymous'), + }; + }; + creator2.pluginName = 'yyy'; + editor.set('enhancePluginContextHook', (originalContext) => { + originalContext.newProp = 1; + }); + await pluginManager.register(creator2); + const [expectedCtx] = mockFn2.mock.calls[0]; + expect(expectedCtx).toHaveProperty('newProp'); }); }); diff --git a/packages/designer/tests/plugin/plugin-utils.test.ts b/packages/designer/tests/plugin/plugin-utils.test.ts new file mode 100644 index 000000000..eb152a049 --- /dev/null +++ b/packages/designer/tests/plugin/plugin-utils.test.ts @@ -0,0 +1,85 @@ +import '../fixtures/window'; +import { isValidPreferenceKey, filterValidOptions } from '../../src/plugin/plugin-utils'; + +describe('plugin utils 测试', () => { + it('isValidPreferenceKey', () => { + expect(isValidPreferenceKey('x')).toBeFalsy(); + expect(isValidPreferenceKey('x', { properties: {} })).toBeFalsy(); + expect(isValidPreferenceKey('x', { properties: 1 })).toBeFalsy(); + expect(isValidPreferenceKey('x', { properties: 'str' })).toBeFalsy(); + expect(isValidPreferenceKey('x', { properties: [] })).toBeFalsy(); + expect( + isValidPreferenceKey('x', { + title: 'title', + properties: [ + { + key: 'y', + type: 'string', + description: 'x desc', + }, + ], + }), + ).toBeFalsy(); + expect( + isValidPreferenceKey('x', { + title: 'title', + properties: [ + { + key: 'x', + type: 'string', + description: 'x desc', + }, + ], + }), + ).toBeTruthy(); + }); + + it('filterValidOptions', () => { + const mockDeclaration = { + title: 'title', + properties: [ + { + key: 'x', + type: 'string', + description: 'x desc', + }, + { + key: 'y', + type: 'string', + description: 'y desc', + }, + { + key: 'z', + type: 'string', + description: 'z desc', + }, + ], + }; + + expect(filterValidOptions()).toBeUndefined(); + expect(filterValidOptions(1)).toBe(1); + expect(filterValidOptions({ + x: 1, + y: 2, + }, mockDeclaration)).toEqual({ + x: 1, + y: 2, + }); + expect(filterValidOptions({ + x: 1, + y: undefined, + }, mockDeclaration)).toEqual({ + x: 1, + }); + expect(filterValidOptions({ + x: 1, + z: null, + }, mockDeclaration)).toEqual({ + x: 1, + }); + expect(filterValidOptions({ + a: 1, + }, mockDeclaration)).toEqual({ + }); + }); +}); diff --git a/packages/designer/tests/project/project-methods.test.ts b/packages/designer/tests/project/project-methods.test.ts index e32daab6d..1cce64b8c 100644 --- a/packages/designer/tests/project/project-methods.test.ts +++ b/packages/designer/tests/project/project-methods.test.ts @@ -113,6 +113,8 @@ describe.only('Project 方法测试', () => { expect(project.documents.length).toBe(4); expect(project.getDocument(project.currentDocument?.id)).toBe(doc3); + expect(project.getDocumentByFileName(project.currentDocument?.fileName)).toBe(doc3); + expect(project.getDocumentByFileName('unknown')).toBeNull(); expect(project.checkExclusive(project.currentDocument)); expect(project.documents[0].opened).toBeTruthy(); From 2022308e21abe7c1960743f1ac25ad607b3be965 Mon Sep 17 00:00:00 2001 From: liujuping Date: Thu, 23 Jun 2022 11:52:53 +0800 Subject: [PATCH 3/4] refactor: remove redundant monitor files --- packages/editor-core/src/utils/index.ts | 1 - packages/editor-core/src/utils/monitor.ts | 42 ----------------------- 2 files changed, 43 deletions(-) delete mode 100644 packages/editor-core/src/utils/monitor.ts diff --git a/packages/editor-core/src/utils/index.ts b/packages/editor-core/src/utils/index.ts index 0e269ef3b..c01cdfc17 100644 --- a/packages/editor-core/src/utils/index.ts +++ b/packages/editor-core/src/utils/index.ts @@ -1,5 +1,4 @@ export * from './get-public-path'; -export * from './monitor'; export * from './obx'; export * from './request'; export * from './focus-tracker'; diff --git a/packages/editor-core/src/utils/monitor.ts b/packages/editor-core/src/utils/monitor.ts deleted file mode 100644 index 0889d4565..000000000 --- a/packages/editor-core/src/utils/monitor.ts +++ /dev/null @@ -1,42 +0,0 @@ -export class Monitor { - fn = (params: any) => { - const { AES = {} } = window as any; - if (typeof AES.log === 'function') { - const { p1 = '', p2 = '', p3 = '', p4 = 'OTHER', ...rest } = params || {}; - AES.log('event', { - p1, - p2, - p3, - p4, - ...rest, - }); - } - }; - - register(fn: () => any) { - if (typeof fn === 'function') { - this.fn = fn; - } - } - - log(params: any) { - if (typeof this.fn === 'function') { - this.fn(params); - } - } - - setConfig(key: string | object, value?: string): void { - const { AES = {} } = window as any; - if (typeof AES?.setConfig !== 'function') { - return; - } - if (typeof key === 'string' && value) { - AES.setConfig(key, value); - } else if (typeof key === 'object') { - AES.setConfig(key); - } - } -} - -const monitor = new Monitor(); -export { monitor }; From da7f77ee91b3bf441a4a57614872df32d6a1d041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=B5=AE=E9=BB=8E?= Date: Thu, 23 Jun 2022 14:44:35 +0800 Subject: [PATCH 4/4] feat: added thisRequiredInJSE API to control whether JSExpression expression access context must use this (#702) --- .../designer/src/builtin-simulator/host.ts | 4 ++ packages/editor-core/src/config.ts | 10 ++++ .../src/renderer-view.tsx | 1 + .../src/renderer-view.tsx | 1 + packages/renderer-core/src/renderer/base.tsx | 19 ++++--- .../renderer-core/src/renderer/renderer.tsx | 1 + packages/renderer-core/src/types/index.ts | 8 ++- packages/renderer-core/src/utils/common.ts | 10 +++- .../__snapshots__/renderer.test.tsx.snap | 18 ++++++ .../tests/renderer/renderer.test.tsx | 48 +++++++++++++++- .../renderer-core/tests/utils/common.test.ts | 56 ++++++++++++++++--- 11 files changed, 154 insertions(+), 22 deletions(-) diff --git a/packages/designer/src/builtin-simulator/host.ts b/packages/designer/src/builtin-simulator/host.ts index e4cfc4c96..e40566f57 100644 --- a/packages/designer/src/builtin-simulator/host.ts +++ b/packages/designer/src/builtin-simulator/host.ts @@ -228,6 +228,10 @@ export class BuiltinSimulatorHost implements ISimulatorHost { diff --git a/packages/rax-simulator-renderer/src/renderer-view.tsx b/packages/rax-simulator-renderer/src/renderer-view.tsx index c11c2dfdf..2e8d6e0fc 100644 --- a/packages/rax-simulator-renderer/src/renderer-view.tsx +++ b/packages/rax-simulator-renderer/src/renderer-view.tsx @@ -219,6 +219,7 @@ class Renderer extends Component<{ onCompGetRef={(schema: any, ref: any) => { documentInstance.mountInstance(schema.id, ref); }} + thisRequiredInJSE={host.thisRequiredInJSE} documentId={document.id} getNode={(id: string) => documentInstance.getNode(id) as any} rendererName="PageRenderer" diff --git a/packages/react-simulator-renderer/src/renderer-view.tsx b/packages/react-simulator-renderer/src/renderer-view.tsx index 5647a5447..d978494c0 100644 --- a/packages/react-simulator-renderer/src/renderer-view.tsx +++ b/packages/react-simulator-renderer/src/renderer-view.tsx @@ -189,6 +189,7 @@ class Renderer extends Component<{ setSchemaChangedSymbol={this.setSchemaChangedSymbol} getNode={(id: string) => documentInstance.getNode(id) as Node} rendererName="PageRenderer" + thisRequiredInJSE={host.thisRequiredInJSE} customCreateElement={(Component: any, props: any, children: any) => { const { __id, ...viewProps } = props; viewProps.componentId = __id; diff --git a/packages/renderer-core/src/renderer/base.tsx b/packages/renderer-core/src/renderer/base.tsx index 6a6d622b1..aee020344 100644 --- a/packages/renderer-core/src/renderer/base.tsx +++ b/packages/renderer-core/src/renderer/base.tsx @@ -12,6 +12,7 @@ import { getValue, parseData, parseExpression, + parseThisRequiredExpression, parseI18n, isEmpty, isSchema, @@ -83,11 +84,13 @@ export default function baseRendererFactory(): IBaseRenderComponent { getLocale: any; setLocale: any; styleElement: any; + parseExpression: any; [key: string]: any; constructor(props: IBaseRendererProps, context: IBaseRendererContext) { super(props, context); this.context = context; + this.parseExpression = props?.thisRequiredInJSE ? parseThisRequiredExpression : parseExpression; this.__beforeInit(props); this.__init(props); this.__afterInit(props); @@ -112,7 +115,7 @@ export default function baseRendererFactory(): IBaseRenderComponent { if (func) { if (isJSExpression(func) || isJSFunction(func)) { - const fn = parseExpression(func, this); + const fn = props.thisRequiredInJSE ? parseThisRequiredExpression(func, this) : parseExpression(func, this); return fn(props, state); } @@ -193,7 +196,7 @@ export default function baseRendererFactory(): IBaseRenderComponent { if (fn) { // TODO, cache if (isJSExpression(fn) || isJSFunction(fn)) { - fn = parseExpression(fn, this); + fn = this.parseExpression(fn, this); } if (typeof fn !== 'function') { console.error(`生命周期${method}类型不符`, fn); @@ -219,7 +222,7 @@ export default function baseRendererFactory(): IBaseRenderComponent { this.__customMethodsList = customMethodsList; forEach(__schema.methods, (val: any, key: string) => { if (isJSExpression(val) || isJSFunction(val)) { - val = parseExpression(val, this); + val = this.parseExpression(val, this); } if (typeof val !== 'function') { console.error(`自定义函数${key}类型不符`, val); @@ -414,7 +417,7 @@ export default function baseRendererFactory(): IBaseRenderComponent { const { __appHelper: appHelper, __components: components = {} } = this.props || {}; if (isJSExpression(schema)) { - return parseExpression(schema, scope); + return this.parseExpression(schema, scope); } if (isI18nData(schema)) { return parseI18n(schema, scope); @@ -434,7 +437,7 @@ export default function baseRendererFactory(): IBaseRenderComponent { const _children = this.getSchemaChildren(schema); // 解析占位组件 if (schema?.componentName === 'Fragment' && _children) { - const tarChildren = isJSExpression(_children) ? parseExpression(_children, scope) : _children; + const tarChildren = isJSExpression(_children) ? this.parseExpression(_children, scope) : _children; return this.__createVirtualDom(tarChildren, scope, parentInfo); } @@ -496,7 +499,7 @@ export default function baseRendererFactory(): IBaseRenderComponent { let scopeKey = ''; // 判断组件是否需要生成scope,且只生成一次,挂在this.__compScopes上 if (Comp.generateScope) { - const key = parseExpression(schema.props?.key, scope); + const key = this.parseExpression(schema.props?.key, scope); if (key) { // 如果组件自己设置key则使用组件自己的key scopeKey = key; @@ -647,7 +650,7 @@ export default function baseRendererFactory(): IBaseRenderComponent { _children.forEach((_child: any) => { const _childVirtualDom = this.__createVirtualDom( - isJSExpression(_child) ? parseExpression(_child, scope) : _child, + isJSExpression(_child) ? this.parseExpression(_child, scope) : _child, scope, { schema, @@ -754,7 +757,7 @@ export default function baseRendererFactory(): IBaseRenderComponent { return checkProps(props); } if (isJSExpression(props)) { - props = parseExpression(props, scope); + props = this.parseExpression(props, scope); // 只有当变量解析出来为模型结构的时候才会继续解析 if (!isSchema(props) && !isJSSlot(props)) return checkProps(props); } diff --git a/packages/renderer-core/src/renderer/renderer.tsx b/packages/renderer-core/src/renderer/renderer.tsx index bd2b3df8b..59d63f790 100644 --- a/packages/renderer-core/src/renderer/renderer.tsx +++ b/packages/renderer-core/src/renderer/renderer.tsx @@ -60,6 +60,7 @@ export default function rendererFactory(): IRenderComponent { schema: {} as RootSchema, onCompGetRef: () => { }, onCompGetCtx: () => { }, + thisRequiredInJSE: true, }; static findDOMNode = findDOMNode; diff --git a/packages/renderer-core/src/types/index.ts b/packages/renderer-core/src/types/index.ts index b0cc9f996..a3e7f0c36 100644 --- a/packages/renderer-core/src/types/index.ts +++ b/packages/renderer-core/src/types/index.ts @@ -128,6 +128,11 @@ export interface IRendererProps { faultComponent?: IGeneralComponent; /** 设备信息 */ device?: string; + /** + * @default true + * JSExpression 是否只支持使用 this 来访问上下文变量 + */ + thisRequiredInJSE?: boolean; } export interface IRendererState { @@ -148,12 +153,13 @@ export interface IBaseRendererProps { __host?: BuiltinSimulatorHost; __container?: any; config?: Record; - designMode?: 'live' | 'design'; + designMode?: 'design'; className?: string; style?: CSSProperties; id?: string | number; getSchemaChangedSymbol?: () => boolean; setSchemaChangedSymbol?: (symbol: boolean) => void; + thisRequiredInJSE?: boolean; documentId?: string; getNode?: any; /** diff --git a/packages/renderer-core/src/utils/common.ts b/packages/renderer-core/src/utils/common.ts index 437e6077f..8db913f3d 100644 --- a/packages/renderer-core/src/utils/common.ts +++ b/packages/renderer-core/src/utils/common.ts @@ -242,7 +242,7 @@ export function transformStringToFunction(str: string) { * @param self scope object * @returns funtion */ -export function parseExpression(str: any, self: any) { +export function parseExpression(str: any, self: any, thisRequired = false) { try { const contextArr = ['"use strict";', 'var __self = arguments[0];']; contextArr.push('return '); @@ -259,14 +259,18 @@ export function parseExpression(str: any, self: any) { if (inSameDomain() && (window.parent as any).__newFunc) { return (window.parent as any).__newFunc(tarStr)(self); } - const code = `with($scope || {}) { ${tarStr} }`; + const code = `with(${thisRequired ? '{}' : '$scope || {}'}) { ${tarStr} }`; return new Function('$scope', code)(self); } catch (err) { - logger.error('parseExpression.error', err, str, self); + logger.error('parseExpression.error', err, str, self?.__self ?? self); return undefined; } } +export function parseThisRequiredExpression(str: any, self: any) { + return parseExpression(str, self, true); +} + /** * capitalize first letter * @param word string to be proccessed diff --git a/packages/renderer-core/tests/renderer/__snapshots__/renderer.test.tsx.snap b/packages/renderer-core/tests/renderer/__snapshots__/renderer.test.tsx.snap index 76ed01e02..169ed545c 100644 --- a/packages/renderer-core/tests/renderer/__snapshots__/renderer.test.tsx.snap +++ b/packages/renderer-core/tests/renderer/__snapshots__/renderer.test.tsx.snap @@ -994,6 +994,24 @@ exports[`JSExpression JSExpression props with loop 1`] = `
`; +exports[`JSExpression JSExpression props with loop, and thisRequiredInJSE is true 1`] = ` +
+
+
+
+`; + exports[`JSExpression JSFunction props 1`] = `
{ ] }; - getComp(schema, components.Div).then(({ component, inst }) => { + getComp(schema, components.Div, { + thisRequiredInJSE: false, + }).then(({ component, inst }) => { // expect(inst[0].props.visible).toBeTruthy(); expect(inst.length).toEqual(2); [1, 2].forEach((i) => { @@ -157,6 +159,50 @@ describe('JSExpression', () => { }); }); + it('JSExpression props with loop, and thisRequiredInJSE is true', (done) => { + const schema = { + componentName: 'Page', + props: {}, + state: { + isShowDialog: true, + }, + children: [ + { + componentName: "Div", + loop: [ + { + name: '1', + }, + { + name: '2' + } + ], + props: { + className: "div-ut", + name1: { + type: 'JSExpression', + value: 'this.item.name', + }, + name2: { + type: 'JSExpression', + value: 'item.name', + }, + } + } + ] + }; + + getComp(schema, components.Div).then(({ component, inst }) => { + expect(inst.length).toEqual(2); + [0, 1].forEach((i) => { + expect(inst[i].props[`name1`]).toBe(i + 1 + ''); + expect(inst[i].props[`name2`]).toBe(undefined); + }) + componentSnapshot = component; + done(); + }); + }); + // it('JSFunction props with loop', (done) => { // const schema = { // componentName: 'Page', diff --git a/packages/renderer-core/tests/utils/common.test.ts b/packages/renderer-core/tests/utils/common.test.ts index a67842ed6..6fac55024 100644 --- a/packages/renderer-core/tests/utils/common.test.ts +++ b/packages/renderer-core/tests/utils/common.test.ts @@ -1,10 +1,10 @@ // @ts-nocheck -import { - isSchema, - isFileSchema, - inSameDomain, - getFileCssName, - isJSSlot, +import { + isSchema, + isFileSchema, + inSameDomain, + getFileCssName, + isJSSlot, getValue, getI18n, transformArrayToMap, @@ -15,9 +15,11 @@ import { isString, serializeParams, parseExpression, + parseThisRequiredExpression, parseI18n, parseData, } from '../../src/utils/common'; +import logger from '../../src/utils/logger'; describe('test isSchema', () => { it('should be false when empty value is passed', () => { @@ -335,19 +337,55 @@ describe('test parseExpression ', () => { const result = parseExpression(mockExpression, { scopeValue: 1 }); expect(result({ param1: 2 })).toBe((1 + 2 + 5)); }); + + it('[success] JSExpression handle without this use scopeValue', () => { + const mockExpression = { + "type": "JSExpression", + "value": "state" + }; + const result = parseExpression(mockExpression, { state: 1 }); + expect(result).toBe((1)); + }); + + it('[success] JSExpression handle without this use scopeValue', () => { + const mockExpression = { + "type": "JSExpression", + "value": "this.state" + }; + const result = parseExpression(mockExpression, { state: 1 }); + expect(result).toBe((1)); + }); }); -describe('test parseExpression ', () => { +describe('test parseThisRequiredExpression', () => { it('can handle JSExpression', () => { const mockExpression = { "type": "JSExpression", "value": "function (params) { return this.scopeValue + params.param1 + 5;}" }; - const result = parseExpression(mockExpression, { scopeValue: 1 }); + const result = parseThisRequiredExpression(mockExpression, { scopeValue: 1 }); expect(result({ param1: 2 })).toBe((1 + 2 + 5)); }); -}); + it('[error] JSExpression handle without this use scopeValue', () => { + const mockExpression = { + "type": "JSExpression", + "value": "state.text" + }; + const fn = logger.error = jest.fn(); + parseThisRequiredExpression(mockExpression, { state: { text: 'text' } }); + expect(fn).toBeCalledWith('parseExpression.error', new ReferenceError('state is not defined'), {"type": "JSExpression", "value": "state.text"}, {"state": {"text": "text"}}); + }); + + it('[success] JSExpression handle without this use scopeValue', () => { + const mockExpression = { + "type": "JSExpression", + "value": "this.state" + }; + const result = parseThisRequiredExpression(mockExpression, { state: 1 }); + expect(result).toBe((1)); + }); +}) describe('test parseI18n ', () => { it('can handle normal parseI18n', () => {