From 11319e1ac95107a45b903ac7273684a0a1276d9c Mon Sep 17 00:00:00 2001 From: liujuping Date: Wed, 11 May 2022 16:08:14 +0800 Subject: [PATCH] test: add renderer-core hoc/leaf single test --- packages/renderer-core/jest.config.js | 2 + packages/renderer-core/src/hoc/leaf.tsx | 30 +- .../hoc/__snapshots__/leaf.test.tsx.snap | 207 ++++--- .../renderer-core/tests/hoc/leaf.test.tsx | 536 ++++++++++++++++-- .../__snapshots__/renderer.test.tsx.snap | 45 +- packages/renderer-core/tests/setup.ts | 10 + .../renderer-core/tests/utils/components.tsx | 15 +- packages/renderer-core/tests/utils/node.ts | 58 +- 8 files changed, 729 insertions(+), 174 deletions(-) diff --git a/packages/renderer-core/jest.config.js b/packages/renderer-core/jest.config.js index 96b36f9d2..4e27e6c90 100644 --- a/packages/renderer-core/jest.config.js +++ b/packages/renderer-core/jest.config.js @@ -23,6 +23,8 @@ const jestConfig = { collectCoverageFrom: [ 'src/**/*.ts', 'src/**/*.tsx', + '!src/utils/logger.ts', + '!src/types/index.ts', ], }; diff --git a/packages/renderer-core/src/hoc/leaf.tsx b/packages/renderer-core/src/hoc/leaf.tsx index a4c529eab..cb09ec2e8 100644 --- a/packages/renderer-core/src/hoc/leaf.tsx +++ b/packages/renderer-core/src/hoc/leaf.tsx @@ -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() { diff --git a/packages/renderer-core/tests/hoc/__snapshots__/leaf.test.tsx.snap b/packages/renderer-core/tests/hoc/__snapshots__/leaf.test.tsx.snap index dbdfd1c5b..253a099c7 100644 --- a/packages/renderer-core/tests/hoc/__snapshots__/leaf.test.tsx.snap +++ b/packages/renderer-core/tests/hoc/__snapshots__/leaf.test.tsx.snap @@ -1,97 +1,146 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`leafWrapper base 1`] = ` -
-
+exports[`children this.props.children is array 1`] = ` +
+
+ content +
+
content
`; -exports[`leafWrapper change ___condition___ props 1`] = ` +exports[`lifecycle leaf change and make componentWillReceiveProps 1`] = ` +
+
+ content new leaf +
+
+`; + +exports[`lifecycle props change and make componentWillReceiveProps 1`] = ` +
+
+ content +
+
+`; + +exports[`lifecycle props change and make componentWillReceiveProps 2`] = ` +
+
+ content 123 +
+
+`; + +exports[`lifecycle props change and make componentWillReceiveProps 3`] = ` +
+
+ content 123 +
+
+`; + +exports[`mini unit render leaf has a loop, render from parent 1`] = ` +
+ this is a new children +
+`; + +exports[`mini unit render make text props change 1`] = ` +
+
+ content +
+
+`; + +exports[`mini unit render make text props change 2`] = `
`; -exports[`leafWrapper change ___condition___ props, but not hidden component 1`] = ` -
-
- new content +exports[`mini unit render parent is a mock leaf 1`] = ` +
+
+ new content to mock
`; -exports[`leafWrapper change props 1`] = ` -
-
- new content +exports[`mini unit render props has new children 1`] = ` +
+ children 01 + children 02 +
+`; + +exports[`onChildrenChange children is array string 1`] = ` +
+ onChildrenChange content 01 + onChildrenChange content 02 +
+`; + +exports[`onPropChange change textNode [key:___condition___] props, but not hidden component 1`] = ` +
+
+ content +
+
+`; + +exports[`onPropChange change textNode [key:___condition___] props, hide textNode component 1`] = `
`; + +exports[`onPropChange change textNode [key:content], content in this.props but not in leaf.export result 1`] = ` +
+
+ content +
+
+`; + +exports[`onPropChange change textNode [key:content], content in this.props but not in leaf.export result 2`] = ` +
+
+
+`; + +exports[`onVisibleChange visible is false 1`] = `
`; + +exports[`onVisibleChange visible is true 1`] = ` +
+
+ content
`; diff --git a/packages/renderer-core/tests/hoc/leaf.test.tsx b/packages/renderer-core/tests/hoc/leaf.test.tsx index 3a6e34c91..106e6741a 100644 --- a/packages/renderer-core/tests/hoc/leaf.test.tsx +++ b/packages/renderer-core/tests/hoc/leaf.test.tsx @@ -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; + } + }, + documentId: '01' } } -describe('leafWrapper', () => { - const Div = leafWrapper(components.Div as any, { - schema: { - id: 'div', +let Div, DivNode, Text, TextNode, component, textSchema, divSchema; +let id = 0; + +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 DivNode = new Node({}); - const TextNode = new Node({}); + DivNode = new Node(divSchema); + TextNode = new Node(textSchema); - const Text = leafWrapper(components.Text as any, { - schema: { - id: 'div', - props: { - content: 'content' - } - }, + nodeMap.set(divSchema.id, DivNode); + nodeMap.set(textSchema.id, TextNode); + + Text = leafWrapper(components.Text as any, { + schema: textSchema, baseRenderer, componentInfo: {}, scope: {}, }); - const component = renderer.create( + component = renderer.create( // @ts-ignore
); +}); - it('base', () => { - let tree = component.toJSON(); - expect(tree).toMatchSnapshot(); - }); +afterEach(() => { + component.unmount(component); +}); - it('change props', () => { +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(( +
+ +
+ )); + + makeSnapshot(component); + + // 有 __tag 标识 + component.update(( +
+ +
+ )); + + makeSnapshot(component); + }); + + it('leaf change and make componentWillReceiveProps', () => { + const newTextNodeLeaf = new Node(textSchema); + component.update(( +
+ +
+ )); + + 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 + + + + ); }) -}); -describe('loop', () => { - const Div = leafWrapper(components.Div as any, { - schema: { - id: 'div', - }, - baseRenderer, - componentInfo: {}, - scope: {}, + 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); }); - const DivNode = new Node({}); - const TextNode = new Node({}); + it('dont render mini render component', () => { + const TextNode = new Node(textSchema, { + parent: new Node({ + id: 'random', + }, { + componentMeta: { + isMinimalRenderUnit: true, + }, + }), + }); - const Text = leafWrapper(components.Text as any, { - schema: { - id: 'div', - props: { - content: 'content' + renderer.create( + // @ts-ignore +
+ +
+ ); + + 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, } - }, - baseRenderer, - componentInfo: {}, - scope: {}, + }); + + renderer.create( + // @ts-ignore +
+ +
+ ); + + TextNode.emitPropChange({ + key: 'content', + newValue: 'new content', + } as any); }); - const component = renderer.create( - // @ts-ignore -
+ it('change component leaf isRoot is true', () => { + const TextNode = new Node(textSchema, { + isRoot: true, + }); + + const component = renderer.create( -
- ); + ); + + 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, + }), + }) + }); + + const component = renderer.create( + + ); + + 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, + }); + }); + + it('parent is a mock leaf', () => { + const MiniRenderDivNode = {}; + + const component = renderer.create( + // @ts-ignore + + + + ); + + 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 + + + + ); + + 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: { + ...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 +
+ + +
+ ); + + 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( + + ); + }); + + 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); + }); }); diff --git a/packages/renderer-core/tests/renderer/__snapshots__/renderer.test.tsx.snap b/packages/renderer-core/tests/renderer/__snapshots__/renderer.test.tsx.snap index 2b96228ad..5ef56f775 100644 --- a/packages/renderer-core/tests/renderer/__snapshots__/renderer.test.tsx.snap +++ b/packages/renderer-core/tests/renderer/__snapshots__/renderer.test.tsx.snap @@ -1026,7 +1026,20 @@ exports[`JSExpression JSSlot has loop 1`] = ` forwardRef={[Function]} useFieldIdAsDomId={false} > -
+
这是一个低代码业务组件~
@@ -1044,7 +1057,20 @@ exports[`JSExpression JSSlot has loop 1`] = ` forwardRef={[Function]} useFieldIdAsDomId={false} > -
+
这是一个低代码业务组件~
@@ -1062,7 +1088,20 @@ exports[`JSExpression JSSlot has loop 1`] = ` forwardRef={[Function]} useFieldIdAsDomId={false} > -
+
这是一个低代码业务组件~
diff --git a/packages/renderer-core/tests/setup.ts b/packages/renderer-core/tests/setup.ts index 45fe6e19b..fd6fd9e5d 100644 --- a/packages/renderer-core/tests/setup.ts +++ b/packages/renderer-core/tests/setup.ts @@ -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; diff --git a/packages/renderer-core/tests/utils/components.tsx b/packages/renderer-core/tests/utils/components.tsx index 5cb557d4b..639151612 100644 --- a/packages/renderer-core/tests/utils/components.tsx +++ b/packages/renderer-core/tests/utils/components.tsx @@ -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) => (
{props.children}
); +const Div = ({_leaf, ...rest}: any) => (
{rest.children}
); -const Text = (props: any) => (
{props.content}
); +const MiniRenderDiv = ({_leaf, ...rest}: any) => { + return ( +
+ {rest.children} +
+ ); +}; + +const Text = ({_leaf, ...rest}: any) => (
{rest.content}
); const SlotComponent = (props: any) => props.mobileSlot; @@ -24,6 +32,7 @@ const components = { Div, SlotComponent, Text, + MiniRenderDiv, }; -export default components; \ No newline at end of file +export default components; diff --git a/packages/renderer-core/tests/utils/node.ts b/packages/renderer-core/tests/utils/node.ts index 2756191c6..01da07a69 100644 --- a/packages/renderer-core/tests/utils/node.ts +++ b/packages/renderer-core/tests/utils/node.ts @@ -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) { - this.schema.props = { - ...this.schema.props, - [val.key + '']: val.newValue, + emitPropChange(val: PropChangeOptions, skip?: boolean) { + if (!skip) { + this.schema.props = { + ...this.schema.props, + [val.key + '']: val.newValue, + } } + this.emitter?.emit('onPropChange', val); }