test: add renderer-core hoc/leaf single test

This commit is contained in:
liujuping 2022-05-11 16:08:14 +08:00
parent bf2c1e96a1
commit 11319e1ac9
8 changed files with 729 additions and 174 deletions

View File

@ -23,6 +23,8 @@ const jestConfig = {
collectCoverageFrom: [
'src/**/*.ts',
'src/**/*.tsx',
'!src/utils/logger.ts',
'!src/types/index.ts',
],
};

View File

@ -216,24 +216,6 @@ export function leafWrapper(Comp: types.IBaseRenderComponent, {
this.recordTime();
}
get childrenMap(): any {
const map = new Map();
if (!this.hasChildren) {
return map;
}
this.children.forEach((d: any) => {
if (Array.isArray(d)) {
map.set(d[0].props.componentId, d);
return;
}
map.set(d.props.componentId, d);
});
return map;
}
get defaultState() {
const {
hidden = false,
@ -253,18 +235,18 @@ export function leafWrapper(Comp: types.IBaseRenderComponent, {
super(props, context);
// 监听以下事件,当变化时更新自己
__debug(`${schema.componentName}[${this.props.componentId}] leaf render in SimulatorRendererView`);
clearRerenderEvent(this.props.componentId);
clearRerenderEvent(componentCacheId);
const _leaf = this.leaf;
this.initOnPropsChangeEvent(_leaf);
this.initOnChildrenChangeEvent(_leaf);
this.initOnVisibleChangeEvent(_leaf);
this.curEventLeaf = _leaf;
cache.ref.set(props.componentId, {
cache.ref.set(componentCacheId, {
makeUnitRender: this.makeUnitRender,
});
let cacheState = cache.state.get(props.componentId);
let cacheState = cache.state.get(componentCacheId);
if (!cacheState || cacheState.__tag !== props.__tag) {
cacheState = this.defaultState;
}
@ -275,7 +257,7 @@ export function leafWrapper(Comp: types.IBaseRenderComponent, {
private curEventLeaf: Node | undefined;
setState(state: any) {
cache.state.set(this.props.componentId, {
cache.state.set(componentCacheId, {
...this.state,
...state,
__tag: this.props.__tag,
@ -489,7 +471,7 @@ export function leafWrapper(Comp: types.IBaseRenderComponent, {
// TODO: 缓存同级其他元素的 children。
// 缓存二级 children Next 查询筛选组件有问题
// 缓存一级 children Next Tab 组件有问题
const nextChild = getChildren(leaf?.export?.(TransformStage.Render) as types.ISchema, scope, Comp); // this.childrenMap
const nextChild = getChildren(leaf?.export?.(TransformStage.Render) as types.ISchema, scope, Comp);
__debug(`${schema.componentName}[${this.props.componentId}] component trigger onChildrenChange event`, nextChild);
this.setState({
nodeChildren: nextChild,
@ -531,7 +513,7 @@ export function leafWrapper(Comp: types.IBaseRenderComponent, {
}
get leaf(): Node | undefined {
return this.props._leaf || getNode(this.props.componentId);
return this.props._leaf || getNode(componentCacheId);
}
render() {

View File

@ -1,97 +1,146 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`leafWrapper base 1`] = `
<div
_leaf={
Node {
"emitter": EventEmitter {
"_events": Object {
"onChildrenChange": [Function],
"onPropChange": [Function],
"onVisibleChange": [Function],
},
"_eventsCount": 3,
"_maxListeners": undefined,
Symbol(kCapture): false,
},
"hasLoop": false,
"schema": Object {},
}
}
>
exports[`children this.props.children is array 1`] = `
<div>
<div
content="content"
>
content
</div>
<div
content="content"
>
content
</div>
</div>
`;
exports[`leafWrapper change ___condition___ props 1`] = `
exports[`lifecycle leaf change and make componentWillReceiveProps 1`] = `
<div>
<div
_leaf={
Node {
"emitter": EventEmitter {
"_events": Object {
"onChildrenChange": [Function],
"onPropChange": [Function],
"onVisibleChange": [Function],
},
"_eventsCount": 3,
"_maxListeners": undefined,
Symbol(kCapture): false,
},
"hasLoop": false,
"schema": Object {},
}
}
__tag="222"
content="content new leaf"
>
content new leaf
</div>
</div>
`;
exports[`lifecycle props change and make componentWillReceiveProps 1`] = `
<div>
<div
content="content"
>
content
</div>
</div>
`;
exports[`lifecycle props change and make componentWillReceiveProps 2`] = `
<div>
<div
content="content 123"
>
content 123
</div>
</div>
`;
exports[`lifecycle props change and make componentWillReceiveProps 3`] = `
<div>
<div
__tag="111"
content="content 123"
>
content 123
</div>
</div>
`;
exports[`mini unit render leaf has a loop, render from parent 1`] = `
<div>
this is a new children
</div>
`;
exports[`mini unit render make text props change 1`] = `
<div>
<div
content="content"
>
content
</div>
</div>
`;
exports[`mini unit render make text props change 2`] = `
<div
newPropKey="newPropValue"
/>
`;
exports[`leafWrapper change ___condition___ props, but not hidden component 1`] = `
<div
_leaf={
Node {
"emitter": EventEmitter {
"_events": Object {
"onChildrenChange": [Function],
"onPropChange": [Function],
"onVisibleChange": [Function],
},
"_eventsCount": 3,
"_maxListeners": undefined,
Symbol(kCapture): false,
},
"hasLoop": false,
"schema": Object {},
}
}
>
exports[`mini unit render parent is a mock leaf 1`] = `
<div>
new content
<div
content="new content to mock"
>
new content to mock
</div>
</div>
`;
exports[`leafWrapper change props 1`] = `
<div
_leaf={
Node {
"emitter": EventEmitter {
"_events": Object {
"onChildrenChange": [Function],
"onPropChange": [Function],
"onVisibleChange": [Function],
},
"_eventsCount": 3,
"_maxListeners": undefined,
Symbol(kCapture): false,
},
"hasLoop": false,
"schema": Object {},
}
}
>
exports[`mini unit render props has new children 1`] = `
<div>
new content
children 01
children 02
</div>
`;
exports[`onChildrenChange children is array string 1`] = `
<div>
onChildrenChange content 01
onChildrenChange content 02
</div>
`;
exports[`onPropChange change textNode [key:___condition___] props, but not hidden component 1`] = `
<div>
<div
content="content"
>
content
</div>
</div>
`;
exports[`onPropChange change textNode [key:___condition___] props, hide textNode component 1`] = `<div />`;
exports[`onPropChange change textNode [key:content], content in this.props but not in leaf.export result 1`] = `
<div>
<div
content="content"
>
content
</div>
</div>
`;
exports[`onPropChange change textNode [key:content], content in this.props but not in leaf.export result 2`] = `
<div>
<div
content={null}
/>
</div>
`;
exports[`onVisibleChange visible is false 1`] = `<div />`;
exports[`onVisibleChange visible is true 1`] = `
<div>
<div
content="content"
>
content
</div>
</div>
`;

View File

@ -6,12 +6,23 @@ import { leafWrapper } from '../../src/hoc/leaf';
import components from '../utils/components';
import Node from '../utils/node';
let rerenderCount = 0;
const nodeMap = new Map();
const makeSnapshot = (component) => {
let tree = component.toJSON();
expect(tree).toMatchSnapshot();
}
const baseRenderer: any = {
__debug () {},
__getComponentProps (schema: any) {
return schema.props;
},
__getSchemaChildrenVirtualDom () {},
__getSchemaChildrenVirtualDom (schema: any) {
return schema.children;
},
context: {
engine: {
createElement,
@ -19,110 +30,525 @@ const baseRenderer: any = {
},
props: {
__host: {},
getNode: () => {},
__container: () => {},
getNode: (id) => nodeMap.get(id),
__container: {
rerender: () => {
rerenderCount = 1 + rerenderCount;
}
}
describe('leafWrapper', () => {
const Div = leafWrapper(components.Div as any, {
schema: {
id: 'div',
},
baseRenderer,
componentInfo: {},
scope: {},
});
documentId: '01'
}
}
const DivNode = new Node({});
const TextNode = new Node({});
let Div, DivNode, Text, TextNode, component, textSchema, divSchema;
let id = 0;
const Text = leafWrapper(components.Text as any, {
schema: {
id: 'div',
beforeEach(() => {
textSchema = {
id: 'text' + id,
props: {
content: 'content'
}
},
};
divSchema = {
id: 'div' + id,
};
id++;
Div = leafWrapper(components.Div as any, {
schema: divSchema,
baseRenderer,
componentInfo: {},
scope: {},
});
const component = renderer.create(
DivNode = new Node(divSchema);
TextNode = new Node(textSchema);
nodeMap.set(divSchema.id, DivNode);
nodeMap.set(textSchema.id, TextNode);
Text = leafWrapper(components.Text as any, {
schema: textSchema,
baseRenderer,
componentInfo: {},
scope: {},
});
component = renderer.create(
// @ts-ignore
<Div _leaf={DivNode}>
<Text _leaf={TextNode} content="content"></Text>
</Div>
);
it('base', () => {
let tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
it('change props', () => {
afterEach(() => {
component.unmount(component);
});
describe('onPropChange', () => {
it('change textNode [key:content] props', () => {
TextNode.emitPropChange({
key: 'content',
newValue: 'new content',
} as any);
let tree = component.toJSON();
expect(tree).toMatchSnapshot();
const root = component.root;
expect(root.findByType(components.Text).props.content).toEqual('new content')
});
it('change ___condition___ props', () => {
it('change textNode [key:___condition___] props, hide textNode component', () => {
// mock leaf?.export result
TextNode.schema.condition = false;
TextNode.emitPropChange({
key: '___condition___',
newValue: false,
} as any);
let tree = component.toJSON();
expect(tree).toMatchSnapshot();
makeSnapshot(component);
});
it('change ___condition___ props, but not hidden component', () => {
it('change textNode [key:___condition___] props, but not hidden component', () => {
TextNode.schema.condition = true;
TextNode.emitPropChange({
key: '___condition___',
newValue: false,
} as any);
let tree = component.toJSON();
expect(tree).toMatchSnapshot();
makeSnapshot(component);
});
it('change textNode [key:content], content in this.props but not in leaf.export result', () => {
makeSnapshot(component);
delete TextNode.schema.props.content;
TextNode.emitPropChange({
key: 'content',
newValue: null,
} as any, true);
makeSnapshot(component);
const root = component.root;
const TextInst = root.findByType(components.Text);
expect(TextInst.props.content).toBeNull();
});
it('change textNode [key:___loop___], make rerender', () => {
expect(leafWrapper(components.Text as any, {
schema: textSchema,
baseRenderer,
componentInfo: {},
scope: {},
})).toEqual(Text);
const nextRerenderCount = rerenderCount + 1;
TextNode.emitPropChange({
key: '___loop___',
newValue: 'new content',
} as any);
expect(rerenderCount).toBe(nextRerenderCount);
expect(leafWrapper(components.Text as any, {
schema: textSchema,
baseRenderer,
componentInfo: {},
scope: {},
})).not.toEqual(Text);
});
});
describe('lifecycle', () => {
it('props change and make componentWillReceiveProps', () => {
makeSnapshot(component);
// 没有 __tag 标识
component.update((
<Div _leaf={DivNode}>
<Text _leaf={TextNode} content="content 123"></Text>
</Div>
));
makeSnapshot(component);
// 有 __tag 标识
component.update((
<Div _leaf={DivNode}>
<Text _leaf={TextNode} __tag="111" content="content 123"></Text>
</Div>
));
makeSnapshot(component);
});
it('leaf change and make componentWillReceiveProps', () => {
const newTextNodeLeaf = new Node(textSchema);
component.update((
<Div _leaf={DivNode}>
<Text _leaf={newTextNodeLeaf} __tag="222" content="content 123"></Text>
</Div>
));
newTextNodeLeaf.emitPropChange({
key: 'content',
newValue: 'content new leaf',
});
makeSnapshot(component);
});
});
describe('mini unit render', () => {
let miniRenderSchema, MiniRenderDiv, MiniRenderDivNode;
beforeEach(() => {
miniRenderSchema = {
id: 'miniDiv' + id,
};
MiniRenderDiv = leafWrapper(components.MiniRenderDiv as any, {
schema: miniRenderSchema,
baseRenderer,
componentInfo: {},
scope: {},
});
MiniRenderDivNode = new Node(miniRenderSchema, {
componentMeta: {
isMinimalRenderUnit: true,
},
});
TextNode = new Node(textSchema, {
parent: MiniRenderDivNode,
});
component = renderer.create(
// @ts-ignore
<MiniRenderDiv _leaf={MiniRenderDivNode}>
<Text _leaf={TextNode} content="content"></Text>
</MiniRenderDiv>
);
})
it('make text props change', () => {
if (!MiniRenderDivNode.schema.props) {
MiniRenderDivNode.schema.props = {};
}
MiniRenderDivNode.schema.props['newPropKey'] = 'newPropValue';
makeSnapshot(component);
const inst = component.root;
const TextInst = inst.findByType(Text).children[0];
TextNode.emitPropChange({
key: 'content',
newValue: 'new content',
} as any);
expect((TextInst as any)?._fiber.stateNode.renderUnitInfo).toEqual({
singleRender: false,
minimalUnitId: 'miniDiv' + id,
minimalUnitName: undefined,
});
makeSnapshot(component);
});
it('dont render mini render component', () => {
const TextNode = new Node(textSchema, {
parent: new Node({
id: 'random',
}, {
componentMeta: {
isMinimalRenderUnit: true,
},
}),
});
renderer.create(
// @ts-ignore
<div>
<Text _leaf={TextNode} content="content"></Text>
</div>
);
const nextCount = rerenderCount + 1;
TextNode.emitPropChange({
key: 'content',
newValue: 'new content',
} as any);
expect(rerenderCount).toBe(nextCount);
});
it('leaf is a mock function', () => {
const TextNode = new Node(textSchema, {
parent: {
isEmpty: () => false,
}
});
renderer.create(
// @ts-ignore
<div>
<Text _leaf={TextNode} content="content"></Text>
</div>
);
TextNode.emitPropChange({
key: 'content',
newValue: 'new content',
} as any);
});
it('change component leaf isRoot is true', () => {
const TextNode = new Node(textSchema, {
isRoot: true,
});
const component = renderer.create(
<Text _leaf={TextNode} content="content"></Text>
);
const inst = component.root;
TextNode.emitPropChange({
key: 'content',
newValue: 'new content',
} as any);
expect((inst.children[0] as any)?._fiber.stateNode.renderUnitInfo).toEqual({
singleRender: true,
});
});
it('change component leaf parent isRoot is true', () => {
const TextNode = new Node(textSchema, {
parent: new Node({
id: 'first-parent',
}, {
componentMeta: {
isMinimalRenderUnit: true,
},
parent: new Node({
id: 'rootId',
}, {
isRoot: true,
}),
})
});
describe('loop', () => {
const Div = leafWrapper(components.Div as any, {
schema: {
id: 'div',
},
baseRenderer,
componentInfo: {},
scope: {},
const component = renderer.create(
<Text _leaf={TextNode} content="content"></Text>
);
const inst = component.root;
TextNode.emitPropChange({
key: 'content',
newValue: 'new content',
} as any);
expect((inst.children[0] as any)?._fiber.stateNode.renderUnitInfo).toEqual({
singleRender: false,
minimalUnitId: 'first-parent',
minimalUnitName: undefined,
});
});
const DivNode = new Node({});
const TextNode = new Node({});
it('parent is a mock leaf', () => {
const MiniRenderDivNode = {};
const Text = leafWrapper(components.Text as any, {
schema: {
id: 'div',
const component = renderer.create(
// @ts-ignore
<MiniRenderDiv _leaf={MiniRenderDivNode}>
<Text _leaf={TextNode} content="content"></Text>
</MiniRenderDiv>
);
TextNode.emitPropChange({
key: 'content',
newValue: 'new content to mock',
} as any);
makeSnapshot(component);
});
it('props has new children', () => {
MiniRenderDivNode.schema.props.children = [
'children 01',
'children 02',
];
TextNode.emitPropChange({
key: 'content',
newValue: 'props'
});
makeSnapshot(component);
});
it('leaf has a loop, render from parent', () => {
MiniRenderDivNode = new Node(miniRenderSchema, {});
TextNode = new Node(textSchema, {
parent: MiniRenderDivNode,
hasLoop: true,
});
component = renderer.create(
// @ts-ignore
<MiniRenderDiv _leaf={MiniRenderDivNode}>
<Text _leaf={TextNode} content="content"></Text>
</MiniRenderDiv>
);
MiniRenderDivNode.schema.children = ['this is a new children'];
TextNode.emitPropChange({
key: 'content',
newValue: '1',
});
makeSnapshot(component);
});
});
describe('component cache', () => {
it('get different component with same is and different doc id', () => {
const baseRenderer02 = {
...baseRenderer,
props: {
content: 'content'
...baseRenderer.props,
documentId: '02',
}
},
}
const Div3 = leafWrapper(components.Div as any, {
schema: divSchema,
baseRenderer: baseRenderer02,
componentInfo: {},
scope: {},
});
expect(Div).not.toEqual(Div3);
});
it('get component again and get ths cache component', () => {
const Div2 = leafWrapper(components.Div as any, {
schema: divSchema,
baseRenderer,
componentInfo: {},
scope: {},
});
expect(Div).toEqual(Div2);
});
});
describe('onVisibleChange', () => {
it('visible is false', () => {
TextNode.emitVisibleChange(false);
makeSnapshot(component);
});
it('visible is true', () => {
TextNode.emitVisibleChange(true);
makeSnapshot(component);
});
});
describe('children', () => {
it('this.props.children is array', () => {
const component = renderer.create(
// @ts-ignore
<Div _leaf={DivNode}>
<Text _leaf={TextNode} content="content"></Text>
<Text _leaf={TextNode} content="content"></Text>
</Div>
);
makeSnapshot(component);
});
});
describe('onChildrenChange', () => {
it('children is array string', () => {
DivNode.schema.children = [
'onChildrenChange content 01',
'onChildrenChange content 02'
]
DivNode.emitChildrenChange();
makeSnapshot(component);
});
});
describe('not render leaf', () => {
let miniRenderSchema, MiniRenderDiv, MiniRenderDivNode;
beforeEach(() => {
miniRenderSchema = {
id: 'miniDiv' + id,
};
MiniRenderDivNode = new Node(miniRenderSchema, {
componentMeta: {
isMinimalRenderUnit: true,
},
});
nodeMap.set(miniRenderSchema.id, MiniRenderDivNode);
MiniRenderDiv = leafWrapper(components.MiniRenderDiv as any, {
schema: miniRenderSchema,
baseRenderer,
componentInfo: {},
scope: {},
});
TextNode = new Node(textSchema, {
parent: MiniRenderDivNode,
});
component = renderer.create(
<Text _leaf={TextNode} content="content"></Text>
);
});
it('onPropsChange', () => {
const nextCount = rerenderCount + 1;
MiniRenderDivNode.emitPropChange({
key: 'any',
newValue: 'any',
});
expect(rerenderCount).toBe(nextCount);
});
it('onChildrenChange', () => {
const nextCount = rerenderCount + 1;
MiniRenderDivNode.emitChildrenChange({
key: 'any',
newValue: 'any',
});
expect(rerenderCount).toBe(nextCount);
});
it('onVisibleChange', () => {
const nextCount = rerenderCount + 1;
MiniRenderDivNode.emitVisibleChange(true);
expect(rerenderCount).toBe(nextCount);
});
});

View File

@ -1026,7 +1026,20 @@ exports[`JSExpression JSSlot has loop 1`] = `
forwardRef={[Function]}
useFieldIdAsDomId={false}
>
<div>
<div
__id="node_ocl1ao1o7w4"
__style__=":root {
font-size: 14px;
color: #666;
}"
behavior="NORMAL"
className="text_l1ao7pfb"
content="这是一个低代码业务组件~"
fieldId="text_l1ao7lvp"
forwardRef={[Function]}
maxLine={0}
showTitle={false}
>
这是一个低代码业务组件~
</div>
</div>
@ -1044,7 +1057,20 @@ exports[`JSExpression JSSlot has loop 1`] = `
forwardRef={[Function]}
useFieldIdAsDomId={false}
>
<div>
<div
__id="node_ocl1ao1o7w4"
__style__=":root {
font-size: 14px;
color: #666;
}"
behavior="NORMAL"
className="text_l1ao7pfb"
content="这是一个低代码业务组件~"
fieldId="text_l1ao7lvp"
forwardRef={[Function]}
maxLine={0}
showTitle={false}
>
这是一个低代码业务组件~
</div>
</div>
@ -1062,7 +1088,20 @@ exports[`JSExpression JSSlot has loop 1`] = `
forwardRef={[Function]}
useFieldIdAsDomId={false}
>
<div>
<div
__id="node_ocl1ao1o7w4"
__style__=":root {
font-size: 14px;
color: #666;
}"
behavior="NORMAL"
className="text_l1ao7pfb"
content="这是一个低代码业务组件~"
fieldId="text_l1ao7lvp"
forwardRef={[Function]}
maxLine={0}
showTitle={false}
>
这是一个低代码业务组件~
</div>
</div>

View File

@ -11,6 +11,16 @@ jest.mock('zen-logger', () => {
};
});
jest.mock('lodash', () => {
const original = jest.requireActual('lodash');
return {
...original,
debounce: (fn) => () => fn(),
throttle: (fn) => () => fn(),
}
})
export const mockConsoleWarn = jest.fn();
console.warn = mockConsoleWarn;

View File

@ -1,9 +1,17 @@
import React from 'react';
import { Box, Breadcrumb, Form, Select, Input, Button, Table, Pagination, Dialog } from '@alifd/next';
const Div = (props: any) => (<div {...props}>{props.children}</div>);
const Div = ({_leaf, ...rest}: any) => (<div {...rest}>{rest.children}</div>);
const Text = (props: any) => (<div>{props.content}</div>);
const MiniRenderDiv = ({_leaf, ...rest}: any) => {
return (
<div {...rest}>
{rest.children}
</div>
);
};
const Text = ({_leaf, ...rest}: any) => (<div {...rest}>{rest.content}</div>);
const SlotComponent = (props: any) => props.mobileSlot;
@ -24,6 +32,7 @@ const components = {
Div,
SlotComponent,
Text,
MiniRenderDiv,
};
export default components;

View File

@ -6,16 +6,47 @@ export default class Node {
schema: any = {
props: {},
};
hasLoop = false;
constructor(schema: any) {
componentMeta = {};
parent;
hasLoop = () => this._hasLoop;
id;
_isRoot: false;
_hasLoop: false;
constructor(schema: any, info: any = {}) {
this.emitter = new EventEmitter();
this.schema = schema;
const {
componentMeta,
parent,
isRoot,
hasLoop,
} = info;
this.schema = {
props: {},
...schema,
};
this.componentMeta = componentMeta || {};
this.parent = parent;
this.id = schema.id;
this._isRoot = isRoot;
this._hasLoop = hasLoop;
}
mockLoop() {
this.hasLoop = true;
}
isRoot = () => this._isRoot;
// componentMeta() {
// return this.componentMeta;
// }
// mockLoop() {
// // this.hasLoop = true;
// }
onChildrenChange(fn: any) {
this.emitter.on('onChildrenChange', fn);
@ -24,6 +55,10 @@ export default class Node {
}
}
emitChildrenChange() {
this.emitter?.emit('onChildrenChange', {});
}
onPropChange(fn: any) {
this.emitter.on('onPropChange', fn);
return () => {
@ -31,11 +66,14 @@ export default class Node {
}
}
emitPropChange(val: PropChangeOptions) {
emitPropChange(val: PropChangeOptions, skip?: boolean) {
if (!skip) {
this.schema.props = {
...this.schema.props,
[val.key + '']: val.newValue,
}
}
this.emitter?.emit('onPropChange', val);
}