/* eslint-disable no-console */ import { tmpl, domBackend } from '../base/env' import * as glassEasel from '../../src' import { Context } from '../base/composed_backend' import type { Element } from '../base/composed_backend' const componentSpace = new glassEasel.ComponentSpace() componentSpace.updateComponentOptions({ writeFieldsToNode: true, writeIdToDOM: true, }) componentSpace.defineComponent({ is: '', }) describe('dump element', () => { test('dump node structure (shadow)', () => { const compDef = componentSpace.defineComponent({ template: tmpl('
A
'), }) const elem = glassEasel.Component.createWithContext('root', compDef.general(), domBackend) const shadowDump = glassEasel.dumpElementToString(elem.shadowRoot, false) expect(shadowDump).toBe( [ '<(virtual):shadow>', ' <(virtual):wx:if>', '
', ' A', ].join('\n'), ) }) test('dump node structure (composed)', () => { const compDefA = componentSpace.defineComponent({ is: 'comp-a', options: { multipleSlots: true }, properties: { prop: String, }, template: tmpl(' '), }) componentSpace.setGlobalUsingComponent('comp-a', compDefA.general()) const compDef = componentSpace.defineComponent({ is: 'comp-b', template: tmpl('
A
'), }) const elem = glassEasel.Component.createWithContext('root', compDef.general(), domBackend) const composedDump = glassEasel.dumpElementToString(elem, true) expect(composedDump).toBe( [ '', ' <(virtual):shadow>', ' ', ' <(virtual):shadow>', ' <(virtual):wx:if>', ' <(virtual):slot (slot) name="a">', '
', ' A', ].join('\n'), ) }) test('dump node structure (external component)', () => { const def = componentSpace.defineComponent({ options: { externalComponent: true }, properties: { prop: String, }, template: tmpl('
'), }) const elem = glassEasel.Component.createWithContext('root', def.general(), domBackend) const composedDump = glassEasel.dumpElementToString(elem, true) expect(composedDump).toBe(['', ' <(external)>'].join('\n')) }) test('dump invalid node', () => { expect(glassEasel.dumpElementToString(null, false)).toBe('<(null)>') expect(glassEasel.dumpElementToString(undefined, false)).toBe('<(undefined)>') expect(glassEasel.dumpElementToString(NaN, false)).toBe('<(unknown)>') }) }) describe('deep copy', () => { test('simple deep copy', () => { const data = { a: [1, 'a'], b: { b1: true, }, s: Symbol('syn'), } const copied = glassEasel.dataUtils.deepCopy(data, false) expect(copied.a).toStrictEqual(data.a) expect(copied.a !== data.a).toBe(true) expect(copied.b).toStrictEqual(data.b) expect(copied.b !== data.b).toBe(true) expect(copied.s.description).toBe(data.s.description) expect(copied.s !== data.s).toBe(true) }) test('deep copy with object recursion', () => { const data = { a: { b: null, c: 123, }, } data.a.b = data.a as unknown as null const copied = glassEasel.dataUtils.deepCopy(data, true) expect(copied.a.c).toBe(data.a.c) expect(copied.a !== data.a).toBe(true) expect(copied.a.b !== data.a.b).toBe(true) expect(copied.a.b).toBe(copied.a) }) test('deep copy with array recursion', () => { const data = { a: [null, Symbol('syn')], } data.a[0] = data.a as unknown as null const copied = glassEasel.dataUtils.deepCopy(data, true) expect(copied.a[1]!.description).toBe(data.a[1]!.description) expect(copied.a !== data.a).toBe(true) expect(copied.a[0] !== data.a[0]).toBe(true) expect(copied.a[0]).toBe(copied.a) }) }) describe('event', () => { test('read event status', () => { const event = new glassEasel.Event('test', {}) expect(event.defaultPrevented()).toBe(false) expect(event.getEventBubbleStatus()).toBe(glassEasel.EventBubbleStatus.Normal) event.preventDefault() expect(event.defaultPrevented()).toBe(true) expect(event.getEventBubbleStatus()).toBe(glassEasel.EventBubbleStatus.NoDefault) }) test('legacy event binding syntax', () => { const eventArr: number[] = [] const childCompDef = glassEasel.Component.register( { lifetimes: { attached() { this.triggerEvent('abc', 123, { capturePhase: true }) this.triggerEvent('abc', 456) }, }, }, componentSpace, ) const compDef = glassEasel.Component.register( { using: { child: childCompDef, }, template: tmpl(` `), methods: { handler(ev: glassEasel.ShadowedEvent) { eventArr.push(ev.detail) }, }, }, componentSpace, ) const comp = glassEasel.Component.createWithContext('root', compDef, domBackend) glassEasel.Element.pretendAttached(comp) expect(eventArr).toEqual([123, 123, 456, 456, 456]) }) test('listener change lifetimes', () => { const eventArr: [boolean, string, any][] = [] const childCompDef = glassEasel.Component.register( { options: { listenerChangeLifetimes: true, }, lifetimes: { listenerChange: ( isAdd: boolean, eventName: string, listener: any, options: EventListenerOptions, ) => { eventArr.push([isAdd, eventName, listener]) expect(options.capture).toBe(true) }, }, }, componentSpace, ) const compDef = glassEasel.Component.register( { using: { child: childCompDef, }, template: tmpl(` `), }, componentSpace, ) const comp = glassEasel.Component.createWithContext('root', compDef, domBackend) const child = comp.getShadowRoot()!.getElementById('a')!.asInstanceOf(childCompDef)! const listener = () => { /* empty */ } child.addListener('testEv', listener, { capture: true }) child.removeListener('testEv', listener, { capture: true }) child.removeListener('testEv', listener, { capture: true }) child.removeListener('testEv2', listener, { capture: true }) expect(eventArr).toEqual([ [true, 'testEv', listener], [false, 'testEv', listener], ]) }) test('error lifetimes stack overflow', () => { const oldConsoleError = console.error console.error = function () {} glassEasel.globalOptions.throwGlobalError = false let errorCalled = 0 let errorObj: Error | null = null const compDef = glassEasel.Component.register({ is: 'component-error-lifetimes-a', template: tmpl('
'), lifetimes: { created() { throw new Error('test') }, error(e: Error) { errorCalled += 1 errorObj = e throw new Error('error in error') }, }, }) glassEasel.createElement('root', compDef) glassEasel.globalOptions.throwGlobalError = true console.error = oldConsoleError expect(errorCalled).toBe(1) expect(errorObj!.message).toBe('test') }) }) describe('component utils', () => { test('#getMethodsFromDef #getMethod #callMethod', () => { const compDef = glassEasel.Component.register( { methods: { abc() { return 'abc' }, }, }, componentSpace, ) expect(compDef.isPrepared()).toBe(false) compDef.prepare() expect(compDef.isPrepared()).toBe(true) expect(glassEasel.Component.getMethodsFromDef(compDef.general()).abc!()).toBe('abc') const comp = glassEasel.Component.createWithContext('root', compDef.general(), domBackend) expect(glassEasel.Component.getMethod(comp.general(), 'abc')!()).toBe('abc') expect(comp.callMethod('abc')).toBe('abc') }) test('#getMethodsFromDef #getMethod #callMethod (in init function)', () => { const compDef = componentSpace .define() .init(({ method }) => { const abc = method(() => 'abc') return { abc, } }) .registerComponent() expect(compDef.isPrepared()).toBe(false) compDef.prepare() expect(compDef.isPrepared()).toBe(true) expect(glassEasel.Component.getMethodsFromDef(compDef.general()).abc).toBe(undefined) const comp = glassEasel.Component.createWithContext('root', compDef.general(), domBackend) expect(glassEasel.Component.getMethod(comp.general(), 'abc')!()).toBe('abc') expect(comp.callMethod('abc')).toBe('abc') }) test('#getMethodsFromDef #getMethod #callMethod (when `useMethodCallerListeners` is set)', () => { const compDef = componentSpace .define() .options({ useMethodCallerListeners: true, }) .init(({ method }) => { const def = method(() => 'def') return { def } }) .methods({ ghi() { return 'ghi' }, }) .registerComponent() const comp = glassEasel.Component.createWithContext('root', compDef.general(), domBackend) const caller = { abc() { return 'abc' }, } comp.setMethodCaller(caller as any) expect(glassEasel.Component.getMethodsFromDef(compDef.general()).abc).toBeUndefined() expect(glassEasel.Component.getMethod(comp.general(), 'abc')!()).toBe('abc') expect(comp.callMethod('abc')).toBe('abc') expect(glassEasel.Component.getMethodsFromDef(compDef.general()).def).toBeUndefined() expect(glassEasel.Component.getMethod(comp.general(), 'def')!()).toBe('def') expect(comp.callMethod('def')).toBe('def') expect(glassEasel.Component.getMethodsFromDef(compDef.general()).ghi).toBeTruthy() expect(glassEasel.Component.getMethod(comp.general(), 'ghi')!()).toBe('ghi') expect(comp.callMethod('ghi')).toBe('ghi') }) test('#isInnerDataExcluded', () => { const compDef = glassEasel.Component.register( { options: { pureDataPattern: /^_/, }, }, componentSpace, ) const comp = glassEasel.Component.createWithContext('root', compDef.general(), domBackend) expect(comp.isInnerDataExcluded('_a')).toBe(true) expect(comp.isInnerDataExcluded('a')).toBe(false) }) test('#getInnerData', () => { const compDef = glassEasel.Component.register( { options: { dataDeepCopy: glassEasel.DeepCopyKind.Simple, }, data: { a: 123, }, }, componentSpace, ) const comp = glassEasel.Component.createWithContext('root', compDef.general(), domBackend) expect(glassEasel.Component.getInnerData(comp.general())).toStrictEqual(comp.data) }) test('#getInnerData', () => { const compDef = glassEasel.Component.register( { options: { dataDeepCopy: glassEasel.DeepCopyKind.Simple, }, data: { a: 123, }, }, componentSpace, ) const comp = glassEasel.Component.createWithContext('root', compDef.general(), domBackend) const oldData = comp.data glassEasel.Component.replaceWholeData(comp.general(), { b: 456 }) expect(oldData).toStrictEqual({ a: 123 }) expect(comp.data).toStrictEqual({ b: 456 }) }) test('getComponentDependencies', () => { const def0 = componentSpace.defineComponent({ is: 'common/def0', using: { p: '/common/def1' } }) const def1 = componentSpace.defineComponent({ is: 'common/def1', using: { p: '/common/def0' } }) const def2 = componentSpace.defineComponent({ is: 'common/def2', using: { p: './none' }, placeholders: { p: './def1' }, }) const def3 = componentSpace.defineComponent({ is: 'def3', using: { p: 'common/def1' } }) const def4 = componentSpace.defineComponent({ is: 'def4', using: { 'p-a': '/common/def2', 'p-b': def3 }, }) def3.prepare() const ret = def4.getComponentDependencies() expect(ret).toContain(def0) expect(ret).toContain(def1) expect(ret).toContain(def2) expect(ret).toContain(def3) expect(ret.size).toBe(4) }) test('fallback event', () => { const events: number[] = [] const child = componentSpace.defineComponent({}) const compDef = componentSpace.defineComponent({ using: { child, }, template: tmpl( ` `, ), methods: { onTap1() { events.push(1) }, onTap2() { events.push(2) }, onTap3() { events.push(3) }, }, }) const elem = glassEasel.createElement('root', compDef.general()) expect(elem.$.child as glassEasel.Element).toBeInstanceOf(glassEasel.Element) expect(elem.$.child as glassEasel.Element).toBeInstanceOf(glassEasel.Component) glassEasel.triggerEvent(elem.$.child as glassEasel.Element, 'tap', {}) expect(events).toStrictEqual([2, 1, 3]) }) test('fallback event on NativeNode', () => { const events: number[] = [] const compDef = componentSpace.defineComponent({ template: tmpl( `
`, { fallbackListenerOnNativeNode: true }, ), methods: { onTap1() { events.push(1) }, onTap2() { events.push(2) }, onTap3() { events.push(3) }, }, }) const elem = glassEasel.createElement('root', compDef.general()) expect(elem.$.div as glassEasel.Element).toBeInstanceOf(glassEasel.Element) expect(elem.$.div as glassEasel.Element).not.toBeInstanceOf(glassEasel.Component) glassEasel.triggerEvent(elem.$.div as glassEasel.Element, 'tap', {}) expect(events).toStrictEqual([2, 1, 3]) }) test('template content update', () => { const compDef = componentSpace .define() .template(tmpl('ABC-{{num}}')) .data(() => ({ num: 123, })) .registerComponent() const elem = glassEasel.Component.createWithContext('root', compDef.general(), domBackend) glassEasel.Element.pretendAttached(elem) const shadowRoot = elem.getShadowRoot()! expect(shadowRoot.childNodes[0]!.asTextNode()!.textContent).toBe('ABC-123') elem.setData({ num: 456 }) expect(shadowRoot.childNodes[0]!.asTextNode()!.textContent).toBe('ABC-456') compDef.updateTemplate(tmpl('DEF-{{num}}')) expect(shadowRoot.childNodes[0]!.asTextNode()!.textContent).toBe('ABC-456') const elem2 = glassEasel.Component.createWithContext('root', compDef.general(), domBackend) const shadowRoot2 = elem2.getShadowRoot()! expect(shadowRoot2.childNodes[0]!.asTextNode()!.textContent).toBe('DEF-123') elem.applyTemplateUpdates() expect(shadowRoot.childNodes[0]!.asTextNode()!.textContent).toBe('DEF-456') elem.setData({ num: 789 }) expect(shadowRoot.childNodes[0]!.asTextNode()!.textContent).toBe('DEF-789') elem2.applyTemplateUpdates() expect(shadowRoot2.childNodes[0]!.asTextNode()!.textContent).toBe('DEF-123') }) test('property dash name conversion', () => { const childComp = componentSpace .define() .property('a1b', String) .template( tmpl(`
{{a1b}}
`), ) .registerComponent() const compDef = componentSpace .define() .usingComponents({ 'child-comp': childComp.general(), }) .template( tmpl(` `), ) .registerComponent() const elem = glassEasel.Component.createWithContext('root', compDef.general(), domBackend) glassEasel.Element.pretendAttached(elem) const shadowRoot = elem.getShadowRoot()! expect(shadowRoot.childNodes[0]!.asInstanceOf(childComp)!.data.a1b).toBe('1') expect(shadowRoot.childNodes[1]!.asInstanceOf(childComp)!.data.a1b).toBe('2') expect(shadowRoot.childNodes[2]!.asInstanceOf(childComp)!.data.a1b).toBe('3') }) test('propertyEarlyInit', () => { const callOrder: number[] = [] const lateInit = componentSpace .define() .options({ propertyEarlyInit: false }) .property('a', Boolean) .template( tmpl(`
{{a}}
`), ) .observer('a', function () { callOrder.push(1) expect(this.getShadowRoot()!.getElementById('a')).toBe(undefined) }) .lifetime('created', function () { callOrder.push(2) expect(this.getShadowRoot()!.getElementById('a')).toBe(undefined) }) .lifetime('attached', function () { callOrder.push(3) expect(this.getShadowRoot()!.getElementById('a')).toBeInstanceOf(glassEasel.NativeNode) }) .registerComponent() const earlyInit = componentSpace .define() .options({ propertyEarlyInit: true }) .property('a', Boolean) .template( tmpl(`
{{a}}
`), ) .observer('a', function () { callOrder.push(4) expect(this.getShadowRoot()!.getElementById('a')).toBe(undefined) }) .lifetime('created', function () { callOrder.push(5) expect(this.getShadowRoot()!.getElementById('a')).toBeInstanceOf(glassEasel.NativeNode) }) .lifetime('attached', function () { callOrder.push(6) expect(this.getShadowRoot()!.getElementById('a')).toBeInstanceOf(glassEasel.NativeNode) }) .registerComponent() const compDef = componentSpace .define() .usingComponents({ 'early-init': earlyInit.general(), 'late-init': lateInit.general(), }) .template( tmpl(` `), ) .registerComponent() const elem = glassEasel.Component.createWithContext('root', compDef.general(), domBackend) glassEasel.Element.pretendAttached(elem) expect(callOrder).toStrictEqual([2, 1, 4, 5, 3, 6]) }) test('hostNodeTagName', () => { const normalComp = componentSpace .define() .options({ externalComponent: false, hostNodeTagName: 'wx-normal' }) .registerComponent() const compDef = componentSpace .define() .usingComponents({ 'normal-comp': normalComp.general(), }) .template( tmpl(` `), ) .registerComponent() const elem = glassEasel.Component.createWithContext('root', compDef.general(), new Context()) glassEasel.Element.pretendAttached(elem) const e = elem.getShadowRoot()!.childNodes[0]!.asElement()!.$$ as Element expect(e._$logicalTagName).toBe('wx-normal') expect(e._$stylingTagName).toBe('normal-comp') }) test('destroy backend element', () => { const compDef = componentSpace.define().registerComponent() const comp = glassEasel.Component.createWithContext('root', compDef.general(), domBackend) const shadowRoot = comp.getShadowRoot()! const t1 = shadowRoot.createTextNode('a') expect(t1.getBackendContext()).toBe(domBackend) expect(t1.getBackendElement()).toBeTruthy() const c1 = shadowRoot.createComponentByDef('c', compDef) expect(c1.getBackendContext()).toBe(domBackend) expect(c1.getBackendElement()).toBeTruthy() c1.appendChild(t1) const sr1 = c1.getShadowRoot()! expect(sr1.getBackendContext()).toBe(domBackend) const n1 = sr1.createNativeNode('n') expect(n1.getBackendContext()).toBe(domBackend) expect(n1.getBackendElement()).toBeTruthy() sr1.appendChild(n1) const v1 = shadowRoot.createVirtualNode() expect(v1.getBackendContext()).toBe(domBackend) v1.appendChild(c1) shadowRoot.appendChild(v1) v1.destroyBackendElementOnSubtree() expect(t1.getBackendElement()).toBeNull() expect(c1.getBackendContext()).toBeNull() expect(c1.getBackendElement()).toBeNull() expect(sr1.getBackendContext()).toBeNull() expect(n1.getBackendContext()).toBeNull() expect(n1.getBackendElement()).toBeNull() expect(v1.getBackendContext()).toBeNull() }) test('destroy backend element on removal', () => { const compDef = componentSpace.define().registerComponent() const comp = glassEasel.Component.createWithContext('root', compDef.general(), domBackend) const shadowRoot = comp.getShadowRoot()! const t1 = shadowRoot.createTextNode('a') t1.destroyBackendElementOnRemoval() expect(t1.getBackendContext()).toBe(domBackend) expect(t1.getBackendElement()).toBeTruthy() const c1 = shadowRoot.createComponentByDef('c', compDef) c1.destroyBackendElementOnRemoval() expect(c1.getBackendContext()).toBe(domBackend) expect(c1.getBackendElement()).toBeTruthy() c1.appendChild(t1) const sr1 = c1.getShadowRoot()! expect(sr1.getBackendContext()).toBe(domBackend) const n1 = sr1.createNativeNode('n') n1.destroyBackendElementOnRemoval() expect(n1.getBackendContext()).toBe(domBackend) expect(n1.getBackendElement()).toBeTruthy() sr1.appendChild(n1) const v1 = shadowRoot.createVirtualNode() v1.destroyBackendElementOnRemoval() expect(v1.getBackendContext()).toBe(domBackend) v1.appendChild(c1) shadowRoot.appendChild(v1) shadowRoot.removeChild(v1) expect(t1.getBackendElement()).toBeNull() expect(c1.getBackendContext()).toBeNull() expect(c1.getBackendElement()).toBeNull() expect(sr1.getBackendContext()).toBeNull() expect(n1.getBackendContext()).toBeNull() expect(n1.getBackendElement()).toBeNull() expect(v1.getBackendContext()).toBeNull() }) })