From a066ba8625b7de179ba6f62fd26cb3d10a0d9b2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AF=9B=E7=91=9E?= Date: Fri, 27 Mar 2020 21:04:01 +0800 Subject: [PATCH] =?UTF-8?q?Feat:=20=E5=B7=A5=E5=85=B7=E5=87=BD=E6=95=B0?= =?UTF-8?q?=E7=AD=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/logo.png | Bin 0 -> 1271 bytes src/components/ChooserAsync.tsx | 7 +- src/components/ChooserAsyncFunctional.tsx | 7 +- src/components/File.tsx | 94 ++++++---- src/components/Image.tsx | 67 +++++-- src/components/Loading.vue | 21 ++- src/pages/other/scss/main.scss | 1 + src/utils/clone.ts | 30 ++-- src/utils/getKey.ts | 38 +++- src/utils/index.ts | 208 ++++++++++++++++++++++ tests/unit/utils/index.spec.ts | 120 +++++++++++++ 11 files changed, 516 insertions(+), 77 deletions(-) create mode 100644 src/assets/logo.png create mode 100644 src/utils/index.ts create mode 100644 tests/unit/utils/index.spec.ts diff --git a/src/assets/logo.png b/src/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..46ca04dee251a4fa85a2891a145fbe20cc619d96 GIT binary patch literal 1271 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+0817m!EPlzi}fpbWjb7-1N zREF=ab|~82?p|H&9FPi<3Q0p2_nKbg9F`6d2a)0F5LviN5F-?-1uh6wgGU@;KHLFx zWcX}ub<4|h4hH*lce~e|TIa|N-yLo4RYl&*8eQTtJ=)5A);GJR=Xg%80{Y!&YpYvf zzSsOZP>Ahpcdsq>UfJl9kmb=;?z6GQH8a<1TD9-CHn-w}|NsA+Nb6JrgE+J#$S)X3 zGcfS;fdK35)2Be-Oetf?`zOY13)%G^e)6sPw@*;|%KXdcU#*P1v1-S;t21mOG>nAE-eH;@V%$t;WjcxYXwEUbR z3z>7z#DtTVO-oacoh9{_MQY8Ot-i}F{j^uD+E(t7w)x6MKX=vIp4w>b*IOPH6jixJ zZ#|uAv~yR1m9_1`d$&$jY?ogCnOnuicG8u{jt?HmM3~l)E(#;^5{P>Y|zRB0* zEz%!bA15~jCmrhl{dVr6;-~M#%Kx{>DI^zpsl1afdH67nWtqCYg=$*b#>z9DEt9H` z|+MWPFs%ZPNO+J zf0-lgZs?zWIq~q~#m;eY33n#>L}?XxEeV>+^y8e1Yo5XT(EXF-y$iEBhj zN@7W>RdP`(kYX@0Ff`XSFw`})3^6dbGBmU@HPtpSv@$SAK61eeMMG|WN@iLmZVf^+ zGrj>egja<`lmsP~D-;yvr)B1(DwI?fq$*?3oE!Zm>f=FR^A+ @@ -96,7 +97,7 @@ export default class extends Vue { }) .catch(err => { this.is = - (typeof this.error === 'function' ? this.error(err) : this.error) || + (isFn(this.error) ? (this.error as any)(err) : this.error) || status.error }) } diff --git a/src/components/ChooserAsyncFunctional.tsx b/src/components/ChooserAsyncFunctional.tsx index b83b3e6..31d187c 100644 --- a/src/components/ChooserAsyncFunctional.tsx +++ b/src/components/ChooserAsyncFunctional.tsx @@ -11,6 +11,8 @@ import Vue, { Component, RenderContext, VNode } from 'vue' import Info from './Info' import Loading from './Loading' +import { hasOwnProperty, isFn } from '@/utils' + /// 常量(UPPER_CASE),单例/变量(camelCase),函数(无副作用,camelCase) /// // const ModuleOne: any = getAsync(() => // import(/* webpackChunkName: "ihOne" */ './ModuleOne') @@ -111,7 +113,7 @@ function get(state: state) { }) .catch(err => { state.i.i = - (typeof state.error === 'function' ? state.error(err) : state.error) || + (isFn(state.error) ? (state.error as any)(err) : state.error) || status.error }) } @@ -251,8 +253,7 @@ export default (context: RenderContext) => { const data = context.data const state = getState( temp, - // eslint-disable-next-line no-prototype-builtins - data.hasOwnProperty('key') ? data.key : (data.key = '') + hasOwnProperty(data, 'key') ? data.key : (data.key = '') ) // situations to use VNode cache diff --git a/src/components/File.tsx b/src/components/File.tsx index d9d74ac..977c112 100644 --- a/src/components/File.tsx +++ b/src/components/File.tsx @@ -1,9 +1,10 @@ /* * @Description: 文件下载(链接) + *【请确保(所在入口/当前文件)引用了'~element-ui/packages/theme-chalk/src/link'样式】 * @Author: 毛瑞 * @Date: 2020-03-02 16:46:53 */ -import { CreateElement } from 'vue' +import { CreateElement, VNode } from 'vue' // see: https://github.com/kaorun343/vue-property-decorator import { Component, Vue, Prop, Watch } from 'vue-property-decorator' @@ -12,6 +13,7 @@ import { Component, Vue, Prop, Watch } from 'vue-property-decorator' // import STYLE from './index.module.scss' import ElLink from 'element-ui/lib/link' +import { isEqual } from '@/utils' import { download, free, IFile, save } from '@/utils/downloader' /// 常量(UPPER_CASE),单例/变量(camelCase),函数(无副作用,camelCase) /// @@ -32,82 +34,112 @@ export default class extends Vue { /// [model] (@Model('change') readonly attr!: string) /// /// [props] (@Prop() readonly attr!: string) /// /** 文件下载地址 */ - @Prop() readonly url!: string - /** 显示文字 */ - @Prop() readonly text!: string + @Prop() readonly href!: string /** 查询参数 */ @Prop() readonly query?: IObject - /** 查询参数 */ + /** 显示文字 */ + @Prop() readonly text?: string + /** 禁用 */ + @Prop() readonly disabled?: boolean + /** 类型 */ @Prop({ default: 'primary' }) readonly type?: string + /** 图表 */ + @Prop({ default: 'el-icon-document' }) readonly icon?: string /// [data] (attr: string = '响应式属性' // 除了 undefined) /// private status: status = status.init + private isSleep = false // 是否失活/休眠 /// 非响应式属性 (attr?: string // undefined) /// private $_file?: IFile + private $_vnode?: VNode /// [computed] (get attr() {} set attr(){}) /// /// [LifeCycle] (private beforeCreate(){}/.../destroyed(){}) /// + private activated() { + this.isSleep = false + } + + private deactivated() { + this.isSleep = true + } + private beforeDestroy() { this.$_file && free(this.$_file) } /// [watch] (@Watch('attr') onAttrChange(val, oldVal) {}) /// - /// [methods] (method(){}) /// - @Watch('url') + @Watch('href') @Watch('query') + private reset(value: any, old: any) { + // diff + if (old && isEqual(value, old)) { + return + } + + this.status = status.init + this.$_file && free(this.$_file) + } + + /// [methods] (method(){}) /// private load() { - this.status = status.loading - download(this.url, this.query) - .then(res => { - this.$_file && free(this.$_file) - this.$_file = res - this.status = status.success - this.save() - setTimeout(this.reset.bind(this), ALIVE) - }) - .catch(() => { - this.status = status.error - }) + const href = this.href + if (href) { + this.status = status.loading + download(href, this.query) + .then(res => { + this.$_file && free(this.$_file) + this.$_file = res + this.status = status.success + this.save() + setTimeout(this.reset.bind(this), ALIVE) + }) + .catch(() => { + this.status = status.error + }) + } } private save() { save(this.$_file as IFile) } - private reset() { - this.status = status.init - this.$_file && free(this.$_file) - } - // see: https://github.com/vuejs/jsx#installation // eslint-disable-next-line @typescript-eslint/no-unused-vars private render(h: CreateElement) { + if (this.isSleep) { + return this.$_vnode + } + // 依赖收集 switch (this.status) { case status.loading: - return ( + return (this.$_vnode = ( {this.text} + {this.$slots.default} - ) + )) case status.error: - return ( + return (this.$_vnode = ( {this.text} + {this.$slots.default} - ) + )) default: - return ( + return (this.$_vnode = ( {this.text} + {this.$slots.default} - ) + )) } } } diff --git a/src/components/Image.tsx b/src/components/Image.tsx index 8e10ca5..990716e 100644 --- a/src/components/Image.tsx +++ b/src/components/Image.tsx @@ -3,15 +3,16 @@ * @Author: 毛瑞 * @Date: 2020-03-02 13:25:23 */ -import { CreateElement } from 'vue' +import { CreateElement, VNode } from 'vue' // see: https://github.com/kaorun343/vue-property-decorator -import { Component, Vue, Prop } from 'vue-property-decorator' +import { Component, Vue, Prop, Watch } from 'vue-property-decorator' /// [import] vue组件,其他,CSS Module /// // import { getAsync } from '@/utils/highOrder' // import STYLE from './index.module.scss' import Info from './Info' +import { isEqual } from '@/utils' import { download, free, IFile } from '@/utils/downloader' /// 常量(UPPER_CASE),单例/变量(camelCase),函数(无副作用,camelCase) /// @@ -33,17 +34,24 @@ export default class extends Vue { /// [model] (@Model('change') readonly attr!: string) /// /// [props] (@Prop() readonly attr!: string) /// /** 文件下载地址 */ - @Prop() readonly url!: string + @Prop() readonly src!: string /** 查询参数 */ @Prop() readonly query?: IObject + @Prop() readonly alt?: string /// [data] (attr: string = '响应式属性' // 除了 undefined) /// private status: status = status.init + private isSleep = false // 是否失活/休眠 /// 非响应式属性 (attr?: string // undefined) /// private $_file?: IFile + private $_vnode?: VNode /// [computed] (get attr() {} set attr(){}) /// /// [LifeCycle] (private beforeCreate(){}/.../destroyed(){}) /// - private created() { - this.load() + private activated() { + this.isSleep = false + } + + private deactivated() { + this.isSleep = true } private beforeDestroy() { @@ -52,9 +60,21 @@ export default class extends Vue { /// [watch] (@Watch('attr') onAttrChange(val, oldVal) {}) /// /// [methods] (method(){}) /// - private load() { + @Watch('src', { immediate: true }) + @Watch('query') + private load(value: any, old: any) { + const src = this.src + if (!src) { + return + } + + // diff + if (old && isEqual(value, old)) { + return + } + this.status = status.loading - download(this.url, this.query) + download(src, this.query) .then(res => { this.$_file && free(this.$_file) this.$_file = res @@ -68,26 +88,43 @@ export default class extends Vue { // see: https://github.com/vuejs/jsx#installation // eslint-disable-next-line @typescript-eslint/no-unused-vars private render(h: CreateElement) { + if (this.isSleep) { + return this.$_vnode + } + // 依赖收集 switch (this.status) { case status.init: - return ( + return (this.$_vnode = ( - ) + )) case status.loading: - return ( - - ) + return (this.$_vnode = ( + + )) case status.error: - return + return (this.$_vnode = ( + + )) default: - return + return (this.$_vnode = ( + {this.alt} + )) } } } diff --git a/src/components/Loading.vue b/src/components/Loading.vue index 8c23089..2d7e311 100644 --- a/src/components/Loading.vue +++ b/src/components/Loading.vue @@ -9,15 +9,15 @@ @@ -56,6 +56,8 @@ export default class extends Vue { svg { width: 100px; + background: url(~@/assets/logo.png) center no-repeat; + animation: hue 2.5s infinite; } circle { @@ -67,13 +69,24 @@ export default class extends Vue { } &:last-child { - animation: dash 2.5s linear infinite; + animation: dash 2.5s infinite; stroke: $colorTheme; stroke-linecap: round; } } } -// 转圈动画 +// 渐变 +@keyframes hue { + 0% { + filter: hue-rotate(0); + } + + 100% { + filter: hue-rotate(360deg); + } +} + +// 转圈 @keyframes dash { 0% { stroke-dasharray: 0, 250%; diff --git a/src/pages/other/scss/main.scss b/src/pages/other/scss/main.scss index 8d5f0b4..0b573da 100644 --- a/src/pages/other/scss/main.scss +++ b/src/pages/other/scss/main.scss @@ -128,6 +128,7 @@ margin-top: $heightHeader; } // 鼠标提示&弹出框 +.el-popper, .el-popover, .el-tooltip__popper { z-index: $zTop !important; diff --git a/src/utils/clone.ts b/src/utils/clone.ts index feb3d1c..7337b98 100644 --- a/src/utils/clone.ts +++ b/src/utils/clone.ts @@ -3,6 +3,8 @@ * @Author: 毛瑞 * @Date: 2019-06-27 12:58:37 */ +import { hasOwnProperty, isObj, isArray, isFn } from '@/utils' + /** 克隆过滤函数返回值 */ interface IClone { @@ -57,12 +59,12 @@ function extend( tmp = filter && filter(key, targetValue, currentValue, source, target, deep) if (tmp) { tmp.jump || (source[key] = tmp.value) // 自定义拷贝 - } else if (!targetValue || typeof targetValue !== 'object') { + } else if (!targetValue || !isObj(targetValue)) { source[key] = targetValue // 拷贝值 } else { // 当前类型应与目标相同(数组/对象) - tmp = Array.isArray(targetValue) // 目标是否数组 - Array.isArray(currentValue) === tmp || (currentValue = 0) // 类型不同 + tmp = isArray(targetValue) // 目标是否数组 + isArray(currentValue) === tmp || (currentValue = 0) // 类型不同 source[key] = extend( currentValue || (tmp ? [] : {}), @@ -85,7 +87,7 @@ function extend( */ function clone(...args: any[]): any { let filter: Filter | undefined - typeof args[0] === 'function' && (filter = args.shift()) + isFn(args[0]) && (filter = args.shift()) let argsLength = args.length @@ -97,7 +99,7 @@ function clone(...args: any[]): any { let index = 0 while (index < argsLength) { tmp = args[index] - if (typeof tmp === 'object') { + if (isObj(tmp)) { if (!current) { current = tmp } else if (!target) { @@ -116,7 +118,7 @@ function clone(...args: any[]): any { if (argsLength === 1) { // 一个参数时克隆当前对象 target = current - current = Array.isArray(target) ? [] : {} + current = isArray(target) ? [] : {} } current && target && extend(current, target, filter, 0) @@ -137,23 +139,17 @@ function clone(...args: any[]): any { * @returns {Object|Array} 原target对象 */ function setDefault(target: IObject | any[], defaultObject: IObject | any[]) { - const TYPE = 'object' - // if (typeof target !== TYPE || typeof defaultObject !== TYPE) { - // return target - // } let key let temp for (key in defaultObject) { - // eslint-disable-next-line no-prototype-builtins - if (target.hasOwnProperty(key)) { - typeof (temp = target[key]) === TYPE && - typeof (key = defaultObject[key]) === TYPE && + if (hasOwnProperty(target, key)) { + isObj((temp = target[key])) && + isObj((key = defaultObject[key])) && // a ^ b 同真(1)同假(0)返回0 - Array.isArray(temp) === Array.isArray(key) && + isArray(temp) === isArray(key) && setDefault(temp, key) } else { - target[key] = - typeof (temp = defaultObject[key]) === TYPE ? clone(temp) : temp + target[key] = isObj((temp = defaultObject[key])) ? clone(temp) : temp } } diff --git a/src/utils/getKey.ts b/src/utils/getKey.ts index 89bc66f..fc30146 100644 --- a/src/utils/getKey.ts +++ b/src/utils/getKey.ts @@ -1,12 +1,42 @@ -/** 计数器缓存 */ -const CACHE: IObject = {} +const CACHE_KEY: IObject = {} /** 获取唯一key * @param {string} name key标识(命名空间) */ function getKey(name?: string): string { return ( - (name || (name = '')) + (CACHE[name] ? ++CACHE[name] : (CACHE[name] = 1)) + (name || (name = '')) + + (CACHE_KEY[name] ? ++CACHE_KEY[name] : (CACHE_KEY[name] = 1)) ) } -export default getKey +const CACHE_UUID: IObject = {} +/** 获取客户端唯一标识(默认长度16) + * @param key uuid标识 + * @param len uuid长度 + * @param refresh 是否更新key对应的uuid + */ +function getUuid( + key?: string | number, + len?: number, + refresh?: boolean | string +) { + if (!refresh && (refresh = CACHE_UUID[key as string])) { + return refresh + } + + refresh = Date.now().toString(36) // 时间戳(长度8) + if ((len = (len || 16) - refresh.length) > 0) { + const CHAR = + 'qwertyuioplkjhgfdsazxcvbnm7896541230MNBVCXZLKJHGFDSAPOIUYTREWQ' + const cLen = CHAR.length + while (len--) { + refresh += CHAR[(Math.random() * cLen) | 0] + } + } else if (len < 0) { + refresh = refresh.substring(0, refresh.length + len) + } + + return (CACHE_UUID[key as string] = refresh) +} + +export { getKey as default, getUuid } diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..2b845d8 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,208 @@ +/** 工具函数 */ + +/** 目标自身是否存在指定属性 (查找原型链请用 key in obj 判断) + * @test true + * + * @param obj 目标对象 + * @param key 目标键 + * + * @returns Boolean + */ +function hasOwnProperty(obj: any, key?: any) { + return Object.prototype.hasOwnProperty.call(obj, key) +} + +/** 获取目标值类型 + * @test true + * + * @param value 目标值 + * + * @returns String + */ +function getType(value?: any): string { + value = Object.prototype.toString.call(value) + return value.substring(8, value.length - 1).toLowerCase() +} + +/** 目标值是否数字 + * @test true + * + * @param value 目标值 + * @param str 是否包括字符串 默认true + * @param nan 是否包括NaN 默认false + * + * @returns Boolean + */ +function isNumber(value?: any, str?: boolean, nan?: boolean) { + str !== false && typeof value === 'string' && (value = parseFloat(value)) + return nan + ? typeof value === 'number' + : !isNaN(value) && typeof value === 'number' +} + +/** 目标是否未定义 + * @test true + * + * @param value 目标值 + * + * @returns Boolean + */ +function isUndef(value?: any) { + return value === undefined +} + +/** 目标是否是null + * @test true + * + * @param value 目标值 + * + * @returns Boolean + */ +function isNull(value?: any) { + return value === null +} + +/** 目标是否是null或undefined + * @test true + * + * @param value 目标值 + * + * @returns Boolean + */ +function isNullish(value?: any) { + return isUndef(value) || isNull(value) +} + +/** 目标是否是布尔值 + * @test true + * + * @param value 目标值 + * + * @returns Boolean + */ +function isBool(value?: any) { + return typeof value === 'boolean' +} + +/** 目标是否是字符串 + * @test true + * + * @param value 目标值 + * + * @returns Boolean + */ +function isString(value?: any) { + return typeof value === 'string' +} + +/** 目标是否是对象/数组 + * @test true + * + * @param value 目标值 + * + * @returns Boolean + */ +function isObj(value?: any) { + return typeof value === 'object' +} + +/** 目标是否是数组 + * @test true + * + * @param value 目标值 + * + * @returns Boolean + */ +function isArray(value?: any) { + return getType(value) === 'array' // Array.isArray(value) +} + +/** 目标是否是对象 + * @test true + * + * @param value 目标值 + * + * @returns Boolean + */ +function isObject(value?: any) { + return isObj(value) && !isArray(value) +} + +/** 目标是否是函数 + * @test true + * + * @param value 目标值 + * + * @returns Boolean + */ +function isFn(value?: any) { + return typeof value === 'function' +} + +/** 比较两个值是否相等 (对象和数组比较值) + * @test true + * + * @param value 当前值 + * @param target 目标值 + * + * @returns Boolean + */ +function isEqual(value?: any, target?: any): boolean { + let type + if ((type = getType(value)) !== getType(target)) { + return false + } + + if (type === 'array') { + if ((type = value.length) !== target.length) { + return false + } + + while (type--) { + if (!isEqual(value[type], target[type])) { + return false + } + } + + return true + } + + if (type === 'object') { + // { a: undefined } {} 视为相同 + const KEYS: IObject<1> = {} // for 性能 + for (type in value) { + KEYS[type] = 1 + if (!isEqual(value[type], target[type])) { + return false + } + } + for (type in target) { + if (!KEYS[type]) { + KEYS[type] = 1 + if (!isEqual(value[type], target[type])) { + return false + } + } + } + + return true + } + + return isNaN(value) ? isNaN(target) : value === target +} + +export { + hasOwnProperty, + getType, + isNumber, + isUndef, + isNull, + isNullish, + isBool, + isString, + isObj, + isArray, + isObject, + isFn, + isEqual, +} diff --git a/tests/unit/utils/index.spec.ts b/tests/unit/utils/index.spec.ts new file mode 100644 index 0000000..86e44f0 --- /dev/null +++ b/tests/unit/utils/index.spec.ts @@ -0,0 +1,120 @@ +/** 工具函数测试 + */ +import { + hasOwnProperty, + getType, + isNumber, + isUndef, + isNull, + isNullish, + isBool, + isString, + isObj, + isArray, + isObject, + isFn, + isEqual, +} from '@/utils' + +describe('@/utils: 工具函数', () => { + it('hasOwnProperty: 自身是否包含指定属性', () => { + expect(hasOwnProperty(0)).toBe(false) + const test = { test: undefined } + expect(hasOwnProperty(test)).toBe(false) + expect(hasOwnProperty(test, 'test')).toBe(true) + expect(hasOwnProperty(test, 'hasOwnProperty')).toBe(false) + }) + + it('getType: 获取目标值类型', () => { + expect(getType(NaN)).toBe('number') + expect(getType(-1)).toBe('number') + expect(getType()).toBe('undefined') + expect(getType(false)).toBe('boolean') + expect(getType('')).toBe('string') + expect(getType(null)).toBe('null') + expect(getType(Symbol('test'))).toBe('symbol') + expect(getType(1n)).toBe('bigint') + expect(getType({})).toBe('object') + expect(getType([])).toBe('array') + expect(getType(Object)).toBe('function') + expect(getType(() => {})).toBe('function') + }) + + it('isNumber: 是否数字', () => { + expect(isNumber(0)).toBe(true) + expect(isNumber('')).toBe(false) + expect(isNumber('0')).toBe(true) + expect(isNumber('0', false)).toBe(false) + expect(isNumber('a', true, true)).toBe(true) + expect(isNumber(NaN, false, true)).toBe(true) + }) + + it('isUndef: 是否undefined', () => { + expect(isUndef()).toBe(true) + expect(isUndef(null)).toBe(false) + }) + + it('isNull: 是否null', () => { + expect(isNull()).toBe(false) + expect(isNull(null)).toBe(true) + }) + + it('isNullish: 是否null/undefined', () => { + expect(isNullish()).toBe(true) + expect(isNullish(null)).toBe(true) + expect(isNullish(0)).toBe(false) + }) + + it('isBool: 是否布尔值', () => { + expect(isBool()).toBe(false) + expect(isBool(true)).toBe(true) + expect(isBool(false)).toBe(true) + }) + + it('isString: 是否字符串', () => { + expect(isString()).toBe(false) + expect(isString('')).toBe(true) + }) + + it('isObj: 是否数组/对象', () => { + expect(isObj()).toBe(false) + expect(isObj([])).toBe(true) + expect(isObj({})).toBe(true) + }) + + it('isArray: 是否数组', () => { + expect(isArray()).toBe(false) + expect(isArray([])).toBe(true) + expect(isArray({})).toBe(false) + }) + + it('isObject: 是否对象', () => { + expect(isObject()).toBe(false) + expect(isObject([])).toBe(false) + expect(isObject({})).toBe(true) + }) + + it('isFn: 是否函数', () => { + expect(isFn()).toBe(false) + expect(isFn([])).toBe(false) + expect(isFn(Object)).toBe(true) + }) + + it('isEqual: 两个值是否相等', () => { + expect(isEqual()).toBe(true) + expect(isEqual(0)).toBe(false) + expect(isEqual(0, '')).toBe(false) + expect(isEqual(NaN, NaN)).toBe(true) + expect(isEqual(Object, Object)).toBe(true) + expect(isEqual({}, {})).toBe(true) + expect(isEqual({ a: 0 }, { a: '0' })).toBe(false) + expect(isEqual({ test: undefined, a: 0 }, { a: 0 })).toBe(true) + expect(isEqual({ a: 0, b: { c: [0] } }, { a: 0, b: { c: 0 } })).toBe(false) + expect(isEqual({ a: 0, b: { c: [0] } }, { a: 0, b: { c: [0] } })).toBe(true) + expect(isEqual([], [])).toBe(true) + expect(isEqual([0, 1, 2, 3], [0, 1, 2, 3, 4, 5, 6])).toBe(false) + expect( + isEqual([0, { a: { b: '' } }, NaN], [0, { a: { b: '' } }, NaN]) + ).toBe(true) + }) +})