325 lines
7.3 KiB
TypeScript
325 lines
7.3 KiB
TypeScript
import {
|
|
ref,
|
|
h,
|
|
render,
|
|
nodeOps,
|
|
serializeInner,
|
|
nextTick,
|
|
VNode,
|
|
provide,
|
|
inject,
|
|
Ref,
|
|
watch,
|
|
SetupContext
|
|
} from '@vue/runtime-test'
|
|
|
|
describe('renderer: component', () => {
|
|
test('should update parent(hoc) component host el when child component self update', async () => {
|
|
const value = ref(true)
|
|
let parentVnode: VNode
|
|
let childVnode1: VNode
|
|
let childVnode2: VNode
|
|
|
|
const Parent = {
|
|
render: () => {
|
|
// let Parent first rerender
|
|
return (parentVnode = h(Child))
|
|
}
|
|
}
|
|
|
|
const Child = {
|
|
render: () => {
|
|
return value.value
|
|
? (childVnode1 = h('div'))
|
|
: (childVnode2 = h('span'))
|
|
}
|
|
}
|
|
|
|
const root = nodeOps.createElement('div')
|
|
render(h(Parent), root)
|
|
expect(serializeInner(root)).toBe(`<div></div>`)
|
|
expect(parentVnode!.el).toBe(childVnode1!.el)
|
|
|
|
value.value = false
|
|
await nextTick()
|
|
expect(serializeInner(root)).toBe(`<span></span>`)
|
|
expect(parentVnode!.el).toBe(childVnode2!.el)
|
|
})
|
|
|
|
it('should create an Component with props', () => {
|
|
const Comp = {
|
|
render: () => {
|
|
return h('div')
|
|
}
|
|
}
|
|
const root = nodeOps.createElement('div')
|
|
render(h(Comp, { id: 'foo', class: 'bar' }), root)
|
|
expect(serializeInner(root)).toBe(`<div id="foo" class="bar"></div>`)
|
|
})
|
|
|
|
it('should create an Component with direct text children', () => {
|
|
const Comp = {
|
|
render: () => {
|
|
return h('div', 'test')
|
|
}
|
|
}
|
|
const root = nodeOps.createElement('div')
|
|
render(h(Comp, { id: 'foo', class: 'bar' }), root)
|
|
expect(serializeInner(root)).toBe(`<div id="foo" class="bar">test</div>`)
|
|
})
|
|
|
|
it('should update an Component tag which is already mounted', () => {
|
|
const Comp1 = {
|
|
render: () => {
|
|
return h('div', 'foo')
|
|
}
|
|
}
|
|
const root = nodeOps.createElement('div')
|
|
render(h(Comp1), root)
|
|
expect(serializeInner(root)).toBe('<div>foo</div>')
|
|
|
|
const Comp2 = {
|
|
render: () => {
|
|
return h('span', 'foo')
|
|
}
|
|
}
|
|
render(h(Comp2), root)
|
|
expect(serializeInner(root)).toBe('<span>foo</span>')
|
|
})
|
|
|
|
// #2072
|
|
it('should not update Component if only changed props are declared emit listeners', () => {
|
|
const Comp1 = {
|
|
emits: ['foo'],
|
|
updated: jest.fn(),
|
|
render: () => null
|
|
}
|
|
const root = nodeOps.createElement('div')
|
|
render(
|
|
h(Comp1, {
|
|
onFoo: () => {}
|
|
}),
|
|
root
|
|
)
|
|
render(
|
|
h(Comp1, {
|
|
onFoo: () => {}
|
|
}),
|
|
root
|
|
)
|
|
expect(Comp1.updated).not.toHaveBeenCalled()
|
|
})
|
|
|
|
// #2043
|
|
test('component child synchronously updating parent state should trigger parent re-render', async () => {
|
|
const App = {
|
|
setup() {
|
|
const n = ref(0)
|
|
provide('foo', n)
|
|
return () => {
|
|
return [h('div', n.value), h(Child)]
|
|
}
|
|
}
|
|
}
|
|
|
|
const Child = {
|
|
setup() {
|
|
const n = inject<Ref<number>>('foo')!
|
|
n.value++
|
|
|
|
return () => {
|
|
return h('div', n.value)
|
|
}
|
|
}
|
|
}
|
|
|
|
const root = nodeOps.createElement('div')
|
|
render(h(App), root)
|
|
expect(serializeInner(root)).toBe(`<div>0</div><div>1</div>`)
|
|
await nextTick()
|
|
expect(serializeInner(root)).toBe(`<div>1</div><div>1</div>`)
|
|
})
|
|
|
|
// #2170
|
|
test('instance.$el should be exposed to watch options', async () => {
|
|
function returnThis(this: any, _arg: any) {
|
|
return this
|
|
}
|
|
const propWatchSpy = jest.fn(returnThis)
|
|
const dataWatchSpy = jest.fn(returnThis)
|
|
let instance: any
|
|
const Comp = {
|
|
props: {
|
|
testProp: String
|
|
},
|
|
|
|
data() {
|
|
return {
|
|
testData: undefined
|
|
}
|
|
},
|
|
|
|
watch: {
|
|
testProp() {
|
|
// @ts-ignore
|
|
propWatchSpy(this.$el)
|
|
},
|
|
testData() {
|
|
// @ts-ignore
|
|
dataWatchSpy(this.$el)
|
|
}
|
|
},
|
|
|
|
created() {
|
|
instance = this
|
|
},
|
|
|
|
render() {
|
|
return h('div')
|
|
}
|
|
}
|
|
|
|
const root = nodeOps.createElement('div')
|
|
render(h(Comp), root)
|
|
await nextTick()
|
|
expect(propWatchSpy).not.toHaveBeenCalled()
|
|
expect(dataWatchSpy).not.toHaveBeenCalled()
|
|
|
|
render(h(Comp, { testProp: 'prop ' }), root)
|
|
await nextTick()
|
|
expect(propWatchSpy).toHaveBeenCalledWith(instance.$el)
|
|
|
|
instance.testData = 1
|
|
await nextTick()
|
|
expect(dataWatchSpy).toHaveBeenCalledWith(instance.$el)
|
|
})
|
|
|
|
// #2200
|
|
test('component child updating parent state in pre-flush should trigger parent re-render', async () => {
|
|
const outer = ref(0)
|
|
const App = {
|
|
setup() {
|
|
const inner = ref(0)
|
|
|
|
return () => {
|
|
return [
|
|
h('div', inner.value),
|
|
h(Child, {
|
|
value: outer.value,
|
|
onUpdate: (val: number) => (inner.value = val)
|
|
})
|
|
]
|
|
}
|
|
}
|
|
}
|
|
|
|
const Child = {
|
|
props: ['value'],
|
|
setup(props: any, { emit }: SetupContext) {
|
|
watch(() => props.value, (val: number) => emit('update', val))
|
|
|
|
return () => {
|
|
return h('div', props.value)
|
|
}
|
|
}
|
|
}
|
|
|
|
const root = nodeOps.createElement('div')
|
|
render(h(App), root)
|
|
expect(serializeInner(root)).toBe(`<div>0</div><div>0</div>`)
|
|
|
|
outer.value++
|
|
await nextTick()
|
|
expect(serializeInner(root)).toBe(`<div>1</div><div>1</div>`)
|
|
})
|
|
|
|
// #2521
|
|
test('should pause tracking deps when initializing legacy options', async () => {
|
|
let childInstance = null as any
|
|
const Child = {
|
|
props: ['foo'],
|
|
data() {
|
|
return {
|
|
count: 0
|
|
}
|
|
},
|
|
watch: {
|
|
foo: {
|
|
immediate: true,
|
|
handler() {
|
|
;(this as any).count
|
|
}
|
|
}
|
|
},
|
|
created() {
|
|
childInstance = this as any
|
|
childInstance.count
|
|
},
|
|
render() {
|
|
return h('h1', (this as any).count)
|
|
}
|
|
}
|
|
|
|
const App = {
|
|
setup() {
|
|
return () => h(Child)
|
|
},
|
|
updated: jest.fn()
|
|
}
|
|
|
|
const root = nodeOps.createElement('div')
|
|
render(h(App), root)
|
|
expect(App.updated).toHaveBeenCalledTimes(0)
|
|
|
|
childInstance.count++
|
|
await nextTick()
|
|
expect(App.updated).toHaveBeenCalledTimes(0)
|
|
})
|
|
|
|
describe('render with access caches', () => {
|
|
// #3297
|
|
test('should not set the access cache in the data() function (production mode)', () => {
|
|
const Comp = {
|
|
data() {
|
|
;(this as any).foo
|
|
return { foo: 1 }
|
|
},
|
|
render() {
|
|
return h('h1', (this as any).foo)
|
|
}
|
|
}
|
|
const root = nodeOps.createElement('div')
|
|
|
|
__DEV__ = false
|
|
render(h(Comp), root)
|
|
__DEV__ = true
|
|
expect(serializeInner(root)).toBe(`<h1>1</h1>`)
|
|
})
|
|
})
|
|
|
|
test('the component VNode should be cloned when reusing it', () => {
|
|
const App = {
|
|
render() {
|
|
const c = [h(Comp)]
|
|
return [c, c, c]
|
|
}
|
|
}
|
|
|
|
const ids: number[] = []
|
|
const Comp = {
|
|
render: () => h('h1'),
|
|
beforeUnmount() {
|
|
ids.push((this as any).$.uid)
|
|
}
|
|
}
|
|
|
|
const root = nodeOps.createElement('div')
|
|
render(h(App), root)
|
|
expect(serializeInner(root)).toBe(`<h1></h1><h1></h1><h1></h1>`)
|
|
|
|
render(null, root)
|
|
expect(serializeInner(root)).toBe(``)
|
|
expect(ids).toEqual([ids[0], ids[0] + 1, ids[0] + 2])
|
|
})
|
|
})
|