rendererTemplateRef.spec.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. import { vi } from 'vitest'
  2. import {
  3. ref,
  4. nodeOps,
  5. h,
  6. render,
  7. nextTick,
  8. defineComponent,
  9. reactive,
  10. serializeInner,
  11. shallowRef
  12. } from '@vue/runtime-test'
  13. // reference: https://vue-composition-api-rfc.netlify.com/api.html#template-refs
  14. describe('api: template refs', () => {
  15. it('string ref mount', () => {
  16. const root = nodeOps.createElement('div')
  17. const el = ref(null)
  18. const Comp = {
  19. setup() {
  20. return {
  21. refKey: el
  22. }
  23. },
  24. render() {
  25. return h('div', { ref: 'refKey' })
  26. }
  27. }
  28. render(h(Comp), root)
  29. expect(el.value).toBe(root.children[0])
  30. })
  31. it('string ref update', async () => {
  32. const root = nodeOps.createElement('div')
  33. const fooEl = ref(null)
  34. const barEl = ref(null)
  35. const refKey = ref('foo')
  36. const Comp = {
  37. setup() {
  38. return {
  39. foo: fooEl,
  40. bar: barEl
  41. }
  42. },
  43. render() {
  44. return h('div', { ref: refKey.value })
  45. }
  46. }
  47. render(h(Comp), root)
  48. expect(fooEl.value).toBe(root.children[0])
  49. expect(barEl.value).toBe(null)
  50. refKey.value = 'bar'
  51. await nextTick()
  52. expect(fooEl.value).toBe(null)
  53. expect(barEl.value).toBe(root.children[0])
  54. })
  55. it('string ref unmount', async () => {
  56. const root = nodeOps.createElement('div')
  57. const el = ref(null)
  58. const toggle = ref(true)
  59. const Comp = {
  60. setup() {
  61. return {
  62. refKey: el
  63. }
  64. },
  65. render() {
  66. return toggle.value ? h('div', { ref: 'refKey' }) : null
  67. }
  68. }
  69. render(h(Comp), root)
  70. expect(el.value).toBe(root.children[0])
  71. toggle.value = false
  72. await nextTick()
  73. expect(el.value).toBe(null)
  74. })
  75. it('function ref mount', () => {
  76. const root = nodeOps.createElement('div')
  77. const fn = vi.fn()
  78. const Comp = defineComponent(() => () => h('div', { ref: fn }))
  79. render(h(Comp), root)
  80. expect(fn.mock.calls[0][0]).toBe(root.children[0])
  81. })
  82. it('function ref update', async () => {
  83. const root = nodeOps.createElement('div')
  84. const fn1 = vi.fn()
  85. const fn2 = vi.fn()
  86. const fn = ref(fn1)
  87. const Comp = defineComponent(() => () => h('div', { ref: fn.value }))
  88. render(h(Comp), root)
  89. expect(fn1.mock.calls).toHaveLength(1)
  90. expect(fn1.mock.calls[0][0]).toBe(root.children[0])
  91. expect(fn2.mock.calls).toHaveLength(0)
  92. fn.value = fn2
  93. await nextTick()
  94. expect(fn1.mock.calls).toHaveLength(1)
  95. expect(fn2.mock.calls).toHaveLength(1)
  96. expect(fn2.mock.calls[0][0]).toBe(root.children[0])
  97. })
  98. it('function ref unmount', async () => {
  99. const root = nodeOps.createElement('div')
  100. const fn = vi.fn()
  101. const toggle = ref(true)
  102. const Comp = defineComponent(
  103. () => () => toggle.value ? h('div', { ref: fn }) : null
  104. )
  105. render(h(Comp), root)
  106. expect(fn.mock.calls[0][0]).toBe(root.children[0])
  107. toggle.value = false
  108. await nextTick()
  109. expect(fn.mock.calls[1][0]).toBe(null)
  110. })
  111. it('render function ref mount', () => {
  112. const root = nodeOps.createElement('div')
  113. const el = ref(null)
  114. const Comp = {
  115. setup() {
  116. return () => h('div', { ref: el })
  117. }
  118. }
  119. render(h(Comp), root)
  120. expect(el.value).toBe(root.children[0])
  121. })
  122. it('render function ref update', async () => {
  123. const root = nodeOps.createElement('div')
  124. const refs = {
  125. foo: ref(null),
  126. bar: ref(null)
  127. }
  128. const refKey = ref<keyof typeof refs>('foo')
  129. const Comp = {
  130. setup() {
  131. return () => h('div', { ref: refs[refKey.value] })
  132. }
  133. }
  134. render(h(Comp), root)
  135. expect(refs.foo.value).toBe(root.children[0])
  136. expect(refs.bar.value).toBe(null)
  137. refKey.value = 'bar'
  138. await nextTick()
  139. expect(refs.foo.value).toBe(null)
  140. expect(refs.bar.value).toBe(root.children[0])
  141. })
  142. it('render function ref unmount', async () => {
  143. const root = nodeOps.createElement('div')
  144. const el = ref(null)
  145. const toggle = ref(true)
  146. const Comp = {
  147. setup() {
  148. return () => (toggle.value ? h('div', { ref: el }) : null)
  149. }
  150. }
  151. render(h(Comp), root)
  152. expect(el.value).toBe(root.children[0])
  153. toggle.value = false
  154. await nextTick()
  155. expect(el.value).toBe(null)
  156. })
  157. test('string ref inside slots', async () => {
  158. const root = nodeOps.createElement('div')
  159. const spy = vi.fn()
  160. const Child = {
  161. render(this: any) {
  162. return this.$slots.default()
  163. }
  164. }
  165. const Comp = {
  166. render() {
  167. return h(Child, () => {
  168. return h('div', { ref: 'foo' })
  169. })
  170. },
  171. mounted(this: any) {
  172. spy(this.$refs.foo.tag)
  173. }
  174. }
  175. render(h(Comp), root)
  176. expect(spy).toHaveBeenCalledWith('div')
  177. })
  178. it('should work with direct reactive property', () => {
  179. const root = nodeOps.createElement('div')
  180. const state = reactive({
  181. refKey: null
  182. })
  183. const Comp = {
  184. setup() {
  185. return state
  186. },
  187. render() {
  188. return h('div', { ref: 'refKey' })
  189. }
  190. }
  191. render(h(Comp), root)
  192. expect(state.refKey).toBe(root.children[0])
  193. })
  194. test('multiple root refs', () => {
  195. const root = nodeOps.createElement('div')
  196. const refKey1 = ref(null)
  197. const refKey2 = ref(null)
  198. const refKey3 = ref(null)
  199. const Comp = {
  200. setup() {
  201. return {
  202. refKey1,
  203. refKey2,
  204. refKey3
  205. }
  206. },
  207. render() {
  208. return [
  209. h('div', { ref: 'refKey1' }),
  210. h('div', { ref: 'refKey2' }),
  211. h('div', { ref: 'refKey3' })
  212. ]
  213. }
  214. }
  215. render(h(Comp), root)
  216. expect(refKey1.value).toBe(root.children[1])
  217. expect(refKey2.value).toBe(root.children[2])
  218. expect(refKey3.value).toBe(root.children[3])
  219. })
  220. // #1505
  221. test('reactive template ref in the same template', async () => {
  222. const Comp = {
  223. setup() {
  224. const el = ref()
  225. return { el }
  226. },
  227. render(this: any) {
  228. return h('div', { id: 'foo', ref: 'el' }, this.el && this.el.props.id)
  229. }
  230. }
  231. const root = nodeOps.createElement('div')
  232. render(h(Comp), root)
  233. // ref not ready on first render, but should queue an update immediately
  234. expect(serializeInner(root)).toBe(`<div id="foo"></div>`)
  235. await nextTick()
  236. // ref should be updated
  237. expect(serializeInner(root)).toBe(`<div id="foo">foo</div>`)
  238. })
  239. // #1834
  240. test('exchange refs', async () => {
  241. const refToggle = ref(false)
  242. const spy = vi.fn()
  243. const Comp = {
  244. render(this: any) {
  245. return [
  246. h('p', { ref: refToggle.value ? 'foo' : 'bar' }),
  247. h('i', { ref: refToggle.value ? 'bar' : 'foo' })
  248. ]
  249. },
  250. mounted(this: any) {
  251. spy(this.$refs.foo.tag, this.$refs.bar.tag)
  252. },
  253. updated(this: any) {
  254. spy(this.$refs.foo.tag, this.$refs.bar.tag)
  255. }
  256. }
  257. const root = nodeOps.createElement('div')
  258. render(h(Comp), root)
  259. expect(spy.mock.calls[0][0]).toBe('i')
  260. expect(spy.mock.calls[0][1]).toBe('p')
  261. refToggle.value = true
  262. await nextTick()
  263. expect(spy.mock.calls[1][0]).toBe('p')
  264. expect(spy.mock.calls[1][1]).toBe('i')
  265. })
  266. // #1789
  267. test('toggle the same ref to different elements', async () => {
  268. const refToggle = ref(false)
  269. const spy = vi.fn()
  270. const Comp = {
  271. render(this: any) {
  272. return refToggle.value ? h('p', { ref: 'foo' }) : h('i', { ref: 'foo' })
  273. },
  274. mounted(this: any) {
  275. spy(this.$refs.foo.tag)
  276. },
  277. updated(this: any) {
  278. spy(this.$refs.foo.tag)
  279. }
  280. }
  281. const root = nodeOps.createElement('div')
  282. render(h(Comp), root)
  283. expect(spy.mock.calls[0][0]).toBe('i')
  284. refToggle.value = true
  285. await nextTick()
  286. expect(spy.mock.calls[1][0]).toBe('p')
  287. })
  288. // #2078
  289. test('handling multiple merged refs', async () => {
  290. const Foo = {
  291. render: () => h('div', 'foo')
  292. }
  293. const Bar = {
  294. render: () => h('div', 'bar')
  295. }
  296. const viewRef = shallowRef<any>(Foo)
  297. const elRef1 = ref()
  298. const elRef2 = ref()
  299. const App = {
  300. render() {
  301. if (!viewRef.value) {
  302. return null
  303. }
  304. const view = h(viewRef.value, { ref: elRef1 })
  305. return h(view, { ref: elRef2 })
  306. }
  307. }
  308. const root = nodeOps.createElement('div')
  309. render(h(App), root)
  310. expect(serializeInner(elRef1.value.$el)).toBe('foo')
  311. expect(elRef1.value).toBe(elRef2.value)
  312. viewRef.value = Bar
  313. await nextTick()
  314. expect(serializeInner(elRef1.value.$el)).toBe('bar')
  315. expect(elRef1.value).toBe(elRef2.value)
  316. viewRef.value = null
  317. await nextTick()
  318. expect(elRef1.value).toBeNull()
  319. expect(elRef1.value).toBe(elRef2.value)
  320. })
  321. // compiled output of <script setup> inline mode
  322. test('raw ref with ref_key', () => {
  323. let refs: any
  324. const el = ref()
  325. const App = {
  326. mounted() {
  327. refs = (this as any).$refs
  328. },
  329. render() {
  330. return h(
  331. 'div',
  332. {
  333. ref: el,
  334. ref_key: 'el'
  335. },
  336. 'hello'
  337. )
  338. }
  339. }
  340. const root = nodeOps.createElement('div')
  341. render(h(App), root)
  342. expect(serializeInner(el.value)).toBe('hello')
  343. expect(serializeInner(refs.el)).toBe('hello')
  344. })
  345. // compiled output of v-for + template ref
  346. test('ref in v-for', async () => {
  347. const show = ref(true)
  348. const list = reactive([1, 2, 3])
  349. const listRefs = ref([])
  350. const mapRefs = () => listRefs.value.map(n => serializeInner(n))
  351. const App = {
  352. render() {
  353. return show.value
  354. ? h(
  355. 'ul',
  356. list.map(i =>
  357. h(
  358. 'li',
  359. {
  360. ref: listRefs,
  361. ref_for: true
  362. },
  363. i
  364. )
  365. )
  366. )
  367. : null
  368. }
  369. }
  370. const root = nodeOps.createElement('div')
  371. render(h(App), root)
  372. expect(mapRefs()).toMatchObject(['1', '2', '3'])
  373. list.push(4)
  374. await nextTick()
  375. expect(mapRefs()).toMatchObject(['1', '2', '3', '4'])
  376. list.shift()
  377. await nextTick()
  378. expect(mapRefs()).toMatchObject(['2', '3', '4'])
  379. show.value = !show.value
  380. await nextTick()
  381. expect(mapRefs()).toMatchObject([])
  382. show.value = !show.value
  383. await nextTick()
  384. expect(mapRefs()).toMatchObject(['2', '3', '4'])
  385. })
  386. test('named ref in v-for', async () => {
  387. const show = ref(true)
  388. const list = reactive([1, 2, 3])
  389. const listRefs = ref([])
  390. const mapRefs = () => listRefs.value.map(n => serializeInner(n))
  391. const App = {
  392. setup() {
  393. return { listRefs }
  394. },
  395. render() {
  396. return show.value
  397. ? h(
  398. 'ul',
  399. list.map(i =>
  400. h(
  401. 'li',
  402. {
  403. ref: 'listRefs',
  404. ref_for: true
  405. },
  406. i
  407. )
  408. )
  409. )
  410. : null
  411. }
  412. }
  413. const root = nodeOps.createElement('div')
  414. render(h(App), root)
  415. expect(mapRefs()).toMatchObject(['1', '2', '3'])
  416. list.push(4)
  417. await nextTick()
  418. expect(mapRefs()).toMatchObject(['1', '2', '3', '4'])
  419. list.shift()
  420. await nextTick()
  421. expect(mapRefs()).toMatchObject(['2', '3', '4'])
  422. show.value = !show.value
  423. await nextTick()
  424. expect(mapRefs()).toMatchObject([])
  425. show.value = !show.value
  426. await nextTick()
  427. expect(mapRefs()).toMatchObject(['2', '3', '4'])
  428. })
  429. // #6697 v-for ref behaves differently under production and development
  430. test('named ref in v-for , should be responsive when rendering', async () => {
  431. const list = ref([1, 2, 3])
  432. const listRefs = ref([])
  433. const App = {
  434. setup() {
  435. return { listRefs }
  436. },
  437. render() {
  438. return h('div', null, [
  439. h('div', null, String(listRefs.value)),
  440. h(
  441. 'ul',
  442. list.value.map(i =>
  443. h(
  444. 'li',
  445. {
  446. ref: 'listRefs',
  447. ref_for: true
  448. },
  449. i
  450. )
  451. )
  452. )
  453. ])
  454. }
  455. }
  456. const root = nodeOps.createElement('div')
  457. render(h(App), root)
  458. await nextTick()
  459. expect(String(listRefs.value)).toBe(
  460. '[object Object],[object Object],[object Object]'
  461. )
  462. expect(serializeInner(root)).toBe(
  463. '<div><div>[object Object],[object Object],[object Object]</div><ul><li>1</li><li>2</li><li>3</li></ul></div>'
  464. )
  465. list.value.splice(0, 1)
  466. await nextTick()
  467. expect(String(listRefs.value)).toBe('[object Object],[object Object]')
  468. expect(serializeInner(root)).toBe(
  469. '<div><div>[object Object],[object Object]</div><ul><li>2</li><li>3</li></ul></div>'
  470. )
  471. })
  472. })