test: add ut for render-core/utils/common

This commit is contained in:
JackLian 2022-04-18 18:28:57 +08:00 committed by 絮黎
parent e08084e357
commit d5daba8aaf
24 changed files with 2687 additions and 664 deletions

View File

@ -1,24 +1,41 @@
module.exports = {
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
'^.+\\.(js|ts|tsx|jsx)$': 'babel-jest',
'^.+\\.(css|less|scss)$': './test/mock/styleMock.js',
},
// testMatch: ['**/bugs/*.test.ts'],
const fs = require('fs');
const { join } = require('path');
const esModules = ['zen-logger'].join('|');
const pkgNames = fs.readdirSync(join('..')).filter(pkgName => !pkgName.startsWith('.'));
const jestConfig = {
// transform: {
// // '^.+\\.[jt]sx?$': 'babel-jest',
// '^.+\\.(ts|tsx)$': 'ts-jest',
// // '^.+\\.(js|jsx)$': 'babel-jest',
// },
// testMatch: ['(/tests?/.*(test))\\.[jt]s$'],
// transformIgnorePatterns: [
// `/node_modules/(?!${esModules})/`,
// ],
testEnvironment: 'jsdom',
// testMatch: ['**/*/common.test.ts'],
transformIgnorePatterns: [
`/node_modules/(?!${esModules})/`,
],
setupFiles: ['./tests/fixtures/unhandled-rejection.ts'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
collectCoverage: false,
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
],
moduleNameMapper: {
'^.+.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
},
setupFilesAfterEnv: [
'./test/setup.ts',
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/icons/**',
'!src/locale/**',
'!src/builtin-simulator/utils/**',
'!src/plugin/sequencify.ts',
'!src/document/node/exclusive-group.ts',
'!src/document/node/props/value-to-source.ts',
'!src/builtin-simulator/live-editing/live-editing.ts',
'!src/designer/offset-observer.ts',
'!src/designer/clipboard.ts',
'!**/node_modules/**',
'!**/vendor/**',
],
};
// 只对本仓库内的 pkg 做 mapping
jestConfig.moduleNameMapper = {};
jestConfig.moduleNameMapper[`^@alilc/lowcode\\-(${pkgNames.join('|')})$`] = '<rootDir>/../$1/src';
module.exports = jestConfig;

View File

@ -10,8 +10,9 @@
"es"
],
"scripts": {
"build": "build-scripts build --skip-demo",
"test": "build-scripts test --config build.test.json",
"build": "build-scripts build --skip-demo"
"test:cov": "build-scripts test --config build.test.json --jest-coverage"
},
"dependencies": {
"@alilc/lowcode-datasource-engine": "^1.0.0",
@ -32,20 +33,24 @@
"zen-logger": "^1.1.4"
},
"devDependencies": {
"@alilc/lowcode-designer": "^1.0.5",
"@alib/build-scripts": "^0.1.18",
"@alilc/lowcode-test-mate": "^1.0.1",
"@babel/plugin-transform-typescript": "^7.16.8",
"@testing-library/react": "^11.2.2",
"@types/classnames": "^2.2.11",
"@types/debug": "^4.1.5",
"@types/jest": "^26.0.16",
"@types/lodash": "^4.14.167",
"@types/node": "^13.7.1",
"@types/prop-types": "^15.7.3",
"@types/react-test-renderer": "^17.0.1",
"@types/serialize-javascript": "^5.0.0",
"babel-jest": "^27.4.6",
"babel-jest": "^26.5.2",
"build-plugin-component": "^0.2.11",
"jest": "^27.4.7",
"jest": "^26.6.3",
"react-test-renderer": "^17.0.2",
"ts-jest": "^27.1.3"
"ts-jest": "^26.5.0"
},
"publishConfig": {
"access": "public",

View File

@ -1,6 +1,6 @@
import PropTypes from 'prop-types';
import baseRendererFactory from './base';
import { isEmpty, goldlog } from '../utils';
import { isEmpty } from '../utils';
import { IRendererAppHelper, IBaseRendererProps, IBaseRenderComponent } from '../types';
export default function addonRendererFactory(): IBaseRenderComponent {
@ -57,21 +57,6 @@ export default function addonRendererFactory(): IBaseRenderComponent {
}
}
goldlog = (goKey: string, params: any) => {
const { addonKey, addonConfig = {} } = this.props.config || {};
goldlog(
goKey,
{
addonKey,
package: addonConfig.package,
version: addonConfig.version,
...this.appHelper.logParams,
...params,
},
'addon',
);
};
get utils() {
const { utils = {} } = this.context.config || {};
return { ...this.appHelper.utils, ...utils };

View File

@ -930,7 +930,7 @@ export default function baseRendererFactory(): IBaseRenderComponent {
const buitin = capitalizeFirstLetter(this.__namespace);
const componentNames = [buitin, ...extraComponents];
return !isSchema(schema, true) || !componentNames.includes(schema?.componentName ?? '');
return !isSchema(schema) || !componentNames.includes(schema?.componentName ?? '');
};
get requestHandlersMap() {

View File

@ -1,7 +1,7 @@
import Debug from 'debug';
import adapter from '../adapter';
import contextFactory from '../context';
import { isFileSchema, goldlog, isEmpty } from '../utils';
import { isFileSchema, isEmpty } from '../utils';
import baseRendererFactory from './base';
import divFactory from '../components/Div';
import { IGeneralConstructor, IRenderComponent, IRendererProps, IRendererState } from '../types';
@ -71,14 +71,6 @@ export default function rendererFactory(): IRenderComponent {
}
async componentDidMount() {
goldlog(
'EXP',
{
action: 'appear',
value: !!this.props.designMode,
},
'renderer',
);
debug(`entry.componentDidMount - ${this.props.schema && this.props.schema.componentName}`);
}

View File

@ -2,6 +2,8 @@ import type { ComponentLifecycle, CSSProperties } from 'react';
import { BuiltinSimulatorHost } from '@alilc/lowcode-designer';
import { RequestHandler, NodeSchema, NodeData, RootSchema, JSONObject } from '@alilc/lowcode-types';
export type ISchema = NodeSchema | RootSchema;
/*
** Duck typed component type supporting both react and rax
*/

View File

@ -1,3 +1,4 @@
/* eslint-disable no-console */
/* eslint-disable no-new-func */
import Debug from 'debug';
import { isI18nData, RootSchema, NodeSchema, isJSExpression, JSSlot } from '@alilc/lowcode-types';
@ -35,35 +36,53 @@ const EXPRESSION_TYPE = {
I18N: 'i18n',
};
const hasSymbol = typeof Symbol === 'function' && Symbol.for;
const REACT_FORWARD_REF_TYPE = hasSymbol ? Symbol.for('react.forward_ref') : 0xead0;
const debug = Debug('utils:index');
const ENV = {
TBE: 'TBE',
WEBIDE: 'WEB-IDE',
VSCODE: 'VSCODE',
WEB: 'WEB',
/**
* check if schema passed in is a valid schema
* @name isSchema
* @returns boolean
*/
export function isSchema(schema: any): schema is NodeSchema {
if (isEmpty(schema)) {
return false;
}
// Leaf and Slot should be valid
if (schema.componentName === 'Leaf' || schema.componentName === 'Slot') {
return true;
}
if (Array.isArray(schema)) {
return schema.every((item) => isSchema(item));
}
// check if props is valid
const isValidProps = (props: any) => {
if (!props) {
return false;
}
if (isJSExpression(props)) {
return true;
}
return (typeof schema.props === 'object' && !Array.isArray(props));
};
return !!(schema.componentName && isValidProps(schema.props));
}
/**
* @name isSchema
* @description
* check if schema passed in is a container type, including : Component Block Page
* @param schema
* @returns boolean
*/
export function isSchema(schema: any, ignoreArr = false): schema is NodeSchema {
if (isEmpty(schema)) return false;
// Leaf 组件也返回 true
if (schema.componentName === 'Leaf' || schema.componentName === 'Slot') return true;
if (!ignoreArr && Array.isArray(schema)) return schema.every((item) => isSchema(item));
return !!(schema.componentName && schema.props && (typeof schema.props === 'object' || isJSExpression(schema.props)));
}
export function isFileSchema(schema: NodeSchema): schema is RootSchema {
if (isEmpty(schema)) return false;
return ['Page', 'Block', 'Component', 'Addon', 'Temp'].includes(schema.componentName);
if (!isSchema(schema)) {
return false;
}
return ['Page', 'Block', 'Component'].includes(schema.componentName);
}
// 判断当前页面是否被嵌入到同域的页面中
/**
* check if current page is nested within another page with same host
* @returns boolean
*/
export function inSameDomain() {
try {
return window.parent !== window && window.parent.location.host === window.location.host;
@ -72,8 +91,15 @@ export function inSameDomain() {
}
}
/**
* get css styled name from schema`s fileName
* FileName -> lce-file-name
* @returns string
*/
export function getFileCssName(fileName: string) {
if (!fileName) return;
if (!fileName) {
return;
}
const name = fileName.replace(/([A-Z])/g, '-$1').toLowerCase();
return (`lce-${name}`)
.split('-')
@ -81,79 +107,45 @@ export function getFileCssName(fileName: string) {
.join('-');
}
// 兼容乐高设计态 JSBlock 的老协议
/**
* check if a object is type of JSSlot
* @returns string
*/
export function isJSSlot(obj: any): obj is JSSlot {
return obj && typeof obj === 'object' && ([EXPRESSION_TYPE.JSSLOT, EXPRESSION_TYPE.JSBLOCK].includes(obj.type));
if (!obj) {
return false;
}
if (typeof obj !== 'object' || Array.isArray(obj)) {
return false;
}
// Compatible with the old protocol JSBlock
return ([EXPRESSION_TYPE.JSSLOT, EXPRESSION_TYPE.JSBLOCK].includes(obj.type));
}
/**
* @name wait
* @description
* get value from an object
* @returns string
*/
export function wait(ms: number) {
return new Promise((resolve) => setTimeout(() => resolve(true), ms));
}
export function curry(Comp: any, hocs = []) {
return hocs.reverse().reduce((pre, cur: (pre: any) => any) => {
return cur(pre);
}, Comp);
}
export function getValue(obj: any, path: string, defaultValue = {}) {
if (isEmpty(obj) || typeof obj !== 'object') return defaultValue;
// array is not valid type, return default value
if (Array.isArray(obj)) {
return defaultValue;
}
if (isEmpty(obj) || typeof obj !== 'object') {
return defaultValue;
}
const res = path.split('.').reduce((pre, cur) => {
return pre && pre[cur];
}, obj);
if (res === undefined) return defaultValue;
if (res === undefined) {
return defaultValue;
}
return res;
}
// 更新obj的内容但不改变obj的指针
export function fillObj(receiver: any = {}, ...suppliers: any) {
Object.keys(receiver).forEach((item) => {
delete receiver[item];
});
Object.assign(receiver, ...suppliers);
return receiver;
}
// 中划线转驼峰
export function toHump(name: string) {
// eslint-disable-next-line no-useless-escape
return name.replace(/\-(\w)/g, (_: any, letter: string) => {
return letter.toUpperCase();
});
}
// 驼峰转中划线
export function toLine(name: string) {
return name.replace(/([A-Z])/g, '-$1').toLowerCase();
}
// 获取当前环境
export function getEnv() {
const { userAgent } = navigator;
const isVscode = /Electron\//.test(userAgent);
if (isVscode) return ENV.VSCODE;
const isTheia = (window as any).is_theia === true;
if (isTheia) return ENV.WEBIDE;
return ENV.WEB;
}
/**
*
* @param {*} locale zh-CNen-US
* @param {*} messages
*/
export function generateI18n(locale = 'zh-CN', messages: any = {}) {
return (key: string, values = {}) => {
if (!messages || !messages[key]) return '';
const formater = new IntlMessageFormat(messages[key], locale);
return formater.format(values);
};
}
/**
*
* @param {*} key
@ -162,7 +154,9 @@ export function generateI18n(locale = 'zh-CN', messages: any = {}) {
* @param {*} messages
*/
export function getI18n(key: string, values = {}, locale = 'zh-CN', messages: Record<string, any> = {}) {
if (!messages || !messages[locale] || !messages[locale][key]) return '';
if (!messages || !messages[locale] || !messages[locale][key]) {
return '';
}
const formater = new IntlMessageFormat(messages[locale][key], locale);
return formater.format(values);
}
@ -172,107 +166,46 @@ export function getI18n(key: string, values = {}, locale = 'zh-CN', messages: Re
* @param {*} Comp
*/
export function canAcceptsRef(Comp: any) {
const hasSymbol = typeof Symbol === 'function' && Symbol.for;
const REACT_FORWARD_REF_TYPE = hasSymbol ? Symbol.for('react.forward_ref') : 0xead0;
// eslint-disable-next-line max-len
return Comp?.$$typeof === REACT_FORWARD_REF_TYPE || Comp?.prototype?.isReactComponent || Comp?.prototype?.setState || Comp._forwardRef;
}
/**
*
* @param {String} gmKey
* @param {Object} params
* @param {String} logKey
* transform array to a object
* @param arr array to be transformed
* @param key key of array item, which`s value will be used as key in result map
* @param overwrite overwrite existing item in result or not
* @returns object result map
*/
export function goldlog(gmKey: string, params = {}, logKey = 'other') {
// vscode 黄金令箭API
const sendIDEMessage = (window as any).sendIDEMessage || (inSameDomain() && (window.parent as any).sendIDEMessage);
const goKey = serializeParams({
sdkVersion: pkg.version,
env: getEnv(),
...params,
});
if (sendIDEMessage) {
sendIDEMessage({
action: 'goldlog',
data: {
logKey: `/lce.core.${logKey}`,
gmKey,
goKey,
},
});
}
(window as any)?.goldlog?.record(`/lce.core.${logKey}`, gmKey, goKey, 'POST');
}
// utils为编辑器打包生成的utils文件内容utilsConfig为数据库存放的utils配置
export function generateUtils(utils: any, utilsConfig: Array<{ name: string; type: string; content: any }>) {
if (!Array.isArray(utilsConfig)) return { ...utils };
const res: any = {};
utilsConfig.forEach((item) => {
if (!item.name || !item.type || !item.content) return;
if (item.type === 'function' && typeof item.content === 'function') {
res[item.name] = item.content;
} else if (item.type === 'npm' && utils[item.name]) {
res[item.name] = utils[item.name];
}
});
return res;
}
// 将函数返回结果转成promise形式如果函数有返回值则根据返回值的bool类型判断是reject还是resolve若函数无返回值默认执行resolve
export function transformToPromise(input: any) {
if (input instanceof Promise) return input;
return new Promise((resolve, reject) => {
if (input || input === undefined) {
resolve({});
} else {
reject();
}
});
}
export function moveArrayItem(arr: any[], sourceIdx: number, distIdx: number, direction: 'after' | 'before') {
if (
!Array.isArray(arr) ||
sourceIdx === distIdx ||
sourceIdx < 0 ||
sourceIdx >= arr.length ||
distIdx < 0 ||
distIdx >= arr.length
) return arr;
const item = arr[sourceIdx];
if (direction === 'after') {
arr.splice(distIdx + 1, 0, item);
} else {
arr.splice(distIdx, 0, item);
}
if (sourceIdx < distIdx) {
arr.splice(sourceIdx, 1);
} else {
arr.splice(sourceIdx + 1, 1);
}
return arr;
}
export function transformArrayToMap(arr: any[], key: string, overwrite = true) {
if (isEmpty(arr) || !Array.isArray(arr)) return {};
if (isEmpty(arr) || !Array.isArray(arr)) {
return {};
}
const res: any = {};
arr.forEach((item) => {
const curKey = item[key];
if (item[key] === undefined) return;
if (res[curKey] && !overwrite) return;
if (item[key] === undefined) {
return;
}
if (res[curKey] && !overwrite) {
return;
}
res[curKey] = item;
});
return res;
}
export function checkPropTypes(value: any, name: string, rule: any, componentName: string) {
let ruleFunction = rule;
if (typeof rule === 'string') {
rule = new Function(`"use strict"; const PropTypes = arguments[0]; return ${rule}`)(PropTypes2);
ruleFunction = new Function(`"use strict"; const PropTypes = arguments[0]; return ${rule}`)(PropTypes2);
}
if (!rule || typeof rule !== 'function') {
if (!ruleFunction || typeof ruleFunction !== 'function') {
console.warn('checkPropTypes should have a function type rule argument');
return true;
}
const err = rule(
const err = ruleFunction(
{
[name]: value,
},
@ -288,62 +221,16 @@ export function checkPropTypes(value: any, name: string, rule: any, componentNam
return !err;
}
export function transformSchemaToPure(obj: any) {
const pureObj = (obj: any): any => {
if (Array.isArray(obj)) {
return obj.map((item) => pureObj(item));
} else if (typeof obj === 'object') {
// 对于undefined及null直接返回
if (!obj) return obj;
const res: any = {};
forEach(obj, (val: any, key: string) => {
if (key.startsWith('__') && key !== '__ignoreParse') return;
res[key] = pureObj(val);
});
return res;
}
return obj;
};
return pureObj(obj);
}
export function transformSchemaToStandard(obj: any) {
const standardObj = (obj: any): any => {
if (Array.isArray(obj)) {
return obj.map((item) => standardObj(item));
} else if (typeof obj === 'object') {
// 对于undefined及null直接返回
if (!obj) return obj;
const res: any = {};
forEach(obj, (val: any, key: string) => {
if (key.startsWith('__') && key !== '__ignoreParse') return;
if (isSchema(val) && key !== 'children' && obj.type !== 'JSSlot') {
res[key] = {
type: 'JSSlot',
value: standardObj(val),
};
// table特殊处理
if (key === 'cell') {
res[key].params = ['value', 'index', 'record'];
}
} else {
res[key] = standardObj(val);
}
});
return res;
} else if (typeof obj === 'function') {
return {
type: 'JSFunction',
value: obj.toString(),
};
}
return obj;
};
return standardObj(obj);
}
/**
* transform string to a function
* @param str function in string form
* @returns funtion
*/
export function transformStringToFunction(str: string) {
if (typeof str !== 'string') return str;
if (typeof str !== 'string') {
return str;
}
if (inSameDomain() && (window.parent as any).__newFunc) {
return (window.parent as any).__newFunc(`"use strict"; return ${str}`)();
} else {
@ -351,6 +238,96 @@ export function transformStringToFunction(str: string) {
}
}
/**
* JSExpressionthis
* @param str expression in string form
* @param self scope object
* @returns funtion
*/
export function parseExpression(str: any, self: any) {
try {
const contextArr = ['"use strict";', 'var __self = arguments[0];'];
contextArr.push('return ');
let tarStr: string;
tarStr = (str.value || '').trim();
// NOTE: use __self replace 'this' in the original function str
// may be wrong in extreme case which contains '__self' already
tarStr = tarStr.replace(/this(\W|$)/g, (_a: any, b: any) => `__self${b}`);
tarStr = contextArr.join('\n') + tarStr;
// 默认调用顶层窗口的parseObj, 保障new Function的window对象是顶层的window对象
if (inSameDomain() && (window.parent as any).__newFunc) {
return (window.parent as any).__newFunc(tarStr)(self);
}
const code = `with($scope || {}) { ${tarStr} }`;
return new Function('$scope', code)(self);
} catch (err) {
debug('parseExpression.error', err, str, self);
return undefined;
}
}
/**
* capitalize first letter
* @param word string to be proccessed
* @returns string capitalized string
*/
export function capitalizeFirstLetter(word: string) {
if (!word || !isString(word) || word.length === 0) {
return word;
}
return word[0].toUpperCase() + word.slice(1);
}
/**
* check str passed in is a string type of not
* @param str obj to be checked
* @returns boolean
*/
export function isString(str: any): boolean {
return {}.toString.call(str) === '[object String]';
}
/**
* check if obj is type of variable structure
* @param obj object to be checked
* @returns boolean
*/
export function isVariable(obj: any) {
if (!obj || Array.isArray(obj)) {
return false;
}
return typeof obj === 'object' && obj?.type === 'variable';
}
/**
* i18n i18n
* @param i18nInfo object
* @param self context
*/
export function parseI18n(i18nInfo: any, self: any) {
return parseExpression({
type: EXPRESSION_TYPE.JSEXPRESSION,
value: `this.i18n('${i18nInfo.key}')`,
}, self);
}
/**
* for each key in targetObj, run fn with the value of the value, and the context paased in.
* @param targetObj object that keys will be for each
* @param fn function that process each item
* @param context
*/
export function forEach(targetObj: any, fn: any, context?: any) {
if (!targetObj || Array.isArray(targetObj) || isString(targetObj) || typeof targetObj !== 'object') {
return;
}
Object.keys(targetObj).forEach((key) => fn.call(context, targetObj[key], key));
}
export function parseData(schema: unknown, self: any): any {
if (isJSExpression(schema)) {
return parseExpression(schema, self);
@ -364,10 +341,14 @@ export function parseData(schema: unknown, self: any): any {
return schema.bind(self);
} else if (typeof schema === 'object') {
// 对于undefined及null直接返回
if (!schema) return schema;
if (!schema) {
return schema;
}
const res: any = {};
forEach(schema, (val: any, key: string) => {
if (key.startsWith('__')) return;
if (key.startsWith('__')) {
return;
}
res[key] = parseData(val, self);
});
return res;
@ -375,79 +356,22 @@ export function parseData(schema: unknown, self: any): any {
return schema;
}
/* 全匹配{{开头,}}结尾的变量表达式或者对象类型JSExpression支持省略this */
export function parseExpression(str: any, self: any) {
try {
const contextArr = ['"use strict";', 'var __self = arguments[0];'];
contextArr.push('return ');
let tarStr;
tarStr = (str.value || '').trim();
tarStr = tarStr.replace(/this(\W|$)/g, (_a: any, b: any) => `__self${b}`);
tarStr = contextArr.join('\n') + tarStr;
// 默认调用顶层窗口的parseObj,保障new Function的window对象是顶层的window对象
if (inSameDomain() && (window.parent as any).__newFunc) {
return (window.parent as any).__newFunc(tarStr)(self);
}
const code = `with($scope || {}) { ${tarStr} }`;
return new Function('$scope', code)(self);
} catch (err) {
debug('parseExpression.error', err, str, self);
return undefined;
}
}
// 首字母大写
export function capitalizeFirstLetter(word: string) {
return word[0].toUpperCase() + word.slice(1);
}
export function isVariable(obj: any) {
return obj && typeof obj === 'object' && obj?.type === 'variable';
}
/* 将 i18n 结构,降级解释为对 i18n 接口的调用 */
export function parseI18n(i18nInfo: any, self: any) {
return parseExpression({
type: EXPRESSION_TYPE.JSEXPRESSION,
value: `this.i18n('${i18nInfo.key}')`,
}, self);
}
export function forEach(obj: any, fn: any, context?: any) {
obj = obj || {};
Object.keys(obj).forEach(key => fn.call(context, obj[key], key));
}
export function shallowEqual(objA: any, objB: any) {
if (objA === objB) {
return true;
}
if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
return false;
}
const keysA = Object.keys(objA);
if (keysA.length !== Object.keys(objB).length) {
return false;
}
for (let i = 0, key; i < keysA.length; i++) {
key = keysA[i];
if (!objB.hasOwnProperty(key) || objA[key] !== objB[key]) {
return false;
}
}
return true;
}
/**
* process params for using in a url query
* @param obj params to be processed
* @returns string
*/
export function serializeParams(obj: any) {
let rst: any = [];
let result: any = [];
forEach(obj, (val: any, key: any) => {
if (val === null || val === undefined || val === '') return;
if (typeof val === 'object') rst.push(`${key}=${encodeURIComponent(JSON.stringify(val))}`);
else rst.push(`${key}=${encodeURIComponent(val)}`);
});
return rst.join('&');
if (val === null || val === undefined || val === '') {
return;
}
if (typeof val === 'object') {
result.push(`${key}=${encodeURIComponent(JSON.stringify(val))}`);
} else {
result.push(`${key}=${encodeURIComponent(val)}`);
}
});
return result.join('&');
}

View File

@ -1,280 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`loop schema loop key 1`] = `
<div
className="lce-page _css_pseudo_node_ocl1djd9o41"
style={Object {}}
>
<div
__componentName="RootHeader"
__id="node_ocl1djd9o42"
>
Component Not Found
</div>
<div
__componentName="RootContent"
__id="node_ocl1djd9o43"
contentBgColor="white"
contentMargin="20"
contentPadding="20"
>
<div
__componentName="Div"
__id="node_ocl1djd9o45"
__style__={Object {}}
behavior="NORMAL"
className="_css_pseudo_node_ocl1djd9o45"
customClassName=""
events={
Object {
"ignored": true,
}
}
fieldId="div_l1djdj1n"
useFieldIdAsDomId={false}
>
<div
__componentName="Div"
__id="node_ocl1djd9o46"
__style__={Object {}}
behavior="NORMAL"
className="_css_pseudo_node_ocl1djd9o46"
customClassName=""
events={
Object {
"ignored": true,
}
}
fieldId="div_l1djdj1o"
useFieldIdAsDomId={false}
>
Component Not Found
</div>
<div
__componentName="Div"
__id="node_ocl1djd9o46"
__style__={Object {}}
behavior="NORMAL"
className="_css_pseudo_node_ocl1djd9o46"
customClassName=""
events={
Object {
"ignored": true,
}
}
fieldId="div_l1djdj1o"
useFieldIdAsDomId={false}
>
Component Not Found
</div>
<div
__componentName="Div"
__id="node_ocl1djd9o46"
__style__={Object {}}
behavior="NORMAL"
className="_css_pseudo_node_ocl1djd9o46"
customClassName=""
events={
Object {
"ignored": true,
}
}
fieldId="div_l1djdj1o"
useFieldIdAsDomId={false}
>
Component Not Found
</div>
</div>
<div
__componentName="Div"
__id="node_ocl1djd9o45"
__style__={Object {}}
behavior="NORMAL"
className="_css_pseudo_node_ocl1djd9o45"
customClassName=""
events={
Object {
"ignored": true,
}
}
fieldId="div_l1djdj1n"
useFieldIdAsDomId={false}
>
<div
__componentName="Div"
__id="node_ocl1djd9o46"
__style__={Object {}}
behavior="NORMAL"
className="_css_pseudo_node_ocl1djd9o46"
customClassName=""
events={
Object {
"ignored": true,
}
}
fieldId="div_l1djdj1o"
useFieldIdAsDomId={false}
>
Component Not Found
</div>
<div
__componentName="Div"
__id="node_ocl1djd9o46"
__style__={Object {}}
behavior="NORMAL"
className="_css_pseudo_node_ocl1djd9o46"
customClassName=""
events={
Object {
"ignored": true,
}
}
fieldId="div_l1djdj1o"
useFieldIdAsDomId={false}
>
Component Not Found
</div>
<div
__componentName="Div"
__id="node_ocl1djd9o46"
__style__={Object {}}
behavior="NORMAL"
className="_css_pseudo_node_ocl1djd9o46"
customClassName=""
events={
Object {
"ignored": true,
}
}
fieldId="div_l1djdj1o"
useFieldIdAsDomId={false}
>
Component Not Found
</div>
</div>
<div
__componentName="Div"
__id="node_ocl1djd9o45"
__style__={Object {}}
behavior="NORMAL"
className="_css_pseudo_node_ocl1djd9o45"
customClassName=""
events={
Object {
"ignored": true,
}
}
fieldId="div_l1djdj1n"
useFieldIdAsDomId={false}
>
<div
__componentName="Div"
__id="node_ocl1djd9o46"
__style__={Object {}}
behavior="NORMAL"
className="_css_pseudo_node_ocl1djd9o46"
customClassName=""
events={
Object {
"ignored": true,
}
}
fieldId="div_l1djdj1o"
useFieldIdAsDomId={false}
>
Component Not Found
</div>
<div
__componentName="Div"
__id="node_ocl1djd9o46"
__style__={Object {}}
behavior="NORMAL"
className="_css_pseudo_node_ocl1djd9o46"
customClassName=""
events={
Object {
"ignored": true,
}
}
fieldId="div_l1djdj1o"
useFieldIdAsDomId={false}
>
Component Not Found
</div>
<div
__componentName="Div"
__id="node_ocl1djd9o46"
__style__={Object {}}
behavior="NORMAL"
className="_css_pseudo_node_ocl1djd9o46"
customClassName=""
events={
Object {
"ignored": true,
}
}
fieldId="div_l1djdj1o"
useFieldIdAsDomId={false}
>
Component Not Found
</div>
</div>
</div>
<div
__componentName="RootFooter"
__id="node_ocl1djd9o44"
>
Component Not Found
</div>
</div>
`;
exports[`notFountComponent not found snapshot 1`] = `
<div
className="lce-page _css_pseudo_node_ockyigdqxl1"
style={Object {}}
>
<div
__componentName="RootHeader"
__id="node_ockyigdqxl2"
>
Component Not Found
</div>
<div
__componentName="RootContent"
__id="node_ockyigdqxl3"
contentBgColor="white"
contentMargin="20"
contentPadding="20"
>
<div
__componentName="Button"
__id="node_ockyigdqxl5"
__style__={Object {}}
baseIcon=""
behavior="NORMAL"
className="_css_pseudo_node_ockyigdqxl5"
content="按 钮"
events={
Object {
"ignored": true,
}
}
fieldId="button_kyige3yf"
loading={false}
otherIcon=""
size="medium"
triggerEventsWhenLoading={false}
type="primary"
>
Component Not Found
</div>
</div>
<div
__componentName="RootFooter"
__id="node_ockyigdqxl4"
>
Component Not Found
</div>
</div>
`;

View File

@ -1,42 +0,0 @@
import renderer from 'react-test-renderer';
import React from 'react';
import '../utils/react-env-init';
import pageRendererFactory from '../../src/renderer/renderer';
import { sampleSchema } from '../mock/sample';
import loopSchema from '../mock/loop';
describe('notFountComponent', () => {
const Render = pageRendererFactory();
const component = renderer.create(
// @ts-ignore
<Render
schema={sampleSchema as any}
components={{}}
appHelper={{}}
/>,
);
it('not found snapshot', () => {
let tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});
describe('loop schema', () => {
it('loop key', () => {
const Render = pageRendererFactory();
const component = renderer.create(
// @ts-ignore
<Render
schema={loopSchema as any}
components={{}}
appHelper={{}}
/>,
);
let tree = component.toJSON();
expect(tree).toMatchSnapshot();
})
})

View File

@ -0,0 +1,567 @@
export default {
componentName: 'Page',
id: 'node_dockcviv8fo1',
props: {
ref: 'outterView',
autoLoading: true,
style: {
padding: '0 5px 0 5px',
},
},
fileName: 'test',
dataSource: {
list: [],
},
state: {
text: 'outter',
isShowDialog: false,
},
css: 'body {font-size: 12px;} .botton{width:100px;color:#ff00ff}',
lifeCycles: {
componentDidMount: {
type: 'JSFunction',
value: "function() {\n console.log('did mount');\n }",
},
componentWillUnmount: {
type: 'JSFunction',
value: "function() {\n console.log('will umount');\n }",
},
},
methods: {
testFunc: {
type: 'JSFunction',
value: "function() {\n console.log('test func');\n }",
},
onClick: {
type: 'JSFunction',
value: 'function() {\n this.setState({\n isShowDialog: true\n })\n }',
},
closeDialog: {
type: 'JSFunction',
value: 'function() {\n this.setState({\n isShowDialog: false\n })\n }',
},
},
children: [
{
componentName: 'Box',
id: 'node_dockcy8n9xed',
props: {
style: {
backgroundColor: 'rgba(31,56,88,0.1)',
padding: '12px 12px 12px 12px',
},
},
children: [
{
componentName: 'Box',
id: 'node_dockcy8n9xee',
props: {
style: {
padding: '12px 12px 12px 12px',
backgroundColor: '#ffffff',
},
},
children: [
{
componentName: 'Breadcrumb',
id: 'node_dockcy8n9xef',
props: {
prefix: 'next-',
maxNode: 100,
component: 'nav',
},
children: [
{
componentName: 'Breadcrumb.Item',
id: 'node_dockcy8n9xeg',
props: {
prefix: 'next-',
children: '首页',
},
},
{
componentName: 'Breadcrumb.Item',
id: 'node_dockcy8n9xei',
props: {
prefix: 'next-',
children: '品质中台',
},
},
{
componentName: 'Breadcrumb.Item',
id: 'node_dockcy8n9xek',
props: {
prefix: 'next-',
children: '商家品质页面管理',
},
},
{
componentName: 'Breadcrumb.Item',
id: 'node_dockcy8n9xem',
props: {
prefix: 'next-',
children: '质检知识条配置',
},
},
],
},
],
},
{
componentName: 'Box',
id: 'node_dockcy8n9xeo',
props: {
style: {
marginTop: '12px',
backgroundColor: '#ffffff',
},
},
children: [
{
componentName: 'Form',
id: 'node_dockcy8n9xep',
props: {
inline: true,
style: {
marginTop: '12px',
marginRight: '12px',
marginLeft: '12px',
},
__events: [],
},
children: [
{
componentName: 'Form.Item',
id: 'node_dockcy8n9xeq',
props: {
style: {
marginBottom: '0',
},
label: '类目名:',
},
children: [
{
componentName: 'Select',
id: 'node_dockcy8n9xer',
props: {
mode: 'single',
hasArrow: true,
cacheValue: true,
style: {
width: '150px',
},
},
},
],
},
{
componentName: 'Form.Item',
id: 'node_dockcy8n9xes',
props: {
style: {
marginBottom: '0',
},
label: '项目类型:',
},
children: [
{
componentName: 'Select',
id: 'node_dockcy8n9xet',
props: {
mode: 'single',
hasArrow: true,
cacheValue: true,
style: {
width: '200px',
},
},
},
],
},
{
componentName: 'Form.Item',
id: 'node_dockcy8n9xeu',
props: {
style: {
marginBottom: '0',
},
label: '项目 ID',
},
children: [
{
componentName: 'Input',
id: 'node_dockcy8n9xev',
props: {
hasBorder: true,
size: 'medium',
autoComplete: 'off',
style: {
width: '200px',
},
},
},
],
},
{
componentName: 'Button.Group',
id: 'node_dockcy8n9xew',
props: {},
children: [
{
componentName: 'Button',
id: 'node_dockcy8n9xex',
props: {
type: 'primary',
style: {
margin: '0 5px 0 5px',
},
htmlType: 'submit',
children: '搜索',
},
},
{
componentName: 'Button',
id: 'node_dockcy8n9xe10',
props: {
type: 'normal',
style: {
margin: '0 5px 0 5px',
},
htmlType: 'reset',
children: '清空',
},
},
],
},
],
},
],
},
{
componentName: 'Box',
id: 'node_dockcy8n9xe1f',
props: {
style: {
backgroundColor: '#ffffff',
paddingBottom: '24px',
display: 'flex',
flexDirection: 'row',
justifyContent: 'flex-end',
},
},
children: [
{
componentName: 'Button',
id: 'node_dockd5nrh9p4',
props: {
type: 'primary',
size: 'medium',
htmlType: 'button',
component: 'button',
children: '新建配置',
style: {},
__events: [
{
type: 'componentEvent',
name: 'onClick',
relatedEventName: 'onClick',
},
],
onClick: {
type: 'JSFunction',
value: 'function(){ this.onClick() }',
},
},
},
],
},
{
componentName: 'Box',
id: 'node_dockd5nrh9p5',
props: {},
children: [
{
componentName: 'Table',
id: 'node_dockjielosj1',
props: {
showMiniPager: true,
showActionBar: true,
actionBar: [
{
title: '新增',
type: 'primary',
},
{
title: '编辑',
},
],
columns: [
{
dataKey: 'name',
width: 200,
align: 'center',
title: '姓名',
editType: 'text',
},
{
dataKey: 'age',
width: 200,
align: 'center',
title: '年龄',
},
{
dataKey: 'email',
width: 200,
align: 'center',
title: '邮箱',
},
],
data: [
{
name: '王小',
id: '1',
age: 15000,
email: 'aaa@abc.com',
},
{
name: '王中',
id: '2',
age: 25000,
email: 'bbb@abc.com',
},
{
name: '王大',
id: '3',
age: 35000,
email: 'ccc@abc.com',
},
],
actionTitle: '操作',
actionWidth: 180,
actionType: 'link',
actionFixed: 'right',
actionHidden: false,
maxWebShownActionCount: 2,
actionColumn: [
{
title: '编辑',
callback: {
type: 'JSFunction',
value: '(rowData, action, table) => {\n return table.editRow(rowData).then((row) => {\n console.log(row);\n });\n }',
},
device: [
'desktop',
],
},
{
title: '保存',
callback: {
type: 'JSFunction',
value: '(rowData, action, table) => { \nreturn table.saveRow(rowData).then((row) => { \nconsole.log(row); \n}); \n}',
},
mode: 'EDIT',
},
],
},
},
{
componentName: 'Box',
id: 'node_dockd5nrh9pg',
props: {
style: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'flex-end',
},
},
children: [
{
componentName: 'Pagination',
id: 'node_dockd5nrh9pf',
props: {
prefix: 'next-',
type: 'normal',
shape: 'normal',
size: 'medium',
defaultCurrent: 1,
total: 100,
pageShowCount: 5,
pageSize: 10,
pageSizePosition: 'start',
showJump: true,
style: {},
},
},
],
},
],
},
],
},
{
componentName: 'Dialog',
id: 'node_dockcy8n9xe1h',
props: {
prefix: 'next-',
footerAlign: 'right',
footerActions: [
'ok',
'cancel',
],
closeable: 'esc,close',
hasMask: true,
align: 'cc cc',
minMargin: 40,
visible: {
type: 'JSExpression',
value: 'this.state.isShowDialog',
},
title: '标题',
events: [],
__events: [
{
type: 'componentEvent',
name: 'onCancel',
relatedEventName: 'closeDialog',
},
{
type: 'componentEvent',
name: 'onClose',
relatedEventName: 'closeDialog',
},
{
type: 'componentEvent',
name: 'onOk',
relatedEventName: 'testFunc',
},
],
onCancel: {
type: 'JSFunction',
value: 'function(){ this.closeDialog() }',
},
onClose: {
type: 'JSFunction',
value: 'function(){ this.closeDialog() }',
},
onOk: {
type: 'JSFunction',
value: 'function(){ this.testFunc() }',
},
},
children: [
{
componentName: 'Form',
id: 'node_dockd5nrh9pi',
props: {
inline: false,
labelAlign: 'top',
labelTextAlign: 'right',
size: 'medium',
},
children: [
{
componentName: 'Form.Item',
id: 'node_dockd5nrh9pj',
props: {
style: {
marginBottom: '0',
minWidth: '200px',
minHeight: '28px',
},
label: '商品类目',
},
children: [
{
componentName: 'Select',
id: 'node_dockd5nrh9pk',
props: {
mode: 'single',
hasArrow: true,
cacheValue: true,
},
},
],
},
{
componentName: 'Form.Item',
id: 'node_dockd5nrh9pl',
props: {
style: {
marginBottom: '0',
minWidth: '200px',
minHeight: '28px',
},
label: '商品类目',
},
children: [
{
componentName: 'Select',
id: 'node_dockd5nrh9pm',
props: {
mode: 'single',
hasArrow: true,
cacheValue: true,
},
},
],
},
{
componentName: 'Form.Item',
id: 'node_dockd5nrh9pn',
props: {
style: {
marginBottom: '0',
minWidth: '200px',
minHeight: '28px',
},
label: '商品类目',
asterisk: true,
},
children: [
{
componentName: 'Select',
id: 'node_dockd5nrh9po',
props: {
mode: 'single',
hasArrow: true,
cacheValue: true,
},
},
],
},
{
componentName: 'Form.Item',
id: 'node_dockd5nrh9pp',
props: {
style: {
marginBottom: '0',
minWidth: '200px',
minHeight: '28px',
},
label: '商品类目',
},
children: [
{
componentName: 'Input',
id: 'node_dockd5nrh9pr',
props: {
hasBorder: true,
size: 'medium',
autoComplete: 'off',
},
},
],
},
],
},
],
},
{
componentName: 'ErrorComponent',
id: 'node_dockd5nrh9pr',
props: {
name: 'error',
},
},
],
};

View File

@ -0,0 +1,7 @@
if (!process.env.LISTENING_TO_UNHANDLED_REJECTION) {
process.on('unhandledRejection', reason => {
throw reason;
});
// Avoid memory leak by adding too many listeners
process.env.LISTENING_TO_UNHANDLED_REJECTION = 'true';
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,324 @@
import React from 'react';
import renderer from 'react-test-renderer';
import schema from '../fixtures/schema/basic';
import '../utils/react-env-init';
import rendererFactory from '../../src/renderer/renderer';
import components from '../utils/components';
const Renderer = rendererFactory();
function getComp(schema, comp = null): Promise<{
component,
inst,
}> {
return new Promise((resolve, reject) => {
const component = renderer.create(
// @ts-ignore
<Renderer
schema={schema}
components={components as any}
/>);
const componentInstance = component.root;
setTimeout(() => {
resolve({
inst: comp ? componentInstance.findAllByType(comp) : null,
component,
});
}, 20);
})
}
beforeEach(() => {
});
let componentSnapshot;
afterEach(() => {
if (componentSnapshot) {
let tree = componentSnapshot.toJSON();
expect(tree).toMatchSnapshot();
componentSnapshot = null;
}
});
describe('Base Render', () => {
it('renderComp', () => {
const content = (
// @ts-ignore
<Renderer
schema={schema as any}
components={components as any}
/>);
const tree = renderer.create(content).toJSON();
expect(tree).toMatchSnapshot();
});
});
describe('JSExpression', () => {
it('base props', (done) => {
const schema = {
componentName: 'Page',
props: {},
children: [
{
componentName: "Div",
props: {
className: 'div-ut',
text: "123",
visible: true,
}
}
]
};
getComp(schema, components.Div).then(({ component, inst }) => {
expect(inst[0].props.text).toBe('123');
expect(inst[0].props.visible).toBeTruthy();
componentSnapshot = component;
done();
});
});
it('JSExpression props', (done) => {
const schema = {
componentName: 'Page',
props: {},
state: {
isShowDialog: true,
},
children: [
{
componentName: "Div",
props: {
className: "div-ut",
visible: {
type: 'JSExpression',
value: 'this.state.isShowDialog',
},
}
}
]
};
getComp(schema, components.Div).then(({ component, inst }) => {
expect(inst[0].props.visible).toBeTruthy();
componentSnapshot = component;
done();
});
});
it('JSExpression props with loop', (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[0].props.visible).toBeTruthy();
expect(inst.length).toEqual(2);
[1, 2].forEach((i) => {
expect(inst[0].props[`name${i}`]).toBe('1');
expect(inst[1].props[`name${i}`]).toBe('2');
})
componentSnapshot = component;
done();
});
});
// it('JSFunction props with loop', (done) => {
// const schema = {
// componentName: 'Page',
// props: {},
// state: {
// isShowDialog: true,
// },
// children: [
// {
// componentName: "Div",
// loop: [
// {
// name: '1',
// },
// {
// name: '2'
// }
// ],
// props: {
// className: "div-ut",
// onClick1: {
// type: 'JSFunction',
// value: '() => this.item.name',
// },
// onClick2: {
// type: 'JSFunction',
// value: 'function(){ return this.item.name }',
// },
// onClick3: {
// type: 'JSFunction',
// value: 'function(){ return item.name }',
// },
// onClick4: {
// type: 'JSFunction',
// value: '() => item.name',
// }
// }
// }
// ]
// };
// getComp(schema, components.Div).then(({ component, inst }) => {
// // expect(inst[0].props.visible).toBeTruthy();
// expect(inst.length).toEqual(2);
// [1, 2, 3, 4].forEach((i) => {
// expect(inst[0].props[`onClick${i}`]()).toBe('1');
// expect(inst[1].props[`onClick${i}`]()).toBe('2');
// })
// componentSnapshot = component;
// done();
// });
// });
it('JSFunction props', (done) => {
const schema = {
componentName: 'Page',
props: {},
state: {
isShowDialog: true,
},
children: [
{
componentName: "Div",
props: {
className: "div-ut",
onClick: {
type: 'JSFunction',
value: 'function() {return this.state.isShowDialog}',
},
}
}
]
};
getComp(schema, components.Div).then(({ component, inst }) => {
expect(!!inst[0].props.onClick).toBeTruthy();
expect(inst[0].props.onClick()).toBeTruthy();
componentSnapshot = component;
done();
});
});
it('JSSlot has loop', (done) => {
const schema = {
componentName: "Page",
props: {},
children: [
{
componentName: "SlotComponent",
id: "node_k8bnubvz",
props: {
mobileSlot: {
type: "JSSlot",
title: "mobile容器",
name: "mobileSlot",
value: [
{
condition: true,
hidden: false,
children: [
{
condition: true,
hidden: false,
loopArgs: [
"item",
"index"
],
isLocked: false,
conditionGroup: "",
componentName: "Text",
id: "node_ocl1ao1o7w4",
title: "",
props: {
maxLine: 0,
showTitle: false,
className: "text_l1ao7pfb",
behavior: "NORMAL",
content: "这是一个低代码业务组件~",
__style__: ":root {\n font-size: 14px;\n color: #666;\n}",
fieldId: "text_l1ao7lvp"
}
}
],
loop: {
type: "JSExpression",
value: "state.content"
},
loopArgs: [
"item",
"index"
],
isLocked: false,
conditionGroup: "",
componentName: "Div",
id: "node_ocl1ao1o7w3",
title: "",
props: {
useFieldIdAsDomId: false,
customClassName: "",
className: "div_l1ao7pfc",
behavior: "NORMAL",
__style__: ":root {\n padding: 12px;\n background: #f2f2f2;\n border: 1px solid #ddd;\n}",
fieldId: "div_l1ao7lvq"
}
}
]
},
},
}
],
state: {
content: {
type: "JSExpression",
value: "[{}, {}, {}]",
},
},
};
getComp(schema, components.Div).then(({ component, inst }) => {
expect(inst.length).toBe(3);
componentSnapshot = component;
done();
});
})
})

View File

@ -0,0 +1,12 @@
jest.mock('zen-logger', () => {
class Logger {
log() {}
error() {}
warn() {}
debug() {}
}
return {
__esModule: true,
default: Logger,
};
});

View File

@ -0,0 +1,426 @@
// @ts-nocheck
import {
isSchema,
isFileSchema,
inSameDomain,
getFileCssName,
isJSSlot,
getValue,
getI18n,
transformArrayToMap,
transformStringToFunction,
isVariable,
capitalizeFirstLetter,
forEach,
isString,
serializeParams,
parseExpression,
parseI18n,
parseData,
} from '../../src/utils/common';
describe('test isSchema', () => {
it('should be false when empty value is passed', () => {
expect(isSchema(null)).toBeFalsy();
expect(isSchema(undefined)).toBeFalsy();
expect(isSchema('')).toBeFalsy();
expect(isSchema({})).toBeFalsy();
});
it('should be true when componentName is Leaf or Slot ', () => {
expect(isSchema({ componentName: 'Leaf' })).toBeTruthy();
expect(isSchema({ componentName: 'Slot' })).toBeTruthy();
});
it('should check each item of an array', () => {
const validArraySchema = [
{ componentName: 'Button', props: {}},
{ componentName: 'Button', props: { type: 'JSExpression' }},
{ componentName: 'Leaf' },
{ componentName: 'Slot'},
];
const invalidArraySchema = [
...validArraySchema,
{ componentName: 'ComponentWithoutProps'},
];
expect(isSchema(validArraySchema)).toBeTruthy();
expect(isSchema(invalidArraySchema)).toBeFalsy();
});
it('normal valid schema should contains componentName, and props of type object or JSExpression', () => {
expect(isSchema({ componentName: 'Button', props: {}})).toBeTruthy();
expect(isSchema({ componentName: 'Button', props: { type: 'JSExpression' }})).toBeTruthy();
expect(isSchema({ xxxName: 'Button'})).toBeFalsy();
expect(isSchema({ componentName: 'Button', props: null})).toBeFalsy();
expect(isSchema({ componentName: 'Button', props: []})).toBeFalsy();
expect(isSchema({ componentName: 'Button', props: 'props string'})).toBeFalsy();
});
});
describe('test isFileSchema ', () => {
it('should be false when invalid schema is passed', () => {
expect(isFileSchema({ xxxName: 'Button'})).toBeFalsy();
expect(isFileSchema({ componentName: 'Button', props: null})).toBeFalsy();
expect(isFileSchema({ componentName: 'Button', props: []})).toBeFalsy();
expect(isFileSchema({ componentName: 'Button', props: 'props string'})).toBeFalsy();
});
it('should be true only when schema with root named Page || Block || Component is passed', () => {
expect(isFileSchema({ componentName: 'Page', props: {}})).toBeTruthy();
expect(isFileSchema({ componentName: 'Block', props: {}})).toBeTruthy();
expect(isFileSchema({ componentName: 'Component', props: {}})).toBeTruthy();
expect(isFileSchema({ componentName: 'Button', props: {}})).toBeFalsy();
});
});
describe('test inSameDomain ', () => {
let windowSpy;
beforeEach(() => {
windowSpy = jest.spyOn(window, "window", "get");
});
afterEach(() => {
windowSpy.mockRestore();
});
it('should work', () => {
windowSpy.mockImplementation(() => ({
parent: {
location: {
host: "example.com"
},
},
location: {
host: "example.com"
}
}));
expect(inSameDomain()).toBeTruthy();
windowSpy.mockImplementation(() => ({
parent: {
location: {
host: "example.com"
},
},
location: {
host: "another.com"
}
}));
expect(inSameDomain()).toBeFalsy();
windowSpy.mockImplementation(() => ({
parent: null,
location: {
host: "example.com"
}
}));
expect(inSameDomain()).toBeFalsy();
});
});
describe('test getFileCssName ', () => {
it('should work', () => {
expect(getFileCssName(null)).toBe(undefined);
expect(getFileCssName(undefined)).toBe(undefined);
expect(getFileCssName('')).toBe(undefined);
expect(getFileCssName('FileName')).toBe('lce-file-name');
expect(getFileCssName('Page1_abc')).toBe('lce-page1_abc');
});
});
describe('test isJSSlot ', () => {
it('should work', () => {
expect(isJSSlot(null)).toBeFalsy();
expect(isJSSlot(undefined)).toBeFalsy();
expect(isJSSlot('stringValue')).toBeFalsy();
expect(isJSSlot([1, 2, 3])).toBeFalsy();
expect(isJSSlot({ type: 'JSSlot' })).toBeTruthy();
expect(isJSSlot({ type: 'JSBlock' })).toBeTruthy();
expect(isJSSlot({ type: 'anyOtherType' })).toBeFalsy();
});
});
describe('test getValue ', () => {
it('should check params', () => {
expect(getValue(null, 'somePath')).toStrictEqual({});
expect(getValue(undefined, 'somePath')).toStrictEqual({});
// array is not valid input, return default
expect(getValue([], 'somePath')).toStrictEqual({});
expect(getValue([], 'somePath', 'aaa')).toStrictEqual('aaa');
expect(getValue([1, 2, 3], 'somePath', 'aaa')).toStrictEqual('aaa');
expect(getValue({}, 'somePath')).toStrictEqual({});
expect(getValue({}, 'somePath', 'default')).toStrictEqual('default');
});
it('should work normally', () => {
// single segment path
expect(getValue({ a: 'aValue' }, 'a')).toStrictEqual('aValue');
expect(getValue({ a: 'aValue', f:null }, 'f')).toBeNull();
expect(getValue({ a: { b: 'bValue' } }, 'a.b')).toStrictEqual('bValue');
expect(getValue({ a: { b: 'bValue', c: { d: 'dValue' } } }, 'a.c.d')).toStrictEqual('dValue');
expect(getValue({ a: { b: 'bValue', c: { d: 'dValue' } } }, 'e')).toStrictEqual({});
});
});
describe('test getI18n ', () => {
it('should work', () => {
const messages = {
'zh-CN': {
'key1': '啊啊啊',
'key2': '哈哈哈',
},
};
expect(getI18n('keyString', {}, 'zh-CN')).toStrictEqual('');
expect(getI18n('keyString', {}, 'zh-CN', null)).toStrictEqual('');
expect(getI18n('keyString', {}, 'en-US', messages)).toStrictEqual('');
expect(getI18n('key3', {}, 'zh-CN', messages)).toStrictEqual('');
});
});
describe('test transformArrayToMap ', () => {
it('should work', () => {
expect(transformArrayToMap([])).toStrictEqual({});
expect(transformArrayToMap('not a array')).toStrictEqual({});
expect(transformArrayToMap({'not Array': 1})).toStrictEqual({});
let mockArray = [
{
name: 'jack',
age: 2,
},
{
name: 'jack',
age: 20,
}
];
// test override
expect(transformArrayToMap(mockArray, 'name', true).jack.age).toBe(20);
expect(transformArrayToMap(mockArray, 'name').jack.age).toBe(20);
expect(transformArrayToMap(mockArray, 'name', false).jack.age).toBe(2);
mockArray = [
{
name: 'jack',
age: 2,
},
{
name: 'rose',
age: 20,
}
];
// normal case
expect(transformArrayToMap(mockArray, 'name').jack.age).toBe(2);
expect(transformArrayToMap(mockArray, 'name').jack.name).toBe('jack');
expect(transformArrayToMap(mockArray, 'name').rose.age).toBe(20);
// key not exists
expect(transformArrayToMap(mockArray, 'nameEn')).toStrictEqual({});
});
});
describe('test transformStringToFunction ', () => {
it('should work', () => {
const mockFun = jest.fn();
expect(transformStringToFunction(mockFun)).toBe(mockFun);
expect(transformStringToFunction(111)).toBe(111);
let mockFnStr = 'function(){return 111;}';
let fn = transformStringToFunction(mockFnStr);
expect(fn()).toBe(111);
mockFnStr = '() => { return 222; }';
fn = transformStringToFunction(mockFnStr);
expect(fn()).toBe(222);
mockFnStr = 'function getValue() { return 333; }';
fn = transformStringToFunction(mockFnStr);
expect(fn()).toBe(333);
mockFnStr = 'function getValue(aaa) {\
return aaa; \
}';
fn = transformStringToFunction(mockFnStr);
expect(fn(123)).toBe(123);
});
});
describe('test isVariable ', () => {
it('should work', () => {
expect(isVariable(null)).toBeFalsy();
expect(isVariable(undefined)).toBeFalsy();
expect(isVariable([1, 2, 3])).toBeFalsy();
expect(isVariable({})).toBeFalsy();
expect(isVariable({ type: 'any other type' })).toBeFalsy();
expect(isVariable({ type: 'variable' })).toBeTruthy();
});
});
describe('test capitalizeFirstLetter ', () => {
it('should work', () => {
expect(capitalizeFirstLetter(null)).toBeNull();
expect(capitalizeFirstLetter()).toBeUndefined();
expect(capitalizeFirstLetter([1, 2, 3])).toStrictEqual([1, 2, 3]);
expect(capitalizeFirstLetter({ a: 1 })).toStrictEqual({ a: 1 });
expect(capitalizeFirstLetter('')).toStrictEqual('');
expect(capitalizeFirstLetter('a')).toStrictEqual('A');
expect(capitalizeFirstLetter('abcd')).toStrictEqual('Abcd');
});
});
describe('test forEach ', () => {
it('should work', () => {
const mockFn = jest.fn();
forEach(null, mockFn);
expect(mockFn).toBeCalledTimes(0);
forEach(undefined, mockFn);
expect(mockFn).toBeCalledTimes(0);
forEach([1, 2, 3], mockFn);
expect(mockFn).toBeCalledTimes(0);
forEach('stringValue', mockFn);
expect(mockFn).toBeCalledTimes(0);
forEach({ a: 1, b: 2, c: 3 }, mockFn);
expect(mockFn).toBeCalledTimes(3);
const mockFn2 = jest.fn();
forEach({ a: 1 }, mockFn2, { b: 'bbb' });
expect(mockFn2).toHaveBeenCalledWith(1, 'a');
let sum = 0;
const mockFn3 = function(value, key) { sum = value + this.b; };
forEach({ a: 1 }, mockFn3, { b: 10 });
expect(sum).toEqual(11);
});
});
describe('test isString ', () => {
it('should work', () => {
expect(isString(123)).toBeFalsy();
expect(isString([])).toBeFalsy();
expect(isString({})).toBeFalsy();
expect(isString(null)).toBeFalsy();
expect(isString(undefined)).toBeFalsy();
expect(isString(true)).toBeFalsy();
expect(isString('111')).toBeTruthy();
expect(isString(new String('111'))).toBeTruthy();
});
});
describe('test serializeParams ', () => {
it('should work', () => {
const mockParams = { a: 1, b: 2, c: 'cvalue', d:[1, 'a', {}], e: {e1: 'value1', e2: 'value2'}};
const result = serializeParams(mockParams);
const decodedParams = decodeURIComponent(result);
expect(result).toBe('a=1&b=2&c=cvalue&d=%5B1%2C%22a%22%2C%7B%7D%5D&e=%7B%22e1%22%3A%22value1%22%2C%22e2%22%3A%22value2%22%7D');
expect(decodedParams).toBe('a=1&b=2&c=cvalue&d=[1,"a",{}]&e={"e1":"value1","e2":"value2"}');
});
});
describe('test parseExpression ', () => {
it('can handle JSExpression', () => {
const mockExpression = {
"type": "JSExpression",
"value": "function (params) { return this.scopeValue + params.param1 + 5;}"
};
const result = parseExpression(mockExpression, { scopeValue: 1 });
expect(result({ param1: 2 })).toBe((1 + 2 + 5));
});
});
describe('test parseExpression ', () => {
it('can handle JSExpression', () => {
const mockExpression = {
"type": "JSExpression",
"value": "function (params) { return this.scopeValue + params.param1 + 5;}"
};
const result = parseExpression(mockExpression, { scopeValue: 1 });
expect(result({ param1: 2 })).toBe((1 + 2 + 5));
});
});
describe('test parseI18n ', () => {
it('can handle normal parseI18n', () => {
const mockI18n = {
"type": "i18n",
"key": "keyA"
};
const mockI18nFun = (key) => { return 'hahaha' + key;};
const result = parseI18n(mockI18n, { i18n: mockI18nFun });
expect(result).toBe('hahahakeyA');
});
});
describe('test parseData ', () => {
it('should work when isJSExpression === true', () => {
const mockExpression = {
"type": "JSExpression",
"value": "function (params) { return this.scopeValue + params.param1 + 5;}"
};
const result = parseData(mockExpression, { scopeValue: 1 });
expect(result({ param1: 2 })).toBe((1 + 2 + 5));
});
it('should work when isI18nData === true', () => {
const mockI18n = {
"type": "i18n",
"key": "keyA"
};
const mockI18nFun = (key) => { return 'hahaha' + key;};
const result = parseData(mockI18n, { i18n: mockI18nFun });
expect(result).toBe('hahahakeyA');
});
it('should work when schema is string', () => {
expect(parseData(' this is a normal string, will be trimmed only ')).toStrictEqual('this is a normal string, will be trimmed only');
});
it('should work when schema is array', () => {
const mockData = [
{
"type": "i18n",
"key": "keyA"
},
' this is a normal string, will be trimmed only ',
];
const mockI18nFun = (key) => { return 'hahaha' + key;};
const result = parseData(mockData, { i18n: mockI18nFun });
expect(result[0]).toStrictEqual('hahahakeyA');
expect(result[1]).toStrictEqual('this is a normal string, will be trimmed only');
});
it('should work when schema is function', () => {
const mockFn = function() { return this.a; };
const result = parseData(mockFn, { a: 111 });
expect(result()).toBe(111);
});
it('should work when schema is null or undefined', () => {
expect(parseData(null)).toBe(null);
expect(parseData(undefined)).toBe(undefined);
});
it('should work when schema is normal object', () => {
expect(parseData({})).toStrictEqual({});
const mockI18nFun = (key) => { return 'hahaha' + key;};
const result = parseData({
key1: {
"type": "i18n",
"key": "keyA"
},
key2: ' this is a normal string, will be trimmed only ',
__privateKey: 'any value',
}, { i18n: mockI18nFun });
expect(result.key1).toStrictEqual('hahahakeyA');
expect(result.key2).toStrictEqual('this is a normal string, will be trimmed only');
expect(result.__privateKey).toBeUndefined();
});
});