rendererTemplateRef.spec.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  1. import {
  2. KeepAlive,
  3. defineAsyncComponent,
  4. defineComponent,
  5. h,
  6. nextTick,
  7. nodeOps,
  8. reactive,
  9. ref,
  10. render,
  11. serializeInner,
  12. shallowRef,
  13. } from '@vue/runtime-test'
  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. expect('Template ref "refKey" used on a non-ref value').toHaveBeenWarned()
  194. })
  195. test('multiple root refs', () => {
  196. const root = nodeOps.createElement('div')
  197. const refKey1 = ref(null)
  198. const refKey2 = ref(null)
  199. const refKey3 = ref(null)
  200. const Comp = {
  201. setup() {
  202. return {
  203. refKey1,
  204. refKey2,
  205. refKey3,
  206. }
  207. },
  208. render() {
  209. return [
  210. h('div', { ref: 'refKey1' }),
  211. h('div', { ref: 'refKey2' }),
  212. h('div', { ref: 'refKey3' }),
  213. ]
  214. },
  215. }
  216. render(h(Comp), root)
  217. expect(refKey1.value).toBe(root.children[1])
  218. expect(refKey2.value).toBe(root.children[2])
  219. expect(refKey3.value).toBe(root.children[3])
  220. })
  221. // #1505
  222. test('reactive template ref in the same template', async () => {
  223. const Comp = {
  224. setup() {
  225. const el = ref()
  226. return { el }
  227. },
  228. render(this: any) {
  229. return h('div', { id: 'foo', ref: 'el' }, this.el && this.el.props.id)
  230. },
  231. }
  232. const root = nodeOps.createElement('div')
  233. render(h(Comp), root)
  234. // ref not ready on first render, but should queue an update immediately
  235. expect(serializeInner(root)).toBe(`<div id="foo"></div>`)
  236. await nextTick()
  237. // ref should be updated
  238. expect(serializeInner(root)).toBe(`<div id="foo">foo</div>`)
  239. })
  240. // #1834
  241. test('exchange refs', async () => {
  242. const refToggle = ref(false)
  243. const spy = vi.fn()
  244. const Comp = {
  245. render(this: any) {
  246. return [
  247. h('p', { ref: refToggle.value ? 'foo' : 'bar' }),
  248. h('i', { ref: refToggle.value ? 'bar' : 'foo' }),
  249. ]
  250. },
  251. mounted(this: any) {
  252. spy(this.$refs.foo.tag, this.$refs.bar.tag)
  253. },
  254. updated(this: any) {
  255. spy(this.$refs.foo.tag, this.$refs.bar.tag)
  256. },
  257. }
  258. const root = nodeOps.createElement('div')
  259. render(h(Comp), root)
  260. expect(spy.mock.calls[0][0]).toBe('i')
  261. expect(spy.mock.calls[0][1]).toBe('p')
  262. refToggle.value = true
  263. await nextTick()
  264. expect(spy.mock.calls[1][0]).toBe('p')
  265. expect(spy.mock.calls[1][1]).toBe('i')
  266. })
  267. // #1789
  268. test('toggle the same ref to different elements', async () => {
  269. const refToggle = ref(false)
  270. const spy = vi.fn()
  271. const Comp = {
  272. render(this: any) {
  273. return refToggle.value ? h('p', { ref: 'foo' }) : h('i', { ref: 'foo' })
  274. },
  275. mounted(this: any) {
  276. spy(this.$refs.foo.tag)
  277. },
  278. updated(this: any) {
  279. spy(this.$refs.foo.tag)
  280. },
  281. }
  282. const root = nodeOps.createElement('div')
  283. render(h(Comp), root)
  284. expect(spy.mock.calls[0][0]).toBe('i')
  285. refToggle.value = true
  286. await nextTick()
  287. expect(spy.mock.calls[1][0]).toBe('p')
  288. })
  289. // #2078
  290. test('handling multiple merged refs', async () => {
  291. const Foo = {
  292. render: () => h('div', 'foo'),
  293. }
  294. const Bar = {
  295. render: () => h('div', 'bar'),
  296. }
  297. const viewRef = shallowRef<any>(Foo)
  298. const elRef1 = ref()
  299. const elRef2 = ref()
  300. const App = {
  301. render() {
  302. if (!viewRef.value) {
  303. return null
  304. }
  305. const view = h(viewRef.value, { ref: elRef1 })
  306. return h(view, { ref: elRef2 })
  307. },
  308. }
  309. const root = nodeOps.createElement('div')
  310. render(h(App), root)
  311. expect(serializeInner(elRef1.value.$el)).toBe('foo')
  312. expect(elRef1.value).toBe(elRef2.value)
  313. viewRef.value = Bar
  314. await nextTick()
  315. expect(serializeInner(elRef1.value.$el)).toBe('bar')
  316. expect(elRef1.value).toBe(elRef2.value)
  317. viewRef.value = null
  318. await nextTick()
  319. expect(elRef1.value).toBeNull()
  320. expect(elRef1.value).toBe(elRef2.value)
  321. })
  322. // compiled output of <script setup> inline mode
  323. test('raw ref with ref_key', () => {
  324. let refs: any
  325. const el = ref()
  326. const App = {
  327. mounted() {
  328. refs = (this as any).$refs
  329. },
  330. render() {
  331. return h(
  332. 'div',
  333. {
  334. ref: el,
  335. ref_key: 'el',
  336. },
  337. 'hello',
  338. )
  339. },
  340. }
  341. const root = nodeOps.createElement('div')
  342. render(h(App), root)
  343. expect(serializeInner(el.value)).toBe('hello')
  344. expect(serializeInner(refs.el)).toBe('hello')
  345. })
  346. // compiled output of v-for + template ref
  347. test('ref in v-for', async () => {
  348. const show = ref(true)
  349. const list = reactive([1, 2, 3])
  350. const listRefs = ref([])
  351. const mapRefs = () => listRefs.value.map(n => serializeInner(n))
  352. const App = {
  353. render() {
  354. return show.value
  355. ? h(
  356. 'ul',
  357. list.map(i =>
  358. h(
  359. 'li',
  360. {
  361. ref: listRefs,
  362. ref_for: true,
  363. },
  364. i,
  365. ),
  366. ),
  367. )
  368. : null
  369. },
  370. }
  371. const root = nodeOps.createElement('div')
  372. render(h(App), root)
  373. expect(mapRefs()).toMatchObject(['1', '2', '3'])
  374. list.push(4)
  375. await nextTick()
  376. expect(mapRefs()).toMatchObject(['1', '2', '3', '4'])
  377. list.shift()
  378. await nextTick()
  379. expect(mapRefs()).toMatchObject(['2', '3', '4'])
  380. show.value = !show.value
  381. await nextTick()
  382. expect(mapRefs()).toMatchObject([])
  383. show.value = !show.value
  384. await nextTick()
  385. expect(mapRefs()).toMatchObject(['2', '3', '4'])
  386. })
  387. test('named ref in v-for', async () => {
  388. const show = ref(true)
  389. const list = reactive([1, 2, 3])
  390. const listRefs = ref([])
  391. const mapRefs = () => listRefs.value.map(n => serializeInner(n))
  392. const App = {
  393. setup() {
  394. return { listRefs }
  395. },
  396. render() {
  397. return show.value
  398. ? h(
  399. 'ul',
  400. list.map(i =>
  401. h(
  402. 'li',
  403. {
  404. ref: 'listRefs',
  405. ref_for: true,
  406. },
  407. i,
  408. ),
  409. ),
  410. )
  411. : null
  412. },
  413. }
  414. const root = nodeOps.createElement('div')
  415. render(h(App), root)
  416. expect(mapRefs()).toMatchObject(['1', '2', '3'])
  417. list.push(4)
  418. await nextTick()
  419. expect(mapRefs()).toMatchObject(['1', '2', '3', '4'])
  420. list.shift()
  421. await nextTick()
  422. expect(mapRefs()).toMatchObject(['2', '3', '4'])
  423. show.value = !show.value
  424. await nextTick()
  425. expect(mapRefs()).toMatchObject([])
  426. show.value = !show.value
  427. await nextTick()
  428. expect(mapRefs()).toMatchObject(['2', '3', '4'])
  429. })
  430. // #6697 v-for ref behaves differently under production and development
  431. test('named ref in v-for , should be responsive when rendering', async () => {
  432. const list = ref([1, 2, 3])
  433. const listRefs = ref([])
  434. const App = {
  435. setup() {
  436. return { listRefs }
  437. },
  438. render() {
  439. return h('div', null, [
  440. h('div', null, String(listRefs.value)),
  441. h(
  442. 'ul',
  443. list.value.map(i =>
  444. h(
  445. 'li',
  446. {
  447. ref: 'listRefs',
  448. ref_for: true,
  449. },
  450. i,
  451. ),
  452. ),
  453. ),
  454. ])
  455. },
  456. }
  457. const root = nodeOps.createElement('div')
  458. render(h(App), root)
  459. await nextTick()
  460. expect(String(listRefs.value)).toBe(
  461. '[object Object],[object Object],[object Object]',
  462. )
  463. expect(serializeInner(root)).toBe(
  464. '<div><div>[object Object],[object Object],[object Object]</div><ul><li>1</li><li>2</li><li>3</li></ul></div>',
  465. )
  466. list.value.splice(0, 1)
  467. await nextTick()
  468. expect(String(listRefs.value)).toBe('[object Object],[object Object]')
  469. expect(serializeInner(root)).toBe(
  470. '<div><div>[object Object],[object Object]</div><ul><li>2</li><li>3</li></ul></div>',
  471. )
  472. })
  473. test('with async component which nested in KeepAlive', async () => {
  474. const AsyncComp = defineAsyncComponent(
  475. () =>
  476. new Promise(resolve =>
  477. setTimeout(() =>
  478. resolve(
  479. defineComponent({
  480. setup(_, { expose }) {
  481. expose({
  482. name: 'AsyncComp',
  483. })
  484. return () => h('div')
  485. },
  486. }) as any,
  487. ),
  488. ),
  489. ),
  490. )
  491. const Comp = defineComponent({
  492. setup(_, { expose }) {
  493. expose({
  494. name: 'Comp',
  495. })
  496. return () => h('div')
  497. },
  498. })
  499. const toggle = ref(false)
  500. const instanceRef = ref<any>(null)
  501. const App = {
  502. render: () => {
  503. return h(KeepAlive, () =>
  504. toggle.value
  505. ? h(AsyncComp, { ref: instanceRef })
  506. : h(Comp, { ref: instanceRef }),
  507. )
  508. },
  509. }
  510. const root = nodeOps.createElement('div')
  511. render(h(App), root)
  512. expect(instanceRef.value.name).toBe('Comp')
  513. // switch to async component
  514. toggle.value = true
  515. await nextTick()
  516. expect(instanceRef.value).toBe(null)
  517. await new Promise(r => setTimeout(r))
  518. expect(instanceRef.value.name).toBe('AsyncComp')
  519. // switch back to normal component
  520. toggle.value = false
  521. await nextTick()
  522. expect(instanceRef.value.name).toBe('Comp')
  523. // switch to async component again
  524. toggle.value = true
  525. await nextTick()
  526. expect(instanceRef.value.name).toBe('AsyncComp')
  527. })
  528. })