| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529 |
- import {
- type Ref,
- type SetupContext,
- type VNode,
- h,
- inject,
- nextTick,
- nodeOps,
- onMounted,
- provide,
- ref,
- render,
- serializeInner,
- watch,
- } 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: vi.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 = vi.fn(returnThis)
- const dataWatchSpy = vi.fn(returnThis)
- let instance: any
- const Comp = {
- props: {
- testProp: String,
- },
- data() {
- return {
- testData: undefined,
- }
- },
- watch: {
- testProp() {
- // @ts-expect-error
- propWatchSpy(this.$el)
- },
- testData() {
- // @ts-expect-error
- 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>`)
- })
- test('child only updates once when triggered in multiple ways', async () => {
- const a = ref(0)
- const calls: string[] = []
- const Parent = {
- setup() {
- return () => {
- calls.push('render parent')
- return h(Child, { count: a.value }, () => a.value)
- }
- },
- }
- const Child = {
- props: ['count'],
- setup(props: any) {
- return () => {
- calls.push('render child')
- return `${props.count} - ${a.value}`
- }
- },
- }
- render(h(Parent), nodeOps.createElement('div'))
- expect(calls).toEqual(['render parent', 'render child'])
- // This will trigger child rendering directly, as well as via a prop change
- a.value++
- await nextTick()
- expect(calls).toEqual([
- 'render parent',
- 'render child',
- 'render parent',
- 'render child',
- ])
- })
- // #7745
- test(`an earlier update doesn't lead to excessive subsequent updates`, async () => {
- const globalCount = ref(0)
- const parentCount = ref(0)
- const calls: string[] = []
- const Root = {
- setup() {
- return () => {
- calls.push('render root')
- return h(Parent, { count: globalCount.value })
- }
- },
- }
- const Parent = {
- props: ['count'],
- setup(props: any) {
- return () => {
- calls.push('render parent')
- return [
- `${globalCount.value} - ${props.count}`,
- h(Child, { count: parentCount.value }),
- ]
- }
- },
- }
- const Child = {
- props: ['count'],
- setup(props: any) {
- watch(
- () => props.count,
- () => {
- calls.push('child watcher')
- globalCount.value = props.count
- },
- )
- return () => {
- calls.push('render child')
- }
- },
- }
- render(h(Root), nodeOps.createElement('div'))
- expect(calls).toEqual(['render root', 'render parent', 'render child'])
- parentCount.value++
- await nextTick()
- expect(calls).toEqual([
- 'render root',
- 'render parent',
- 'render child',
- 'render parent',
- 'child watcher',
- 'render child',
- 'render root',
- 'render parent',
- ])
- })
- // #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: vi.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])
- })
- test('child component props update should not lead to double update', async () => {
- const text = ref(0)
- const spy = vi.fn()
- const App = {
- render() {
- return h(Comp, { text: text.value })
- },
- }
- const Comp = {
- props: ['text'],
- render(this: any) {
- spy()
- return h('h1', this.text)
- },
- }
- const root = nodeOps.createElement('div')
- render(h(App), root)
- expect(serializeInner(root)).toBe(`<h1>0</h1>`)
- expect(spy).toHaveBeenCalledTimes(1)
- text.value++
- await nextTick()
- expect(serializeInner(root)).toBe(`<h1>1</h1>`)
- expect(spy).toHaveBeenCalledTimes(2)
- })
- it('should warn accessing `this` in a <script setup> template', () => {
- const App = {
- setup() {
- return {
- __isScriptSetup: true,
- }
- },
- render(this: any) {
- return this.$attrs.id
- },
- }
- const root = nodeOps.createElement('div')
- render(h(App), root)
- expect(
- `Property '$attrs' was accessed via 'this'. Avoid using 'this' in templates.`,
- ).toHaveBeenWarned()
- })
- test('should not update child component if style is not changed', async () => {
- const text = ref(0)
- const spy = vi.fn()
- const ClientOnly = {
- setup(_: any, { slots }: SetupContext) {
- const mounted = ref(false)
- onMounted(() => {
- mounted.value = true
- })
- return () => {
- if (mounted.value) {
- return slots.default!()
- }
- }
- },
- }
- const App = {
- render() {
- return h(ClientOnly, null, {
- default: () => [
- h('span', null, [text.value]),
- h(Comp, { style: { width: '100%' } }),
- ],
- })
- },
- }
- const Comp = {
- render(this: any) {
- spy()
- return null
- },
- }
- const root = nodeOps.createElement('div')
- render(h(App), root)
- expect(serializeInner(root)).toBe(`<!---->`)
- await nextTick()
- expect(serializeInner(root)).toBe(`<span>0</span><!---->`)
- expect(spy).toHaveBeenCalledTimes(1)
- text.value++
- await nextTick()
- expect(serializeInner(root)).toBe(`<span>1</span><!---->`)
- // expect Comp to not be re-rendered
- expect(spy).toHaveBeenCalledTimes(1)
- })
- })
|