2
0

vnode.spec.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. import {
  2. createBlock,
  3. createVNode,
  4. openBlock,
  5. Comment,
  6. Fragment,
  7. Text,
  8. cloneVNode,
  9. mergeProps,
  10. normalizeVNode,
  11. transformVNodeArgs
  12. } from '../src/vnode'
  13. import { Data } from '../src/component'
  14. import { ShapeFlags, PatchFlags } from '@vue/shared'
  15. import { h, reactive, isReactive } from '../src'
  16. import { createApp, nodeOps, serializeInner } from '@vue/runtime-test'
  17. describe('vnode', () => {
  18. test('create with just tag', () => {
  19. const vnode = createVNode('p')
  20. expect(vnode.type).toBe('p')
  21. expect(vnode.props).toBe(null)
  22. })
  23. test('create with tag and props', () => {
  24. const vnode = createVNode('p', {})
  25. expect(vnode.type).toBe('p')
  26. expect(vnode.props).toMatchObject({})
  27. })
  28. test('create with tag, props and children', () => {
  29. const vnode = createVNode('p', {}, ['foo'])
  30. expect(vnode.type).toBe('p')
  31. expect(vnode.props).toMatchObject({})
  32. expect(vnode.children).toMatchObject(['foo'])
  33. })
  34. test('create with 0 as props', () => {
  35. const vnode = createVNode('p', null)
  36. expect(vnode.type).toBe('p')
  37. expect(vnode.props).toBe(null)
  38. })
  39. test('vnode keys', () => {
  40. for (const key of ['', 'a', 0, 1, NaN]) {
  41. expect(createVNode('div', { key }).key).toBe(key)
  42. }
  43. expect(createVNode('div').key).toBe(null)
  44. expect(createVNode('div', { key: undefined }).key).toBe(null)
  45. })
  46. test('create with class component', () => {
  47. class Component {
  48. $props: any
  49. static __vccOpts = { template: '<div />' }
  50. }
  51. const vnode = createVNode(Component)
  52. expect(vnode.type).toEqual(Component.__vccOpts)
  53. })
  54. describe('class normalization', () => {
  55. test('string', () => {
  56. const vnode = createVNode('p', { class: 'foo baz' })
  57. expect(vnode.props).toMatchObject({ class: 'foo baz' })
  58. })
  59. test('array<string>', () => {
  60. const vnode = createVNode('p', { class: ['foo', 'baz'] })
  61. expect(vnode.props).toMatchObject({ class: 'foo baz' })
  62. })
  63. test('array<object>', () => {
  64. const vnode = createVNode('p', {
  65. class: [{ foo: 'foo' }, { baz: 'baz' }]
  66. })
  67. expect(vnode.props).toMatchObject({ class: 'foo baz' })
  68. })
  69. test('object', () => {
  70. const vnode = createVNode('p', { class: { foo: 'foo', baz: 'baz' } })
  71. expect(vnode.props).toMatchObject({ class: 'foo baz' })
  72. })
  73. })
  74. describe('style normalization', () => {
  75. test('array', () => {
  76. const vnode = createVNode('p', {
  77. style: [{ foo: 'foo' }, { baz: 'baz' }]
  78. })
  79. expect(vnode.props).toMatchObject({ style: { foo: 'foo', baz: 'baz' } })
  80. })
  81. test('object', () => {
  82. const vnode = createVNode('p', { style: { foo: 'foo', baz: 'baz' } })
  83. expect(vnode.props).toMatchObject({ style: { foo: 'foo', baz: 'baz' } })
  84. })
  85. })
  86. describe('children normalization', () => {
  87. const nop = jest.fn
  88. test('null', () => {
  89. const vnode = createVNode('p', null, null)
  90. expect(vnode.children).toBe(null)
  91. expect(vnode.shapeFlag).toBe(ShapeFlags.ELEMENT)
  92. })
  93. test('array', () => {
  94. const vnode = createVNode('p', null, ['foo'])
  95. expect(vnode.children).toMatchObject(['foo'])
  96. expect(vnode.shapeFlag).toBe(
  97. ShapeFlags.ELEMENT | ShapeFlags.ARRAY_CHILDREN
  98. )
  99. })
  100. test('object', () => {
  101. const vnode = createVNode('p', null, { foo: 'foo' })
  102. expect(vnode.children).toMatchObject({ foo: 'foo' })
  103. expect(vnode.shapeFlag).toBe(
  104. ShapeFlags.ELEMENT | ShapeFlags.SLOTS_CHILDREN
  105. )
  106. })
  107. test('function', () => {
  108. const vnode = createVNode('p', null, nop)
  109. expect(vnode.children).toMatchObject({ default: nop })
  110. expect(vnode.shapeFlag).toBe(
  111. ShapeFlags.ELEMENT | ShapeFlags.SLOTS_CHILDREN
  112. )
  113. })
  114. test('string', () => {
  115. const vnode = createVNode('p', null, 'foo')
  116. expect(vnode.children).toBe('foo')
  117. expect(vnode.shapeFlag).toBe(
  118. ShapeFlags.ELEMENT | ShapeFlags.TEXT_CHILDREN
  119. )
  120. })
  121. test('element with slots', () => {
  122. const children = [createVNode('span', null, 'hello')]
  123. const vnode = createVNode('div', null, {
  124. default: () => children
  125. })
  126. expect(vnode.children).toBe(children)
  127. expect(vnode.shapeFlag).toBe(
  128. ShapeFlags.ELEMENT | ShapeFlags.ARRAY_CHILDREN
  129. )
  130. })
  131. })
  132. test('normalizeVNode', () => {
  133. // null / undefined -> Comment
  134. expect(normalizeVNode(null)).toMatchObject({ type: Comment })
  135. expect(normalizeVNode(undefined)).toMatchObject({ type: Comment })
  136. // boolean -> Comment
  137. // this is for usage like `someBoolean && h('div')` and behavior consistency
  138. // with 2.x (#574)
  139. expect(normalizeVNode(true)).toMatchObject({ type: Comment })
  140. expect(normalizeVNode(false)).toMatchObject({ type: Comment })
  141. // array -> Fragment
  142. expect(normalizeVNode(['foo'])).toMatchObject({ type: Fragment })
  143. // VNode -> VNode
  144. const vnode = createVNode('div')
  145. expect(normalizeVNode(vnode)).toBe(vnode)
  146. // mounted VNode -> cloned VNode
  147. const mounted = createVNode('div')
  148. mounted.el = {}
  149. const normalized = normalizeVNode(mounted)
  150. expect(normalized).not.toBe(mounted)
  151. expect(normalized).toEqual(mounted)
  152. // primitive types
  153. expect(normalizeVNode('foo')).toMatchObject({ type: Text, children: `foo` })
  154. expect(normalizeVNode(1)).toMatchObject({ type: Text, children: `1` })
  155. })
  156. test('type shapeFlag inference', () => {
  157. expect(createVNode('div').shapeFlag).toBe(ShapeFlags.ELEMENT)
  158. expect(createVNode({}).shapeFlag).toBe(ShapeFlags.STATEFUL_COMPONENT)
  159. expect(createVNode(() => {}).shapeFlag).toBe(
  160. ShapeFlags.FUNCTIONAL_COMPONENT
  161. )
  162. expect(createVNode(Text).shapeFlag).toBe(0)
  163. })
  164. test('cloneVNode', () => {
  165. const node1 = createVNode('div', { foo: 1 }, null)
  166. expect(cloneVNode(node1)).toEqual(node1)
  167. const node2 = createVNode({}, null, [node1])
  168. const cloned2 = cloneVNode(node2)
  169. expect(cloned2).toEqual(node2)
  170. expect(cloneVNode(node2)).toEqual(node2)
  171. expect(cloneVNode(node2)).toEqual(cloned2)
  172. // #1041 should use resolved key/ref
  173. expect(cloneVNode(createVNode('div', { key: 1 })).key).toBe(1)
  174. expect(cloneVNode(createVNode('div', { key: 1 }), { key: 2 }).key).toBe(2)
  175. expect(cloneVNode(createVNode('div'), { key: 2 }).key).toBe(2)
  176. // ref normalizes to [currentRenderingInstance, ref]
  177. expect(cloneVNode(createVNode('div', { ref: 'foo' })).ref).toEqual([
  178. null,
  179. 'foo'
  180. ])
  181. expect(
  182. cloneVNode(createVNode('div', { ref: 'foo' }), { ref: 'bar' }).ref
  183. ).toEqual([null, 'bar'])
  184. expect(cloneVNode(createVNode('div'), { ref: 'bar' }).ref).toEqual([
  185. null,
  186. 'bar'
  187. ])
  188. })
  189. describe('mergeProps', () => {
  190. test('class', () => {
  191. let props1: Data = { class: 'c' }
  192. let props2: Data = { class: ['cc'] }
  193. let props3: Data = { class: [{ ccc: true }] }
  194. let props4: Data = { class: { cccc: true } }
  195. expect(mergeProps(props1, props2, props3, props4)).toMatchObject({
  196. class: 'c cc ccc cccc'
  197. })
  198. })
  199. test('style', () => {
  200. let props1: Data = {
  201. style: {
  202. color: 'red',
  203. fontSize: 10
  204. }
  205. }
  206. let props2: Data = {
  207. style: [
  208. {
  209. color: 'blue',
  210. width: '200px'
  211. },
  212. {
  213. width: '300px',
  214. height: '300px',
  215. fontSize: 30
  216. }
  217. ]
  218. }
  219. expect(mergeProps(props1, props2)).toMatchObject({
  220. style: {
  221. color: 'blue',
  222. width: '300px',
  223. height: '300px',
  224. fontSize: 30
  225. }
  226. })
  227. })
  228. test('style w/ strings', () => {
  229. let props1: Data = {
  230. style: 'width:100px;right:10;top:10'
  231. }
  232. let props2: Data = {
  233. style: [
  234. {
  235. color: 'blue',
  236. width: '200px'
  237. },
  238. {
  239. width: '300px',
  240. height: '300px',
  241. fontSize: 30
  242. }
  243. ]
  244. }
  245. expect(mergeProps(props1, props2)).toMatchObject({
  246. style: {
  247. color: 'blue',
  248. width: '300px',
  249. height: '300px',
  250. fontSize: 30,
  251. right: '10',
  252. top: '10'
  253. }
  254. })
  255. })
  256. test('handlers', () => {
  257. let clickHandler1 = function() {}
  258. let clickHandler2 = function() {}
  259. let focusHandler2 = function() {}
  260. let props1: Data = { onClick: clickHandler1 }
  261. let props2: Data = { onClick: clickHandler2, onFocus: focusHandler2 }
  262. expect(mergeProps(props1, props2)).toMatchObject({
  263. onClick: [clickHandler1, clickHandler2],
  264. onFocus: focusHandler2
  265. })
  266. })
  267. test('default', () => {
  268. let props1: Data = { foo: 'c' }
  269. let props2: Data = { foo: {}, bar: ['cc'] }
  270. let props3: Data = { baz: { ccc: true } }
  271. expect(mergeProps(props1, props2, props3)).toMatchObject({
  272. foo: {},
  273. bar: ['cc'],
  274. baz: { ccc: true }
  275. })
  276. })
  277. })
  278. describe('dynamic children', () => {
  279. test('with patchFlags', () => {
  280. const hoist = createVNode('div')
  281. let vnode1
  282. const vnode = (openBlock(),
  283. createBlock('div', null, [
  284. hoist,
  285. (vnode1 = createVNode('div', null, 'text', PatchFlags.TEXT))
  286. ]))
  287. expect(vnode.dynamicChildren).toStrictEqual([vnode1])
  288. })
  289. test('should not track vnodes with only HYDRATE_EVENTS flag', () => {
  290. const hoist = createVNode('div')
  291. const vnode = (openBlock(),
  292. createBlock('div', null, [
  293. hoist,
  294. createVNode('div', null, 'text', PatchFlags.HYDRATE_EVENTS)
  295. ]))
  296. expect(vnode.dynamicChildren).toStrictEqual([])
  297. })
  298. test('many times call openBlock', () => {
  299. const hoist = createVNode('div')
  300. let vnode1, vnode2, vnode3
  301. const vnode = (openBlock(),
  302. createBlock('div', null, [
  303. hoist,
  304. (vnode1 = createVNode('div', null, 'text', PatchFlags.TEXT)),
  305. (vnode2 = (openBlock(),
  306. createBlock('div', null, [
  307. hoist,
  308. (vnode3 = createVNode('div', null, 'text', PatchFlags.TEXT))
  309. ])))
  310. ]))
  311. expect(vnode.dynamicChildren).toStrictEqual([vnode1, vnode2])
  312. expect(vnode2.dynamicChildren).toStrictEqual([vnode3])
  313. })
  314. test('with stateful component', () => {
  315. const hoist = createVNode('div')
  316. let vnode1
  317. const vnode = (openBlock(),
  318. createBlock('div', null, [
  319. hoist,
  320. (vnode1 = createVNode({}, null, 'text'))
  321. ]))
  322. expect(vnode.dynamicChildren).toStrictEqual([vnode1])
  323. })
  324. test('with functional component', () => {
  325. const hoist = createVNode('div')
  326. let vnode1
  327. const vnode = (openBlock(),
  328. createBlock('div', null, [
  329. hoist,
  330. (vnode1 = createVNode(() => {}, null, 'text'))
  331. ]))
  332. expect(vnode.dynamicChildren).toStrictEqual([vnode1])
  333. })
  334. test('with suspense', () => {
  335. const hoist = createVNode('div')
  336. let vnode1
  337. const vnode = (openBlock(),
  338. createBlock('div', null, [
  339. hoist,
  340. (vnode1 = createVNode(() => {}, null, 'text'))
  341. ]))
  342. expect(vnode.dynamicChildren).toStrictEqual([vnode1])
  343. })
  344. // #1039
  345. // <component :is="foo">{{ bar }}</component>
  346. // - content is compiled as slot
  347. // - dynamic component resolves to plain element, but as a block
  348. // - block creation disables its own tracking, accidentally causing the
  349. // slot content (called during the block node creation) to be missed
  350. test('element block should track normalized slot children', () => {
  351. const hoist = createVNode('div')
  352. let vnode1
  353. const vnode = (openBlock(),
  354. createBlock('div', null, {
  355. default: () => {
  356. return [
  357. hoist,
  358. (vnode1 = createVNode('div', null, 'text', PatchFlags.TEXT))
  359. ]
  360. }
  361. }))
  362. expect(vnode.dynamicChildren).toStrictEqual([vnode1])
  363. })
  364. test('openBlock w/ disableTracking: true', () => {
  365. const hoist = createVNode('div')
  366. let vnode1
  367. const vnode = (openBlock(),
  368. createBlock('div', null, [
  369. // a v-for fragment block generated by the compiler
  370. // disables tracking because it always diffs its
  371. // children.
  372. (vnode1 = (openBlock(true),
  373. createBlock(Fragment, null, [
  374. hoist,
  375. /*vnode2*/ createVNode(() => {}, null, 'text')
  376. ])))
  377. ]))
  378. expect(vnode.dynamicChildren).toStrictEqual([vnode1])
  379. expect(vnode1.dynamicChildren).toStrictEqual([])
  380. })
  381. test('openBlock without disableTracking: true', () => {
  382. const hoist = createVNode('div')
  383. let vnode1, vnode2
  384. const vnode = (openBlock(),
  385. createBlock('div', null, [
  386. (vnode1 = (openBlock(),
  387. createBlock(Fragment, null, [
  388. hoist,
  389. (vnode2 = createVNode(() => {}, null, 'text'))
  390. ])))
  391. ]))
  392. expect(vnode.dynamicChildren).toStrictEqual([vnode1])
  393. expect(vnode1.dynamicChildren).toStrictEqual([vnode2])
  394. })
  395. })
  396. describe('transformVNodeArgs', () => {
  397. afterEach(() => {
  398. // reset
  399. transformVNodeArgs()
  400. })
  401. test('no-op pass through', () => {
  402. transformVNodeArgs(args => args)
  403. const vnode = createVNode('div', { id: 'foo' }, 'hello')
  404. expect(vnode).toMatchObject({
  405. type: 'div',
  406. props: { id: 'foo' },
  407. children: 'hello',
  408. shapeFlag: ShapeFlags.ELEMENT | ShapeFlags.TEXT_CHILDREN
  409. })
  410. })
  411. test('direct override', () => {
  412. transformVNodeArgs(() => ['div', { id: 'foo' }, 'hello'])
  413. const vnode = createVNode('p')
  414. expect(vnode).toMatchObject({
  415. type: 'div',
  416. props: { id: 'foo' },
  417. children: 'hello',
  418. shapeFlag: ShapeFlags.ELEMENT | ShapeFlags.TEXT_CHILDREN
  419. })
  420. })
  421. test('receive component instance as 2nd arg', () => {
  422. transformVNodeArgs((args, instance) => {
  423. if (instance) {
  424. return ['h1', null, instance.type.name]
  425. } else {
  426. return args
  427. }
  428. })
  429. const App = {
  430. // this will be the name of the component in the h1
  431. name: 'Root Component',
  432. render() {
  433. return h('p') // this will be overwritten by the transform
  434. }
  435. }
  436. const root = nodeOps.createElement('div')
  437. createApp(App).mount(root)
  438. expect(serializeInner(root)).toBe('<h1>Root Component</h1>')
  439. })
  440. test('should not be observable', () => {
  441. const a = createVNode('div')
  442. const b = reactive(a)
  443. expect(b).toBe(a)
  444. expect(isReactive(b)).toBe(false)
  445. })
  446. })
  447. })