import { KeepAlive, defineAsyncComponent, defineComponent, h, nextTick, nodeOps, reactive, ref, render, serializeInner, shallowRef, watch, } from '@vue/runtime-test' describe('api: template refs', () => { it('string ref mount', () => { const root = nodeOps.createElement('div') const el = ref(null) const Comp = { setup() { return { refKey: el, } }, render() { return h('div', { ref: 'refKey' }) }, } render(h(Comp), root) expect(el.value).toBe(root.children[0]) }) it('string ref update', async () => { const root = nodeOps.createElement('div') const fooEl = ref(null) const barEl = ref(null) const refKey = ref('foo') const Comp = { setup() { return { foo: fooEl, bar: barEl, } }, render() { return h('div', { ref: refKey.value }) }, } render(h(Comp), root) expect(fooEl.value).toBe(root.children[0]) expect(barEl.value).toBe(null) refKey.value = 'bar' await nextTick() expect(fooEl.value).toBe(null) expect(barEl.value).toBe(root.children[0]) }) it('string ref unmount', async () => { const root = nodeOps.createElement('div') const el = ref(null) const toggle = ref(true) const Comp = { setup() { return { refKey: el, } }, render() { return toggle.value ? h('div', { ref: 'refKey' }) : null }, } render(h(Comp), root) expect(el.value).toBe(root.children[0]) toggle.value = false await nextTick() expect(el.value).toBe(null) }) it('function ref mount', () => { const root = nodeOps.createElement('div') const fn = vi.fn() const Comp = defineComponent(() => () => h('div', { ref: fn })) render(h(Comp), root) expect(fn.mock.calls[0][0]).toBe(root.children[0]) }) it('function ref update', async () => { const root = nodeOps.createElement('div') const fn1 = vi.fn() const fn2 = vi.fn() const fn = ref(fn1) const Comp = defineComponent(() => () => h('div', { ref: fn.value })) render(h(Comp), root) expect(fn1.mock.calls).toHaveLength(1) expect(fn1.mock.calls[0][0]).toBe(root.children[0]) expect(fn2.mock.calls).toHaveLength(0) fn.value = fn2 await nextTick() expect(fn1.mock.calls).toHaveLength(1) expect(fn2.mock.calls).toHaveLength(1) expect(fn2.mock.calls[0][0]).toBe(root.children[0]) }) it('function ref unmount', async () => { const root = nodeOps.createElement('div') const fn = vi.fn() const toggle = ref(true) const Comp = defineComponent( () => () => (toggle.value ? h('div', { ref: fn }) : null), ) render(h(Comp), root) expect(fn.mock.calls[0][0]).toBe(root.children[0]) toggle.value = false await nextTick() expect(fn.mock.calls[1][0]).toBe(null) }) it('render function ref mount', () => { const root = nodeOps.createElement('div') const el = ref(null) const Comp = { setup() { return () => h('div', { ref: el }) }, } render(h(Comp), root) expect(el.value).toBe(root.children[0]) }) it('render function ref update', async () => { const root = nodeOps.createElement('div') const refs = { foo: ref(null), bar: ref(null), } const refKey = ref('foo') const Comp = { setup() { return () => h('div', { ref: refs[refKey.value] }) }, } render(h(Comp), root) expect(refs.foo.value).toBe(root.children[0]) expect(refs.bar.value).toBe(null) refKey.value = 'bar' await nextTick() expect(refs.foo.value).toBe(null) expect(refs.bar.value).toBe(root.children[0]) }) it('render function ref unmount', async () => { const root = nodeOps.createElement('div') const el = ref(null) const toggle = ref(true) const Comp = { setup() { return () => (toggle.value ? h('div', { ref: el }) : null) }, } render(h(Comp), root) expect(el.value).toBe(root.children[0]) toggle.value = false await nextTick() expect(el.value).toBe(null) }) // #12639 it('update and unmount child in the same tick', async () => { const root = nodeOps.createElement('div') const el = ref(null) const toggle = ref(true) const show = ref(true) const Comp = defineComponent({ emits: ['change'], props: ['show'], setup(props, { emit }) { watch( () => props.show, () => { emit('change') }, ) return () => h('div', 'hi') }, }) const App = { setup() { return { refKey: el, } }, render() { return toggle.value ? h(Comp, { ref: 'refKey', show: show.value, onChange: () => (toggle.value = false), }) : null }, } render(h(App), root) expect(el.value).not.toBe(null) show.value = false await nextTick() expect(el.value).toBe(null) }) it('set and change ref in the same tick', async () => { const root = nodeOps.createElement('div') const show = ref(false) const refName = ref('a') const Child = defineComponent({ setup() { refName.value = 'b' return () => {} }, }) const Comp = { render() { return h(Child, { ref: refName.value, }) }, updated(this: any) { expect(this.$refs.a).toBe(null) expect(this.$refs.b).not.toBe(null) }, } const App = { render() { return show.value ? h(Comp) : null }, } render(h(App), root) expect(refName.value).toBe('a') show.value = true await nextTick() expect(refName.value).toBe('b') }) it('unset old ref when new ref is absent', async () => { const root1 = nodeOps.createElement('div') const root2 = nodeOps.createElement('div') const el1 = ref(null) const el2 = ref(null) const toggle = ref(true) const Comp1 = { setup() { return () => (toggle.value ? h('div', { ref: el1 }) : h('div')) }, } const Comp2 = { setup() { return () => h('div', { ref: toggle.value ? el2 : undefined }) }, } render(h(Comp1), root1) render(h(Comp2), root2) expect(el1.value).toBe(root1.children[0]) expect(el2.value).toBe(root2.children[0]) toggle.value = false await nextTick() expect(el1.value).toBe(null) expect(el2.value).toBe(null) }) test('string ref inside slots', async () => { const root = nodeOps.createElement('div') const spy = vi.fn() const Child = { render(this: any) { return this.$slots.default() }, } const Comp = { render() { return h(Child, () => { return h('div', { ref: 'foo' }) }) }, mounted(this: any) { spy(this.$refs.foo.tag) }, } render(h(Comp), root) expect(spy).toHaveBeenCalledWith('div') }) it('should work with direct reactive property', () => { const root = nodeOps.createElement('div') const state = reactive({ refKey: null, }) const Comp = { setup() { return state }, render() { return h('div', { ref: 'refKey' }) }, } render(h(Comp), root) expect(state.refKey).toBe(root.children[0]) expect('Template ref "refKey" used on a non-ref value').toHaveBeenWarned() }) test('multiple root refs', () => { const root = nodeOps.createElement('div') const refKey1 = ref(null) const refKey2 = ref(null) const refKey3 = ref(null) const Comp = { setup() { return { refKey1, refKey2, refKey3, } }, render() { return [ h('div', { ref: 'refKey1' }), h('div', { ref: 'refKey2' }), h('div', { ref: 'refKey3' }), ] }, } render(h(Comp), root) expect(refKey1.value).toBe(root.children[1]) expect(refKey2.value).toBe(root.children[2]) expect(refKey3.value).toBe(root.children[3]) }) // #1505 test('reactive template ref in the same template', async () => { const Comp = { setup() { const el = ref() return { el } }, render(this: any) { return h('div', { id: 'foo', ref: 'el' }, this.el && this.el.props.id) }, } const root = nodeOps.createElement('div') render(h(Comp), root) // ref not ready on first render, but should queue an update immediately expect(serializeInner(root)).toBe(`
`) await nextTick() // ref should be updated expect(serializeInner(root)).toBe(`
foo
`) }) // #1834 test('exchange refs', async () => { const refToggle = ref(false) const spy = vi.fn() const Comp = { render(this: any) { return [ h('p', { ref: refToggle.value ? 'foo' : 'bar' }), h('i', { ref: refToggle.value ? 'bar' : 'foo' }), ] }, mounted(this: any) { spy(this.$refs.foo.tag, this.$refs.bar.tag) }, updated(this: any) { spy(this.$refs.foo.tag, this.$refs.bar.tag) }, } const root = nodeOps.createElement('div') render(h(Comp), root) expect(spy.mock.calls[0][0]).toBe('i') expect(spy.mock.calls[0][1]).toBe('p') refToggle.value = true await nextTick() expect(spy.mock.calls[1][0]).toBe('p') expect(spy.mock.calls[1][1]).toBe('i') }) // #1789 test('toggle the same ref to different elements', async () => { const refToggle = ref(false) const spy = vi.fn() const Comp = { render(this: any) { return refToggle.value ? h('p', { ref: 'foo' }) : h('i', { ref: 'foo' }) }, mounted(this: any) { spy(this.$refs.foo.tag) }, updated(this: any) { spy(this.$refs.foo.tag) }, } const root = nodeOps.createElement('div') render(h(Comp), root) expect(spy.mock.calls[0][0]).toBe('i') refToggle.value = true await nextTick() expect(spy.mock.calls[1][0]).toBe('p') }) // #2078 test('handling multiple merged refs', async () => { const Foo = { render: () => h('div', 'foo'), } const Bar = { render: () => h('div', 'bar'), } const viewRef = shallowRef(Foo) const elRef1 = ref() const elRef2 = ref() const App = { render() { if (!viewRef.value) { return null } const view = h(viewRef.value, { ref: elRef1 }) return h(view, { ref: elRef2 }) }, } const root = nodeOps.createElement('div') render(h(App), root) expect(serializeInner(elRef1.value.$el)).toBe('foo') expect(elRef1.value).toBe(elRef2.value) viewRef.value = Bar await nextTick() expect(serializeInner(elRef1.value.$el)).toBe('bar') expect(elRef1.value).toBe(elRef2.value) viewRef.value = null await nextTick() expect(elRef1.value).toBeNull() expect(elRef1.value).toBe(elRef2.value) }) // compiled output of