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] 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();