import * as glassEasel from 'glass-easel'
import { virtual as matchElementWithDom } from '../../../glass-easel/tests/base/match'
import {
hookBuilderToSyncData,
type ShadowSyncElement,
ReflectTemplateEngine,
} from '../../src/backend'
import {
getViewNode,
shadowSyncBackend,
syncController,
tmpl,
viewComponentSpace,
} from '../base/env'
const componentSpace = new glassEasel.ComponentSpace()
componentSpace.updateComponentOptions({
writeFieldsToNode: true,
writeIdToDOM: true,
})
const domHtml = (elem: glassEasel.Element): string => {
const domElem = elem.getBackendElement() as unknown as Element
return domElem.innerHTML
}
describe('backend', () => {
test('basic backend', () => {
const child = componentSpace.defineComponent({
properties: {
text: String,
},
template: tmpl(`
{{text}}
`),
})
const rootDef = componentSpace.defineComponent({
using: { child },
template: tmpl(`
!{{text}}!
`),
data: {
text: '',
},
})
const elem = glassEasel.Component.createWithContext('root', rootDef, shadowSyncBackend)
elem.destroyBackendElementOnDetach()
expect(domHtml(elem)).toEqual('')
elem.setData({
text: '123',
})
expect(domHtml(elem)).toEqual('123!123!')
elem.setData({
text: '233',
})
expect(domHtml(elem)).toEqual('233!233!')
elem.setData({
text: '',
})
expect(domHtml(elem)).toEqual('')
})
test('external component', () => {
viewComponentSpace.setGlobalUsingComponent(
'wx-button',
viewComponentSpace.defineComponent({
is: 'wx-button',
options: {
externalComponent: true,
},
properties: {
disabled: Boolean,
},
template: tmpl(''),
}) as glassEasel.GeneralComponentDefinition,
)
const ops: any[] = []
const rootDef = componentSpace.defineComponent({
template: tmpl(`
{{text}}
`),
data: {
text: '123',
disabled: true,
},
methods: {
handleTap(e: glassEasel.Event) {
ops.push(e.detail)
},
},
})
const root = glassEasel.Component.createWithContext('root', rootDef, shadowSyncBackend)
root.destroyBackendElementOnDetach()
expect(domHtml(root)).toEqual('')
root.setData({ disabled: false })
expect(domHtml(root)).toEqual('')
root.setData({ text: '23333' })
expect(domHtml(root)).toEqual('')
const button = root.getShadowRoot()!.childNodes[0]!
const viewButton = getViewNode(button) as glassEasel.GeneralComponent
viewButton.triggerEvent('tap', { foo: 'foo' })
expect(ops).toEqual([{ foo: 'foo' }])
})
test('hook to sync behavior builder', async () => {
const beh = hookBuilderToSyncData(componentSpace.define())
.property('name', String)
.property('value', String)
.registerBehavior()
const compDef = componentSpace.defineComponent({
behaviors: [beh],
template: tmpl(`
{{name}}-{{value}}
`),
})
const root = glassEasel.Component.createWithContext('root', compDef, shadowSyncBackend)
root.destroyBackendElementOnDetach()
const viewRoot = getViewNode(root) as glassEasel.GeneralComponent
expect(domHtml(root)).toEqual('-')
expect(viewRoot.data.name).toEqual('')
expect(viewRoot.data.value).toEqual('')
root.setData({ name: 'a', value: 'b' })
await Promise.resolve()
expect(domHtml(root)).toEqual('a-b')
expect(viewRoot.data.name).toEqual('a')
expect(viewRoot.data.value).toEqual('b')
})
test('hook to sync behavior builder with method caller', async () => {
const methodCallerMap = new WeakMap()
const beh = hookBuilderToSyncData(componentSpace.define(), (methodCaller) =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
methodCallerMap.get(methodCaller),
)
.property('name', String)
.property('value', String)
.registerBehavior()
const compDef = componentSpace
.define()
.behavior(beh)
.template(
tmpl(`
{{name}}-{{value}}
`),
)
.methodCallerInit(function () {
const methodCaller = {}
methodCallerMap.set(methodCaller, this)
return methodCaller
})
.registerComponent()
const root = glassEasel.Component.createWithContext('root', compDef, shadowSyncBackend)
root.destroyBackendElementOnDetach()
const viewRoot = getViewNode(root) as glassEasel.GeneralComponent
expect(domHtml(root)).toEqual('-')
expect(viewRoot.data.name).toEqual('')
expect(viewRoot.data.value).toEqual('')
root.setData({ name: 'a', value: 'b' })
await Promise.resolve()
expect(domHtml(root)).toEqual('a-b')
expect(viewRoot.data.name).toEqual('a')
expect(viewRoot.data.value).toEqual('b')
})
test('hook template engine to sync', () => {
viewComponentSpace.setGlobalUsingComponent(
'wx-textarea',
viewComponentSpace.defineComponent({
is: 'wx-textarea',
externalClasses: ['placeholder-class'],
properties: {
disabled: Boolean,
maxlength: {
type: Number,
value: 140,
},
},
template: tmpl(
'',
),
}) as glassEasel.GeneralComponentDefinition,
)
componentSpace.setGlobalUsingComponent(
'wx-textarea',
componentSpace.defineComponent({
is: 'wx-textarea',
options: {
externalComponent: true,
templateEngine: ReflectTemplateEngine,
},
properties: {
disabled: Boolean,
maxlength: {
type: Number,
value: 140,
},
placeholderClass: {
type: String,
value: '',
},
},
}) as glassEasel.GeneralComponentDefinition,
)
const ops: any[] = []
const rootDef = componentSpace.defineComponent({
template: tmpl(`
{{text}}
`),
data: {
text: '123',
disabled: true,
placeholderClass: '',
},
methods: {
handleTap(e: glassEasel.Event) {
ops.push(e.detail)
},
},
})
const root = glassEasel.Component.createWithContext('root', rootDef, shadowSyncBackend)
root.destroyBackendElementOnDetach()
expect(domHtml(root)).toEqual(
'123',
)
root.setData({ disabled: false })
expect(domHtml(root)).toEqual(
'123',
)
root.setData({ text: '23333' })
expect(domHtml(root)).toEqual(
'23333',
)
root.setData({ placeholderClass: 'a' })
expect(domHtml(root)).toEqual(
'23333',
)
const textarea = root.getShadowRoot()!.childNodes[0]!
const viewButton = getViewNode(textarea) as glassEasel.GeneralComponent
viewButton.triggerEvent('tap', { foo: 'foo' })
expect(ops).toEqual([{ foo: 'foo' }])
})
test('sync setModelListener', () => {
viewComponentSpace.setGlobalUsingComponent(
'wx-input',
viewComponentSpace.defineComponent({
is: 'wx-input',
properties: {
value: {
type: String,
value: '',
},
},
}) as glassEasel.GeneralComponentDefinition,
)
const rootDef = componentSpace.defineComponent({
template: tmpl(`
`),
data: {
value: '123',
},
})
const root = glassEasel.Component.createWithContext('root', rootDef, shadowSyncBackend)
root.destroyBackendElementOnDetach()
const input = root.getShadowRoot()!.childNodes[0]!
const inputOnView = getViewNode(input) as glassEasel.GeneralComponent
expect(domHtml(root)).toEqual('')
matchElementWithDom(root)
expect(inputOnView.data.value).toEqual('123')
inputOnView.setData({ value: '456' })
expect(inputOnView.data.value).toEqual('456')
expect(root.data.value).toEqual('456')
})
test('external structure on lifetimes', () => {
viewComponentSpace.setGlobalUsingComponent(
'wx-button',
viewComponentSpace.defineComponent({
is: 'wx-button',
options: {
externalComponent: true,
},
template: tmpl(''),
attached() {
matchElementWithDom(getViewNode(root))
},
detached() {
matchElementWithDom(getViewNode(root))
},
}) as glassEasel.GeneralComponentDefinition,
)
const subComp = componentSpace.defineComponent({
template: tmpl(`
`),
})
const rootDef = componentSpace.defineComponent({
using: {
'sub-comp': subComp,
},
template: tmpl(`
123
`),
data: {
show: false,
},
})
const root = glassEasel.Component.createWithContext('root', rootDef, shadowSyncBackend)
shadowSyncBackend.getRootNode().appendChild(root.getBackendElement() as ShadowSyncElement)
glassEasel.Component.pretendAttached(root)
root.destroyBackendElementOnDetach()
expect(domHtml(root)).toEqual(
'',
)
matchElementWithDom(root)
matchElementWithDom(getViewNode(root))
root.setData({ show: true })
expect(domHtml(root)).toEqual(
'',
)
matchElementWithDom(root)
matchElementWithDom(getViewNode(root))
})
test('syncing dynamic slots', () => {
viewComponentSpace.setGlobalUsingComponent(
'list',
viewComponentSpace.defineComponent({
is: 'list',
options: {
dynamicSlots: true,
},
properties: {
list: {
type: Array,
value: [],
},
},
data: {
displayList: [] as number[],
},
template: tmpl(''),
}) as glassEasel.GeneralComponentDefinition,
)
componentSpace.setGlobalUsingComponent(
'list',
componentSpace.defineComponent({
is: 'list',
options: {
dynamicSlots: true,
templateEngine: ReflectTemplateEngine,
},
properties: {
list: {
type: Array,
value: [],
},
},
}) as glassEasel.GeneralComponentDefinition,
)
const listArray = new Array(100).fill(0).map((_, i) => i)
const rootDef = componentSpace.defineComponent({
template: tmpl(`
{{item}}
`),
data: {
listArray,
},
})
const root = glassEasel.Component.createWithContext('root', rootDef, shadowSyncBackend)
shadowSyncBackend.getRootNode().appendChild(root.getBackendElement() as ShadowSyncElement)
glassEasel.Component.pretendAttached(root)
root.destroyBackendElementOnDetach()
const list = root.getShadowRoot()!.childNodes[0] as glassEasel.GeneralComponent
const listOnView = getViewNode(list)
expect(domHtml(root)).toEqual('
')
matchElementWithDom(root)
matchElementWithDom(getViewNode(root))
expect(list.data.list).toEqual(listArray)
expect(listOnView.data.list).toEqual(listArray)
listOnView.setData({
displayList: [0, 1, 2],
})
expect(listOnView.data.displayList).toEqual([0, 1, 2])
expect(domHtml(root)).toEqual('012
')
matchElementWithDom(root)
matchElementWithDom(getViewNode(root))
listOnView.spliceArrayDataOnPath(['displayList'], undefined, undefined, [3, 4])
listOnView.applyDataUpdates()
expect(listOnView.data.displayList).toEqual([0, 1, 2, 3, 4])
expect(domHtml(root)).toEqual('01234
')
matchElementWithDom(root)
matchElementWithDom(getViewNode(root))
listOnView.spliceArrayDataOnPath(['displayList'], 0, 2, [])
listOnView.applyDataUpdates()
expect(listOnView.data.displayList).toEqual([2, 3, 4])
expect(domHtml(root)).toEqual('234
')
matchElementWithDom(root)
matchElementWithDom(getViewNode(root))
})
test('wxs', () => {
const customHandler = jest.fn().mockReturnThis()
shadowSyncBackend.onCustomMethodFromView((element, options) => {
const component = element as glassEasel.GeneralComponent
const { method, args } = options as { method: string; args: any[] }
component.callMethod(method, args)
})
syncController.handleCustomMethod = (
element: glassEasel.Element,
{
evName: eventName,
final,
mutated,
capture,
generalLvaluePath: lvaluePath,
}: {
evName: string
final: boolean
mutated: boolean
capture: boolean
generalLvaluePath: (string | number)[]
},
) => {
const isWXS =
lvaluePath?.[0] === glassEasel.template.GeneralLvaluePathPrefix.InlineScript ||
lvaluePath?.[0] === glassEasel.template.GeneralLvaluePathPrefix.Script
expect(isWXS).toBe(true)
if (lvaluePath[0] === glassEasel.template.GeneralLvaluePathPrefix.Script) {
// const path = lvaluePath[1] as string
// const key = lvaluePath[2] as string
// return
throw new Error('not implemented')
} else if (lvaluePath[0] === glassEasel.template.GeneralLvaluePathPrefix.InlineScript) {
const wxmlFile = lvaluePath[1] as string
const module = lvaluePath[2] as string
const key = lvaluePath[3] as string
expect(wxmlFile).toBe('')
expect(module).toBe('w')
expect(key).toBe('customHandler')
syncController.setListenerStats(
element,
eventName,
capture,
// eslint-disable-next-line no-nested-ternary
final
? glassEasel.EventMutLevel.Final
: mutated
? glassEasel.EventMutLevel.Mut
: glassEasel.EventMutLevel.None,
function (this: glassEasel.GeneralComponent, _ev: glassEasel.ShadowedEvent) {
syncController.sendCustomMethod(this.ownerShadowRoot!.getHostNode(), {
method: 'customHandler',
args: [true],
})
},
)
} else {
throw new Error('unexpected lvalue path')
}
}
const def = glassEasel.registerElement({
template: tmpl(
`
module.exports = { customHandler: function () { throw new Error("should not call") } }
`,
{},
{
C: (elem, evName, listener, final, mutated, capture, generalLvaluePath) => {
const prefix = generalLvaluePath?.[0] as number | null | undefined
const isWxsHandler =
prefix === glassEasel.template.GeneralLvaluePathPrefix.Script ||
prefix === glassEasel.template.GeneralLvaluePathPrefix.InlineScript
if (isWxsHandler) {
;(elem.getBackendElement() as ShadowSyncElement).sendCustomMethod({
evName,
final,
mutated,
capture,
generalLvaluePath,
})
return null
}
return (e) => listener.call(elem.ownerShadowRoot?.getHostNode().getMethodCaller(), e)
},
},
),
methods: {
customHandler,
},
})
const elem = glassEasel.Component.createWithContext('root', def.general(), shadowSyncBackend)
const div = elem.getShadowRoot()!.getElementById('a')!
getViewNode(div).triggerEvent('customEv')
expect(customHandler).toHaveBeenCalledTimes(1)
expect(customHandler).toHaveBeenCalledWith([true])
expect(customHandler).toHaveReturnedWith(elem)
})
})