Merge remote-tracking branch 'origin/develop' into release/1.0.11-beta.0

This commit is contained in:
LeoYuan 袁力皓 2022-06-23 14:44:52 +08:00
commit d1ec7996a7
25 changed files with 1060 additions and 188 deletions

View File

@ -6,7 +6,8 @@
],
"nohoist": [
"**/css-modules-typescript-loader",
"**/@alife/theme-lowcode-*"
"**/@alifc/theme-lowcode-*",
"**/jest"
]
},
"scripts": {

View File

@ -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: [

View File

@ -228,6 +228,10 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
return this.get('requestHandlersMap') || null;
}
get thisRequiredInJSE(): any {
return engineConfig.get('thisRequiredInJSE') ?? true;
}
@computed get componentsAsset(): Asset | undefined {
return this.get('componentsAsset');
}

View File

@ -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<T = any> {
serialize(data: NodeSchema): T;
unserialize(data: T): NodeSchema;
export interface Serialization<K = NodeSchema, T = string> {
serialize(data: K): T;
unserialize(data: T): K;
}
let currentSerialization: Serialization<any> = {
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<T = NodeSchema> {
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<T, string> = {
serialize(data: T): string {
return JSON.stringify(data);
},
unserialize(data: string) {
return JSON.parse(data);
},
};
setSerialization(serialization: Serialization<T, string>) {
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;

View File

@ -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);

View File

@ -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;

View File

@ -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\\":[]}]}"`;

View File

@ -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<Node>(() => {
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<Node>(
() => {
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<Node>(
() => {
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<Node>(
() => {
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<Node>(
() => {
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<Node>(
() => {
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<Node>(
() => {
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<Node>(
() => {
const data = tree.toObject();
return data;
},
(data) => {
},
);
history.back();
history.forward();
});
it('no session', () => {
const history = new History<Node>(
() => {
const data = tree.toObject();
return data;
},
(data) => {
},
);
// @ts-ignore
history.session = undefined;
history.back();
history.forward();
history.savePoint();
});
});

View File

@ -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 });
});
});

View File

@ -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');
});
});

View File

@ -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({
});
});
});

View File

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

View File

@ -133,6 +133,10 @@ const VALID_ENGINE_OPTIONS = {
type: 'object',
description: '数据源引擎的请求处理器映射',
},
thisRequiredInJSE: {
type: 'boolean',
description: 'JSExpression 是否只支持使用 this 来访问上下文变量',
},
};
export interface EngineOptions {
/**
@ -248,6 +252,12 @@ export interface EngineOptions {
*
*/
requestHandlersMap?: RequestHandlersMap;
/**
* @default true
* JSExpression 使 this 访 'state.xxx' false
*/
thisRequiredInJSE?: boolean;
}
const getStrictModeValue = (engineOptions: EngineOptions, defaultValue: boolean): boolean => {

View File

@ -1,5 +1,4 @@
export * from './get-public-path';
export * from './monitor';
export * from './obx';
export * from './request';
export * from './focus-tracker';

View File

@ -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 };

View File

@ -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"

View File

@ -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;

View File

@ -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() {

View File

@ -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);
}
@ -475,10 +478,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);
@ -500,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;
@ -651,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,
@ -758,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);
}

View File

@ -60,6 +60,7 @@ export default function rendererFactory(): IRenderComponent {
schema: {} as RootSchema,
onCompGetRef: () => { },
onCompGetCtx: () => { },
thisRequiredInJSE: true,
};
static findDOMNode = findDOMNode;

View File

@ -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<string, any>;
designMode?: 'live' | 'design';
designMode?: 'design';
className?: string;
style?: CSSProperties;
id?: string | number;
getSchemaChangedSymbol?: () => boolean;
setSchemaChangedSymbol?: (symbol: boolean) => void;
thisRequiredInJSE?: boolean;
documentId?: string;
getNode?: any;
/**

View File

@ -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

View File

@ -994,6 +994,24 @@ exports[`JSExpression JSExpression props with loop 1`] = `
</div>
`;
exports[`JSExpression JSExpression props with loop, and thisRequiredInJSE is true 1`] = `
<div
className="lce-page"
style={Object {}}
>
<div
className="div-ut"
forwardRef={[Function]}
name1="1"
/>
<div
className="div-ut"
forwardRef={[Function]}
name1="2"
/>
</div>
`;
exports[`JSExpression JSFunction props 1`] = `
<div
className="lce-page"
@ -1121,3 +1139,20 @@ exports[`JSExpression base props 1`] = `
/>
</div>
`;
exports[`designMode designMode:default 1`] = `
<div
className="lce-page"
style={Object {}}
>
<div
className="div-ut"
forwardRef={[Function]}
>
<div
className="div-ut-children"
forwardRef={[Function]}
/>
</div>
</div>
`;

View File

@ -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<{
<Renderer
schema={schema}
components={components as any}
{...others}
/>);
const componentInstance = component.root;
@ -144,7 +145,9 @@ describe('JSExpression', () => {
]
};
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) => {
@ -156,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',
@ -321,4 +368,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();
});
});
})

View File

@ -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', () => {