vnode.spec.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777
  1. import {
  2. Comment,
  3. Fragment,
  4. Text,
  5. type VNode,
  6. VaporSlot,
  7. cloneVNode,
  8. createBlock,
  9. createVNode,
  10. isBlockTreeEnabled,
  11. mergeProps,
  12. normalizeVNode,
  13. openBlock,
  14. transformVNodeArgs,
  15. } from '../src/vnode'
  16. import { PatchFlags, ShapeFlags } from '@vue/shared'
  17. import type { Data } from '../src/component'
  18. import { h, isReactive, reactive, ref, setBlockTracking, withCtx } from '../src'
  19. import { createApp, nodeOps, serializeInner } from '@vue/runtime-test'
  20. import { setCurrentRenderingInstance } from '../src/componentRenderContext'
  21. describe('vnode', () => {
  22. test('create with just tag', () => {
  23. const vnode = createVNode('p')
  24. expect(vnode.type).toBe('p')
  25. expect(vnode.props).toBe(null)
  26. })
  27. test('create with tag and props', () => {
  28. const vnode = createVNode('p', {})
  29. expect(vnode.type).toBe('p')
  30. expect(vnode.props).toMatchObject({})
  31. })
  32. test('create with tag, props and children', () => {
  33. const vnode = createVNode('p', {}, ['foo'])
  34. expect(vnode.type).toBe('p')
  35. expect(vnode.props).toMatchObject({})
  36. expect(vnode.children).toMatchObject(['foo'])
  37. })
  38. test('create with 0 as props', () => {
  39. const vnode = createVNode('p', null)
  40. expect(vnode.type).toBe('p')
  41. expect(vnode.props).toBe(null)
  42. })
  43. test('show warn when create with invalid type', () => {
  44. const vnode = createVNode('')
  45. expect('Invalid vnode type when creating vnode').toHaveBeenWarned()
  46. expect(vnode.type).toBe(Comment)
  47. })
  48. test('create from an existing vnode', () => {
  49. const vnode1 = createVNode('p', { id: 'foo' })
  50. const vnode2 = createVNode(vnode1, { class: 'bar' }, 'baz')
  51. expect(vnode2).toMatchObject({
  52. type: 'p',
  53. props: {
  54. id: 'foo',
  55. class: 'bar',
  56. },
  57. children: 'baz',
  58. shapeFlag: ShapeFlags.ELEMENT | ShapeFlags.TEXT_CHILDREN,
  59. })
  60. })
  61. test('create from an existing text vnode', () => {
  62. const vnode1 = createVNode('div', null, 'text', PatchFlags.TEXT)
  63. const vnode2 = createVNode(vnode1)
  64. expect(vnode2).toMatchObject({
  65. type: 'div',
  66. patchFlag: PatchFlags.BAIL,
  67. children: 'text',
  68. shapeFlag: ShapeFlags.ELEMENT | ShapeFlags.TEXT_CHILDREN,
  69. })
  70. })
  71. test('vnode keys', () => {
  72. for (const key of ['', 'a', 0, 1, NaN]) {
  73. expect(createVNode('div', { key }).key).toBe(key)
  74. }
  75. expect(createVNode('div').key).toBe(null)
  76. expect(createVNode('div', { key: undefined }).key).toBe(null)
  77. expect(`VNode created with invalid key (NaN)`).toHaveBeenWarned()
  78. })
  79. test('create with class component', () => {
  80. class Component {
  81. $props: any
  82. static __vccOpts = { template: '<div />' }
  83. }
  84. const vnode = createVNode(Component)
  85. expect(vnode.type).toEqual(Component.__vccOpts)
  86. })
  87. describe('class normalization', () => {
  88. test('string', () => {
  89. const vnode = createVNode('p', { class: 'foo baz' })
  90. expect(vnode.props).toMatchObject({ class: 'foo baz' })
  91. })
  92. test('array<string>', () => {
  93. const vnode = createVNode('p', { class: ['foo', 'baz'] })
  94. expect(vnode.props).toMatchObject({ class: 'foo baz' })
  95. })
  96. test('array<object>', () => {
  97. const vnode = createVNode('p', {
  98. class: [{ foo: 'foo' }, { baz: 'baz' }],
  99. })
  100. expect(vnode.props).toMatchObject({ class: 'foo baz' })
  101. })
  102. test('object', () => {
  103. const vnode = createVNode('p', { class: { foo: 'foo', baz: 'baz' } })
  104. expect(vnode.props).toMatchObject({ class: 'foo baz' })
  105. })
  106. })
  107. describe('style normalization', () => {
  108. test('array', () => {
  109. const vnode = createVNode('p', {
  110. style: [{ foo: 'foo' }, { baz: 'baz' }],
  111. })
  112. expect(vnode.props).toMatchObject({ style: { foo: 'foo', baz: 'baz' } })
  113. })
  114. test('object', () => {
  115. const vnode = createVNode('p', { style: { foo: 'foo', baz: 'baz' } })
  116. expect(vnode.props).toMatchObject({ style: { foo: 'foo', baz: 'baz' } })
  117. })
  118. })
  119. describe('children normalization', () => {
  120. const nop = vi.fn
  121. test('null', () => {
  122. const vnode = createVNode('p', null, null)
  123. expect(vnode.children).toBe(null)
  124. expect(vnode.shapeFlag).toBe(ShapeFlags.ELEMENT)
  125. })
  126. test('array', () => {
  127. const vnode = createVNode('p', null, ['foo'])
  128. expect(vnode.children).toMatchObject(['foo'])
  129. expect(vnode.shapeFlag).toBe(
  130. ShapeFlags.ELEMENT | ShapeFlags.ARRAY_CHILDREN,
  131. )
  132. })
  133. test('object', () => {
  134. const vnode = createVNode({}, null, { foo: 'foo' })
  135. expect(vnode.children).toMatchObject({ foo: 'foo' })
  136. expect(vnode.shapeFlag).toBe(
  137. ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.SLOTS_CHILDREN,
  138. )
  139. })
  140. test('function', () => {
  141. const vnode = createVNode('p', null, nop)
  142. expect(vnode.children).toMatchObject({ default: nop })
  143. expect(vnode.shapeFlag).toBe(
  144. ShapeFlags.ELEMENT | ShapeFlags.SLOTS_CHILDREN,
  145. )
  146. })
  147. test('string', () => {
  148. const vnode = createVNode('p', null, 'foo')
  149. expect(vnode.children).toBe('foo')
  150. expect(vnode.shapeFlag).toBe(
  151. ShapeFlags.ELEMENT | ShapeFlags.TEXT_CHILDREN,
  152. )
  153. })
  154. test('element with slots', () => {
  155. const children = [createVNode('span', null, 'hello')]
  156. const vnode = createVNode('div', null, {
  157. default: () => children,
  158. })
  159. expect(vnode.children).toBe(children)
  160. expect(vnode.shapeFlag).toBe(
  161. ShapeFlags.ELEMENT | ShapeFlags.ARRAY_CHILDREN,
  162. )
  163. })
  164. })
  165. test('normalizeVNode', () => {
  166. // null / undefined -> Comment
  167. expect(normalizeVNode(null)).toMatchObject({ type: Comment })
  168. expect(normalizeVNode(undefined)).toMatchObject({ type: Comment })
  169. // boolean -> Comment
  170. // this is for usage like `someBoolean && h('div')` and behavior consistency
  171. // with 2.x (#574)
  172. expect(normalizeVNode(true)).toMatchObject({ type: Comment })
  173. expect(normalizeVNode(false)).toMatchObject({ type: Comment })
  174. // array -> Fragment
  175. expect(normalizeVNode(['foo'])).toMatchObject({ type: Fragment })
  176. // VNode -> VNode
  177. const vnode = createVNode('div')
  178. expect(normalizeVNode(vnode)).toBe(vnode)
  179. // mounted VNode -> cloned VNode
  180. const mounted = createVNode('div')
  181. mounted.el = {}
  182. const normalized = normalizeVNode(mounted)
  183. expect(normalized).not.toBe(mounted)
  184. expect(normalized).toEqual(mounted)
  185. // primitive types
  186. expect(normalizeVNode('foo')).toMatchObject({ type: Text, children: `foo` })
  187. expect(normalizeVNode(1)).toMatchObject({ type: Text, children: `1` })
  188. })
  189. test('type shapeFlag inference', () => {
  190. expect(createVNode('div').shapeFlag).toBe(ShapeFlags.ELEMENT)
  191. expect(createVNode({}).shapeFlag).toBe(ShapeFlags.STATEFUL_COMPONENT)
  192. expect(createVNode(() => {}).shapeFlag).toBe(
  193. ShapeFlags.FUNCTIONAL_COMPONENT,
  194. )
  195. expect(createVNode(Text).shapeFlag).toBe(0)
  196. })
  197. test('cloneVNode', () => {
  198. const node1 = createVNode('div', { foo: 1 }, null)
  199. expect(cloneVNode(node1)).toEqual(node1)
  200. const node2 = createVNode({}, null, [node1])
  201. const cloned2 = cloneVNode(node2)
  202. expect(cloned2).toEqual(node2)
  203. expect(cloneVNode(node2)).toEqual(cloned2)
  204. })
  205. test('cloneVNode preserves vapor slot metadata', () => {
  206. const node = createVNode(VaporSlot as any)
  207. const viHook = vi.fn()
  208. const outletFallback = () => []
  209. const slotRef = {} as any
  210. const slotScope = {} as any
  211. const slotMeta = {
  212. slot: () => [],
  213. fallback: () => [],
  214. outletFallback,
  215. state: { localFallback: 'fallback state' },
  216. ref: slotRef,
  217. scope: slotScope,
  218. }
  219. const slotBlock = { block: true }
  220. node.vi = viHook
  221. node.vs = slotMeta as any
  222. node.vb = slotBlock as any
  223. const cloned = cloneVNode(node)
  224. expect(cloned.vi).toBe(viHook)
  225. expect(cloned.vs).not.toBe(slotMeta)
  226. expect(cloned.vs!.slot).toBe(slotMeta.slot)
  227. expect(cloned.vs!.fallback).toBe(slotMeta.fallback)
  228. expect(cloned.vs!.outletFallback).toBe(outletFallback)
  229. expect(cloned.vs!.state).toBeUndefined()
  230. expect(cloned.vs!.ref).toBeUndefined()
  231. expect(cloned.vs!.scope).toBeUndefined()
  232. expect(cloned.vb).toBe(slotBlock)
  233. })
  234. test('cloneVNode keeps mounted vapor slot runtime state', () => {
  235. const node = createVNode(VaporSlot as any)
  236. const slotRef = {} as any
  237. const slotScope = {} as any
  238. const slotState = { localFallback: 'fallback state' }
  239. node.el = {} as any
  240. node.vs = {
  241. slot: () => [],
  242. fallback: () => [],
  243. state: slotState,
  244. ref: slotRef,
  245. scope: slotScope,
  246. } as any
  247. const cloned = cloneVNode(node)
  248. expect(cloned.vs).not.toBe(node.vs)
  249. expect(cloned.vs!.state).toBe(slotState)
  250. expect(cloned.vs!.ref).toBe(slotRef)
  251. expect(cloned.vs!.scope).toBe(slotScope)
  252. })
  253. test('cloneVNode key normalization', () => {
  254. // #1041 should use resolved key/ref
  255. expect(cloneVNode(createVNode('div', { key: 1 })).key).toBe(1)
  256. expect(cloneVNode(createVNode('div', { key: 1 }), { key: 2 }).key).toBe(2)
  257. expect(cloneVNode(createVNode('div'), { key: 2 }).key).toBe(2)
  258. })
  259. // ref normalizes to [currentRenderingInstance, ref]
  260. test('cloneVNode ref normalization', () => {
  261. const mockInstance1 = { type: {} } as any
  262. const mockInstance2 = { type: {} } as any
  263. setCurrentRenderingInstance(mockInstance1)
  264. const original = createVNode('div', { ref: 'foo' })
  265. expect(original.ref).toMatchObject({
  266. i: mockInstance1,
  267. r: 'foo',
  268. f: false,
  269. })
  270. // clone and preserve original ref
  271. const cloned1 = cloneVNode(original)
  272. expect(cloned1.ref).toMatchObject({ i: mockInstance1, r: 'foo', f: false })
  273. // cloning with new ref, but with same context instance
  274. const cloned2 = cloneVNode(original, { ref: 'bar' })
  275. expect(cloned2.ref).toMatchObject({ i: mockInstance1, r: 'bar', f: false })
  276. // cloning and adding ref to original that has no ref
  277. const original2 = createVNode('div')
  278. const cloned3 = cloneVNode(original2, { ref: 'bar' })
  279. expect(cloned3.ref).toMatchObject({ i: mockInstance1, r: 'bar', f: false })
  280. // cloning with different context instance
  281. setCurrentRenderingInstance(mockInstance2)
  282. // clone and preserve original ref
  283. const cloned4 = cloneVNode(original)
  284. // #1311 should preserve original context instance!
  285. expect(cloned4.ref).toMatchObject({ i: mockInstance1, r: 'foo', f: false })
  286. // cloning with new ref, but with same context instance
  287. const cloned5 = cloneVNode(original, { ref: 'bar' })
  288. // new ref should use current context instance and overwrite original
  289. expect(cloned5.ref).toMatchObject({ i: mockInstance2, r: 'bar', f: false })
  290. // cloning and adding ref to original that has no ref
  291. const cloned6 = cloneVNode(original2, { ref: 'bar' })
  292. expect(cloned6.ref).toMatchObject({ i: mockInstance2, r: 'bar', f: false })
  293. const original3 = createVNode('div', { ref: 'foo', ref_for: true })
  294. expect(original3.ref).toMatchObject({
  295. i: mockInstance2,
  296. r: 'foo',
  297. f: true,
  298. })
  299. const cloned7 = cloneVNode(original3, { ref: 'bar', ref_for: true })
  300. expect(cloned7.ref).toMatchObject({ i: mockInstance2, r: 'bar', f: true })
  301. const r = ref()
  302. const original4 = createVNode('div', { ref: r, ref_key: 'foo' })
  303. expect(original4.ref).toMatchObject({
  304. i: mockInstance2,
  305. r,
  306. k: 'foo',
  307. })
  308. const cloned8 = cloneVNode(original4)
  309. expect(cloned8.ref).toMatchObject({ i: mockInstance2, r, k: 'foo' })
  310. // @ts-expect-error #8230
  311. const original5 = createVNode('div', { ref: 111, ref_key: 'foo' })
  312. expect(original5.ref).toMatchObject({
  313. i: mockInstance2,
  314. r: '111',
  315. k: 'foo',
  316. })
  317. const cloned9 = cloneVNode(original5)
  318. expect(cloned9.ref).toMatchObject({ i: mockInstance2, r: '111', k: 'foo' })
  319. setCurrentRenderingInstance(null)
  320. })
  321. test('cloneVNode ref merging', () => {
  322. const mockInstance1 = { type: {} } as any
  323. const mockInstance2 = { type: {} } as any
  324. setCurrentRenderingInstance(mockInstance1)
  325. const original = createVNode('div', { ref: 'foo' })
  326. expect(original.ref).toMatchObject({ i: mockInstance1, r: 'foo', f: false })
  327. // clone and preserve original ref
  328. setCurrentRenderingInstance(mockInstance2)
  329. const cloned1 = cloneVNode(original, { ref: 'bar' }, true)
  330. expect(cloned1.ref).toMatchObject([
  331. { i: mockInstance1, r: 'foo', f: false },
  332. { i: mockInstance2, r: 'bar', f: false },
  333. ])
  334. setCurrentRenderingInstance(null)
  335. })
  336. test('cloneVNode class normalization', () => {
  337. const vnode = createVNode('div')
  338. const expectedProps = {
  339. class: 'a b',
  340. }
  341. expect(cloneVNode(vnode, { class: 'a b' }).props).toMatchObject(
  342. expectedProps,
  343. )
  344. expect(cloneVNode(vnode, { class: ['a', 'b'] }).props).toMatchObject(
  345. expectedProps,
  346. )
  347. expect(
  348. cloneVNode(vnode, { class: { a: true, b: true } }).props,
  349. ).toMatchObject(expectedProps)
  350. expect(
  351. cloneVNode(vnode, { class: [{ a: true, b: true }] }).props,
  352. ).toMatchObject(expectedProps)
  353. })
  354. test('cloneVNode style normalization', () => {
  355. const vnode = createVNode('div')
  356. const expectedProps = {
  357. style: {
  358. color: 'blue',
  359. width: '300px',
  360. },
  361. }
  362. expect(
  363. cloneVNode(vnode, { style: 'color: blue; width: 300px;' }).props,
  364. ).toMatchObject(expectedProps)
  365. expect(
  366. cloneVNode(vnode, {
  367. style: {
  368. color: 'blue',
  369. width: '300px',
  370. },
  371. }).props,
  372. ).toMatchObject(expectedProps)
  373. expect(
  374. cloneVNode(vnode, {
  375. style: [
  376. {
  377. color: 'blue',
  378. width: '300px',
  379. },
  380. ],
  381. }).props,
  382. ).toMatchObject(expectedProps)
  383. })
  384. describe('mergeProps', () => {
  385. test('class', () => {
  386. let props1: Data = { class: { c: true } }
  387. let props2: Data = { class: ['cc'] }
  388. let props3: Data = { class: [{ ccc: true }] }
  389. let props4: Data = { class: { cccc: true } }
  390. expect(mergeProps(props1, props2, props3, props4)).toMatchObject({
  391. class: 'c cc ccc cccc',
  392. })
  393. })
  394. test('style', () => {
  395. let props1: Data = {
  396. style: [
  397. {
  398. color: 'red',
  399. fontSize: 10,
  400. },
  401. ],
  402. }
  403. let props2: Data = {
  404. style: [
  405. {
  406. color: 'blue',
  407. width: '200px',
  408. },
  409. {
  410. width: '300px',
  411. height: '300px',
  412. fontSize: 30,
  413. },
  414. ],
  415. }
  416. expect(mergeProps(props1, props2)).toMatchObject({
  417. style: {
  418. color: 'blue',
  419. width: '300px',
  420. height: '300px',
  421. fontSize: 30,
  422. },
  423. })
  424. })
  425. test('style w/ strings', () => {
  426. let props1: Data = {
  427. style: 'width:100px;right:10;top:10',
  428. }
  429. let props2: Data = {
  430. style: [
  431. {
  432. color: 'blue',
  433. width: '200px',
  434. },
  435. {
  436. width: '300px',
  437. height: '300px',
  438. fontSize: 30,
  439. },
  440. ],
  441. }
  442. expect(mergeProps(props1, props2)).toMatchObject({
  443. style: {
  444. color: 'blue',
  445. width: '300px',
  446. height: '300px',
  447. fontSize: 30,
  448. right: '10',
  449. top: '10',
  450. },
  451. })
  452. })
  453. test('handlers', () => {
  454. let clickHandler1 = function () {}
  455. let clickHandler2 = function () {}
  456. let focusHandler2 = function () {}
  457. let props1: Data = { onClick: clickHandler1 }
  458. let props2: Data = { onClick: clickHandler2, onFocus: focusHandler2 }
  459. expect(mergeProps(props1, props2)).toMatchObject({
  460. onClick: [clickHandler1, clickHandler2],
  461. onFocus: focusHandler2,
  462. })
  463. let props3: Data = { onClick: undefined }
  464. expect(mergeProps(props1, props3)).toMatchObject({
  465. onClick: clickHandler1,
  466. })
  467. const props4: Data = { onClick: undefined }
  468. expect(mergeProps(props4)).toHaveProperty('onClick', undefined)
  469. expect(mergeProps({ onClick: null })).toMatchObject({
  470. onClick: null,
  471. })
  472. expect(
  473. mergeProps({ 'onUpdate:modelValue': undefined }),
  474. ).not.toHaveProperty('onUpdate:modelValue')
  475. expect(mergeProps({ 'onUpdate:modelValue': null })).not.toHaveProperty(
  476. 'onUpdate:modelValue',
  477. )
  478. })
  479. test('default', () => {
  480. let props1: Data = { foo: 'c' }
  481. let props2: Data = { foo: {}, bar: ['cc'] }
  482. let props3: Data = { baz: { ccc: true } }
  483. expect(mergeProps(props1, props2, props3)).toMatchObject({
  484. foo: {},
  485. bar: ['cc'],
  486. baz: { ccc: true },
  487. })
  488. })
  489. })
  490. describe('dynamic children', () => {
  491. test('with patchFlags', () => {
  492. const hoist = createVNode('div')
  493. let vnode1
  494. const vnode =
  495. (openBlock(),
  496. createBlock('div', null, [
  497. hoist,
  498. (vnode1 = createVNode('div', null, 'text', PatchFlags.TEXT)),
  499. ]))
  500. expect(vnode.dynamicChildren).toStrictEqual([vnode1])
  501. })
  502. test('should not track vnodes with only NEED_HYDRATION flag', () => {
  503. const hoist = createVNode('div')
  504. const vnode =
  505. (openBlock(),
  506. createBlock('div', null, [
  507. hoist,
  508. createVNode('div', null, 'text', PatchFlags.NEED_HYDRATION),
  509. ]))
  510. expect(vnode.dynamicChildren).toStrictEqual([])
  511. })
  512. test('many times call openBlock', () => {
  513. const hoist = createVNode('div')
  514. let vnode1, vnode2, vnode3
  515. const vnode =
  516. (openBlock(),
  517. createBlock('div', null, [
  518. hoist,
  519. (vnode1 = createVNode('div', null, 'text', PatchFlags.TEXT)),
  520. (vnode2 =
  521. (openBlock(),
  522. createBlock('div', null, [
  523. hoist,
  524. (vnode3 = createVNode('div', null, 'text', PatchFlags.TEXT)),
  525. ]))),
  526. ]))
  527. expect(vnode.dynamicChildren).toStrictEqual([vnode1, vnode2])
  528. expect(vnode2.dynamicChildren).toStrictEqual([vnode3])
  529. })
  530. test('with stateful component', () => {
  531. const hoist = createVNode('div')
  532. let vnode1
  533. const vnode =
  534. (openBlock(),
  535. createBlock('div', null, [
  536. hoist,
  537. (vnode1 = createVNode({}, null, 'text')),
  538. ]))
  539. expect(vnode.dynamicChildren).toStrictEqual([vnode1])
  540. })
  541. test('with functional component', () => {
  542. const hoist = createVNode('div')
  543. let vnode1
  544. const vnode =
  545. (openBlock(),
  546. createBlock('div', null, [
  547. hoist,
  548. (vnode1 = createVNode(() => {}, null, 'text')),
  549. ]))
  550. expect(vnode.dynamicChildren).toStrictEqual([vnode1])
  551. })
  552. // #1039
  553. // <component :is="foo">{{ bar }}</component>
  554. // - content is compiled as slot
  555. // - dynamic component resolves to plain element, but as a block
  556. // - block creation disables its own tracking, accidentally causing the
  557. // slot content (called during the block node creation) to be missed
  558. test('element block should track normalized slot children', () => {
  559. const hoist = createVNode('div')
  560. let vnode1: any
  561. const vnode =
  562. (openBlock(),
  563. createBlock('div', null, {
  564. default: () => {
  565. return [
  566. hoist,
  567. (vnode1 = createVNode('div', null, 'text', PatchFlags.TEXT)),
  568. ]
  569. },
  570. }))
  571. expect(vnode.dynamicChildren).toStrictEqual([vnode1])
  572. })
  573. test('openBlock w/ disableTracking: true', () => {
  574. const hoist = createVNode('div')
  575. let vnode1
  576. const vnode =
  577. (openBlock(),
  578. createBlock('div', null, [
  579. // a v-for fragment block generated by the compiler
  580. // disables tracking because it always diffs its
  581. // children.
  582. (vnode1 =
  583. (openBlock(true),
  584. createBlock(Fragment, null, [
  585. hoist,
  586. /*vnode2*/ createVNode(() => {}, null, 'text'),
  587. ]))),
  588. ]))
  589. expect(vnode.dynamicChildren).toStrictEqual([vnode1])
  590. expect(vnode1.dynamicChildren).toStrictEqual([])
  591. })
  592. test('openBlock without disableTracking: true', () => {
  593. const hoist = createVNode('div')
  594. let vnode1, vnode2
  595. const vnode =
  596. (openBlock(),
  597. createBlock('div', null, [
  598. (vnode1 =
  599. (openBlock(),
  600. createBlock(Fragment, null, [
  601. hoist,
  602. (vnode2 = createVNode(() => {}, null, 'text')),
  603. ]))),
  604. ]))
  605. expect(vnode.dynamicChildren).toStrictEqual([vnode1])
  606. expect(vnode1.dynamicChildren).toStrictEqual([vnode2])
  607. })
  608. test('should not track openBlock() when tracking is disabled', () => {
  609. let vnode1
  610. const vnode =
  611. (openBlock(),
  612. createBlock('div', null, [
  613. setBlockTracking(-1, true),
  614. (vnode1 = (openBlock(), createBlock('div'))),
  615. setBlockTracking(1),
  616. vnode1,
  617. ]))
  618. const expected: VNode['dynamicChildren'] = []
  619. expected.hasOnce = true
  620. expect(vnode.dynamicChildren).toStrictEqual(expected)
  621. })
  622. // #5657
  623. test('error of slot function execution should not affect block tracking', () => {
  624. expect(isBlockTreeEnabled).toStrictEqual(1)
  625. const slotFn = withCtx(
  626. () => {
  627. throw new Error('slot execution error')
  628. },
  629. { type: {}, appContext: {} } as any,
  630. )
  631. const Parent = {
  632. setup(_: any, { slots }: any) {
  633. return () => {
  634. try {
  635. slots.default()
  636. } catch (e) {}
  637. }
  638. },
  639. }
  640. const vnode =
  641. (openBlock(), createBlock(Parent, null, { default: slotFn }))
  642. createApp(vnode).mount(nodeOps.createElement('div'))
  643. expect(isBlockTreeEnabled).toStrictEqual(1)
  644. })
  645. })
  646. describe('transformVNodeArgs', () => {
  647. afterEach(() => {
  648. // reset
  649. transformVNodeArgs()
  650. })
  651. test('no-op pass through', () => {
  652. transformVNodeArgs(args => args)
  653. const vnode = createVNode('div', { id: 'foo' }, 'hello')
  654. expect(vnode).toMatchObject({
  655. type: 'div',
  656. props: { id: 'foo' },
  657. children: 'hello',
  658. shapeFlag: ShapeFlags.ELEMENT | ShapeFlags.TEXT_CHILDREN,
  659. })
  660. })
  661. test('direct override', () => {
  662. transformVNodeArgs(() => ['div', { id: 'foo' }, 'hello'])
  663. const vnode = createVNode('p')
  664. expect(vnode).toMatchObject({
  665. type: 'div',
  666. props: { id: 'foo' },
  667. children: 'hello',
  668. shapeFlag: ShapeFlags.ELEMENT | ShapeFlags.TEXT_CHILDREN,
  669. })
  670. })
  671. test('receive component instance as 2nd arg', () => {
  672. transformVNodeArgs((args, instance) => {
  673. if (instance) {
  674. return ['h1', null, instance.type.name]
  675. } else {
  676. return args
  677. }
  678. })
  679. const App = {
  680. // this will be the name of the component in the h1
  681. name: 'Root Component',
  682. render() {
  683. return h('p') // this will be overwritten by the transform
  684. },
  685. }
  686. const root = nodeOps.createElement('div')
  687. createApp(App).mount(root)
  688. expect(serializeInner(root)).toBe('<h1>Root Component</h1>')
  689. })
  690. test('should not be observable', () => {
  691. const a = createVNode('div')
  692. const b = reactive(a)
  693. expect(b).toBe(a)
  694. expect(isReactive(b)).toBe(false)
  695. })
  696. })
  697. })