diff --git a/packages/renderer-core/jest.config.js b/packages/renderer-core/jest.config.js index c719abc5f..6fce0fb84 100644 --- a/packages/renderer-core/jest.config.js +++ b/packages/renderer-core/jest.config.js @@ -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('|')})$`] = '/../$1/src'; + +module.exports = jestConfig; \ No newline at end of file diff --git a/packages/renderer-core/package.json b/packages/renderer-core/package.json index d0a68a764..e76ed0e32 100644 --- a/packages/renderer-core/package.json +++ b/packages/renderer-core/package.json @@ -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", diff --git a/packages/renderer-core/src/renderer/addon.tsx b/packages/renderer-core/src/renderer/addon.tsx index 66bdd44f6..1341fbb35 100644 --- a/packages/renderer-core/src/renderer/addon.tsx +++ b/packages/renderer-core/src/renderer/addon.tsx @@ -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 }; diff --git a/packages/renderer-core/src/renderer/base.tsx b/packages/renderer-core/src/renderer/base.tsx index 286e0da43..d3e2eb18c 100644 --- a/packages/renderer-core/src/renderer/base.tsx +++ b/packages/renderer-core/src/renderer/base.tsx @@ -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() { diff --git a/packages/renderer-core/src/renderer/renderer.tsx b/packages/renderer-core/src/renderer/renderer.tsx index 17738beff..f3111176b 100644 --- a/packages/renderer-core/src/renderer/renderer.tsx +++ b/packages/renderer-core/src/renderer/renderer.tsx @@ -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}`); } diff --git a/packages/renderer-core/src/types/index.ts b/packages/renderer-core/src/types/index.ts index e85602c7b..bd0bbedbe 100644 --- a/packages/renderer-core/src/types/index.ts +++ b/packages/renderer-core/src/types/index.ts @@ -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 */ diff --git a/packages/renderer-core/src/utils/common.ts b/packages/renderer-core/src/utils/common.ts index b2c72de47..52642c4d4 100644 --- a/packages/renderer-core/src/utils/common.ts +++ b/packages/renderer-core/src/utils/common.ts @@ -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-CN、en-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 = {}) { - 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) { } } +/** + * 对象类型JSExpression,支持省略this + * @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)}`); + 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 rst.join('&'); + return result.join('&'); } \ No newline at end of file diff --git a/packages/renderer-core/test/renderer/__snapshots__/base.test.tsx.snap b/packages/renderer-core/test/renderer/__snapshots__/base.test.tsx.snap deleted file mode 100644 index 565c0ce4e..000000000 --- a/packages/renderer-core/test/renderer/__snapshots__/base.test.tsx.snap +++ /dev/null @@ -1,280 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`loop schema loop key 1`] = ` -
-
- Component Not Found -
-
-
-
- Component Not Found -
-
- Component Not Found -
-
- Component Not Found -
-
-
-
- Component Not Found -
-
- Component Not Found -
-
- Component Not Found -
-
-
-
- Component Not Found -
-
- Component Not Found -
-
- Component Not Found -
-
-
-
- Component Not Found -
-
-`; - -exports[`notFountComponent not found snapshot 1`] = ` -
-
- Component Not Found -
-
-
- Component Not Found -
-
-
- Component Not Found -
-
-`; diff --git a/packages/renderer-core/test/renderer/base.test.tsx b/packages/renderer-core/test/renderer/base.test.tsx deleted file mode 100644 index 4a22d1f83..000000000 --- a/packages/renderer-core/test/renderer/base.test.tsx +++ /dev/null @@ -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 - , - ); - - 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 - , - ); - - let tree = component.toJSON(); - expect(tree).toMatchSnapshot(); - }) -}) \ No newline at end of file diff --git a/packages/renderer-core/tests/fixtures/schema/basic.ts b/packages/renderer-core/tests/fixtures/schema/basic.ts new file mode 100644 index 000000000..cc587163a --- /dev/null +++ b/packages/renderer-core/tests/fixtures/schema/basic.ts @@ -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', + }, + }, + ], +}; diff --git a/packages/renderer-core/tests/fixtures/unhandled-rejection.ts b/packages/renderer-core/tests/fixtures/unhandled-rejection.ts new file mode 100644 index 000000000..d2ab20b0b --- /dev/null +++ b/packages/renderer-core/tests/fixtures/unhandled-rejection.ts @@ -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'; +} diff --git a/packages/renderer-core/test/hoc/__snapshots__/leaf.test.tsx.snap b/packages/renderer-core/tests/hoc/__snapshots__/leaf.test.tsx.snap similarity index 100% rename from packages/renderer-core/test/hoc/__snapshots__/leaf.test.tsx.snap rename to packages/renderer-core/tests/hoc/__snapshots__/leaf.test.tsx.snap diff --git a/packages/renderer-core/test/hoc/leaf.test.tsx b/packages/renderer-core/tests/hoc/leaf.test.tsx similarity index 100% rename from packages/renderer-core/test/hoc/leaf.test.tsx rename to packages/renderer-core/tests/hoc/leaf.test.tsx diff --git a/packages/renderer-core/test/mock/loop.ts b/packages/renderer-core/tests/mock/loop.ts similarity index 100% rename from packages/renderer-core/test/mock/loop.ts rename to packages/renderer-core/tests/mock/loop.ts diff --git a/packages/renderer-core/test/mock/sample.ts b/packages/renderer-core/tests/mock/sample.ts similarity index 100% rename from packages/renderer-core/test/mock/sample.ts rename to packages/renderer-core/tests/mock/sample.ts diff --git a/packages/renderer-core/test/mock/styleMock.js b/packages/renderer-core/tests/mock/styleMock.js similarity index 100% rename from packages/renderer-core/test/mock/styleMock.js rename to packages/renderer-core/tests/mock/styleMock.js diff --git a/packages/renderer-core/tests/renderer/__snapshots__/renderer.test.tsx.snap b/packages/renderer-core/tests/renderer/__snapshots__/renderer.test.tsx.snap new file mode 100644 index 000000000..2b96228ad --- /dev/null +++ b/packages/renderer-core/tests/renderer/__snapshots__/renderer.test.tsx.snap @@ -0,0 +1,1084 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Base Render renderComp 1`] = ` +
+
+
+ +
+
+
+
+
+ +
+
+ + + + + + + + 请选择 + + +   + + + + + + + + + + + + + + + + +
+
+
+
+ +
+
+ + + + + + + + 请选择 + + +   + + + + + + + + + + + + + + + + +
+
+
+
+ +
+
+ + + + + +
+
+
+ + +
+ +
+
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + +
+
+ 姓名 +
+
+
+ 年龄 +
+
+
+ 邮箱 +
+
+
+ 没有数据 +
+
+
+
+
+
+ +
+ + + + + + +
+ + + + 1 + + / + 10 + + + 到第 + + + + + + 页 + + +
+
+
+
+
+ + + + + + + + 请选择 + + +   + + + + + + + + + + + + + + +
+`; + +exports[`JSExpression JSExpression props 1`] = ` +
+
+
+`; + +exports[`JSExpression JSExpression props with loop 1`] = ` +
+
+
+
+`; + +exports[`JSExpression JSFunction props 1`] = ` +
+
+
+`; + +exports[`JSExpression JSSlot has loop 1`] = ` +
+
+
+ 这是一个低代码业务组件~ +
+
+
+
+ 这是一个低代码业务组件~ +
+
+
+
+ 这是一个低代码业务组件~ +
+
+
+`; + +exports[`JSExpression base props 1`] = ` +
+
+
+`; diff --git a/packages/renderer-core/tests/renderer/renderer.test.tsx b/packages/renderer-core/tests/renderer/renderer.test.tsx new file mode 100644 index 000000000..d1917c5a5 --- /dev/null +++ b/packages/renderer-core/tests/renderer/renderer.test.tsx @@ -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 + ); + + 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 + ); + 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(); + }); + }) +}) \ No newline at end of file diff --git a/packages/renderer-core/tests/setup.ts b/packages/renderer-core/tests/setup.ts new file mode 100644 index 000000000..a1b5e7328 --- /dev/null +++ b/packages/renderer-core/tests/setup.ts @@ -0,0 +1,12 @@ +jest.mock('zen-logger', () => { + class Logger { + log() {} + error() {} + warn() {} + debug() {} + } + return { + __esModule: true, + default: Logger, + }; +}); \ No newline at end of file diff --git a/packages/renderer-core/tests/utils/common.test.ts b/packages/renderer-core/tests/utils/common.test.ts new file mode 100644 index 000000000..a67842ed6 --- /dev/null +++ b/packages/renderer-core/tests/utils/common.test.ts @@ -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(); + + }); +}); \ No newline at end of file diff --git a/packages/renderer-core/test/utils/components.tsx b/packages/renderer-core/tests/utils/components.tsx similarity index 100% rename from packages/renderer-core/test/utils/components.tsx rename to packages/renderer-core/tests/utils/components.tsx diff --git a/packages/renderer-core/test/utils/is-use-loop.test.ts b/packages/renderer-core/tests/utils/is-use-loop.test.ts similarity index 100% rename from packages/renderer-core/test/utils/is-use-loop.test.ts rename to packages/renderer-core/tests/utils/is-use-loop.test.ts diff --git a/packages/renderer-core/test/utils/node.ts b/packages/renderer-core/tests/utils/node.ts similarity index 100% rename from packages/renderer-core/test/utils/node.ts rename to packages/renderer-core/tests/utils/node.ts diff --git a/packages/renderer-core/test/utils/react-env-init.ts b/packages/renderer-core/tests/utils/react-env-init.ts similarity index 100% rename from packages/renderer-core/test/utils/react-env-init.ts rename to packages/renderer-core/tests/utils/react-env-init.ts