+import { ref, reactive } from '@vue/reactivity'
+import {
+ renderToString,
+ h,
+ nodeOps,
+ render,
+ serializeInner,
+ nextTick,
+ watch,
+ createComponent,
+ triggerEvent,
+ TestElement
+} from '@vue/runtime-test'
+
// reference: https://vue-composition-api-rfc.netlify.com/api.html#setup
describe('api: setup context', () => {
- test.todo('should work')
+ it('should expose return values to template render context', () => {
+ const Comp = {
+ setup() {
+ return {
+ // ref should auto-unwrap
+ ref: ref('foo'),
+ // object exposed as-is
+ object: reactive({ msg: 'bar' }),
+ // primitive value exposed as-is
+ value: 'baz'
+ }
+ },
+ render() {
+ return `${this.ref} ${this.object.msg} ${this.value}`
+ }
+ }
+ expect(renderToString(h(Comp))).toMatch(`foo bar baz`)
+ })
+
+ it('should support returning render function', () => {
+ const Comp = {
+ setup() {
+ return () => {
+ return h('div', 'hello')
+ }
+ }
+ }
+ expect(renderToString(h(Comp))).toMatch(`hello`)
+ })
+
+ it('props', async () => {
+ const count = ref(0)
+ let dummy
+
+ const Parent = {
+ render: () => h(Child, { count: count.value })
+ }
+
+ const Child = createComponent({
+ setup(props: { count: number }) {
+ watch(() => {
+ dummy = props.count
+ })
+ return () => h('div', props.count)
+ }
+ })
+
+ const root = nodeOps.createElement('div')
+ render(h(Parent), root)
+ expect(serializeInner(root)).toMatch(`<div>0</div>`)
+ expect(dummy).toBe(0)
+
+ // props should be reactive
+ count.value++
+ await nextTick()
+ expect(serializeInner(root)).toMatch(`<div>1</div>`)
+ expect(dummy).toBe(1)
+ })
+
+ it('context.attrs', async () => {
+ const toggle = ref(true)
+
+ const Parent = {
+ render: () => h(Child, toggle.value ? { id: 'foo' } : { class: 'baz' })
+ }
+
+ const Child = {
+ // explicit empty props declaration
+ // puts everything received in attrs
+ props: {},
+ setup(props: any, { attrs }: any) {
+ return () => h('div', attrs)
+ }
+ }
+
+ const root = nodeOps.createElement('div')
+ render(h(Parent), root)
+ expect(serializeInner(root)).toMatch(`<div id="foo"></div>`)
+
+ // should update even though it's not reactive
+ toggle.value = false
+ await nextTick()
+ expect(serializeInner(root)).toMatch(`<div class="baz"></div>`)
+ })
+
+ it('context.slots', async () => {
+ const id = ref('foo')
+
+ const Parent = {
+ render: () =>
+ h(Child, null, {
+ foo: () => id.value,
+ bar: () => 'bar'
+ })
+ }
+
+ const Child = {
+ setup(props: any, { slots }: any) {
+ return () => h('div', [...slots.foo(), ...slots.bar()])
+ }
+ }
+
+ const root = nodeOps.createElement('div')
+ render(h(Parent), root)
+ expect(serializeInner(root)).toMatch(`<div>foobar</div>`)
+
+ // should update even though it's not reactive
+ id.value = 'baz'
+ await nextTick()
+ expect(serializeInner(root)).toMatch(`<div>bazbar</div>`)
+ })
+
+ it('context.emit', async () => {
+ const count = ref(0)
+ const spy = jest.fn()
+
+ const Parent = {
+ render: () =>
+ h(Child, {
+ count: count.value,
+ onInc: (newVal: number) => {
+ spy()
+ count.value = newVal
+ }
+ })
+ }
+
+ const Child = createComponent({
+ props: {
+ count: {
+ type: Number,
+ default: 1
+ }
+ },
+ setup(props, { emit }) {
+ return () =>
+ h(
+ 'div',
+ {
+ onClick: () => emit('inc', props.count + 1)
+ },
+ props.count
+ )
+ }
+ })
+
+ const root = nodeOps.createElement('div')
+ render(h(Parent), root)
+ expect(serializeInner(root)).toMatch(`<div>0</div>`)
+
+ // emit should trigger parent handler
+ triggerEvent(root.children[0] as TestElement, 'click')
+ expect(spy).toHaveBeenCalled()
+ await nextTick()
+ expect(serializeInner(root)).toMatch(`<div>1</div>`)
+ })
})
attrs: Data
slots: Slots
refs: Data
- parent: ComponentInstance | null
- root: ComponentInstance
emit: ((event: string, ...args: unknown[]) => void)
}
attrs: new Proxy(instance, SetupProxyHandlers.attrs),
slots: new Proxy(instance, SetupProxyHandlers.slots),
refs: new Proxy(instance, SetupProxyHandlers.refs),
- emit: instance.emit,
- parent: instance.parent,
- root: instance.root
+ emit: instance.emit
} as any
return __DEV__ ? Object.freeze(context) : context
}
slots,
attrs,
refs,
- emit,
- parent,
- root
+ emit
} = instance
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
return normalizeVNode(
attrs,
slots,
refs,
- emit,
- parent,
- root
+ emit
})
: render(props, null as any)
)
return true
}
}
+ console.log(111)
return false
}
import { createRenderer, VNode } from '@vue/runtime-core'
import { nodeOps, TestElement } from './nodeOps'
import { patchProp } from './patchProp'
+import { serializeInner } from './serialize'
export const render = createRenderer({
patchProp,
...nodeOps
}) as (node: VNode | null, container: TestElement) => VNode
-export { serialize } from './serialize'
-export { triggerEvent } from './triggerEvent'
+// convenience for one-off render validations
+export function renderToString(vnode: VNode) {
+ const root = nodeOps.createElement('div')
+ render(vnode, root)
+ return serializeInner(root)
+}
+
+export * from './triggerEvent'
+export * from './serialize'
export * from './nodeOps'
export * from './jestUtils'
export * from '@vue/runtime-core'
}
}
+export function serializeInner(
+ node: TestElement,
+ indent: number = 0,
+ depth: number = 0
+) {
+ const newLine = indent ? `\n` : ``
+ return node.children.length
+ ? newLine +
+ node.children.map(c => serialize(c, indent, depth + 1)).join(newLine) +
+ newLine
+ : ``
+}
+
function serializeElement(
node: TestElement,
indent: number,
): string {
const props = Object.keys(node.props)
.map(key => {
- return isOn(key) ? `` : `${key}=${JSON.stringify(node.props[key])}`
+ const value = node.props[key]
+ return isOn(key) || value == null ? `` : `${key}=${JSON.stringify(value)}`
})
+ .filter(_ => _)
.join(' ')
- const newLine = indent ? `\n` : ``
- const children = node.children.length
- ? newLine +
- node.children.map(c => serialize(c, indent, depth + 1)).join(newLine) +
- newLine
- : ``
const padding = indent ? ` `.repeat(indent).repeat(depth) : ``
return (
`${padding}<${node.tag}${props ? ` ${props}` : ``}>` +
- `${children}` +
+ `${serializeInner(node, indent, depth)}` +
`${padding}</${node.tag}>`
)
}