vnode.spec.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  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 } 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 reoslved 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('handlers', () => {
  229. let clickHander1 = function() {}
  230. let clickHander2 = function() {}
  231. let focusHander2 = function() {}
  232. let props1: Data = { onClick: clickHander1 }
  233. let props2: Data = { onClick: clickHander2, onFocus: focusHander2 }
  234. expect(mergeProps(props1, props2)).toMatchObject({
  235. onClick: [clickHander1, clickHander2],
  236. onFocus: focusHander2
  237. })
  238. })
  239. test('default', () => {
  240. let props1: Data = { foo: 'c' }
  241. let props2: Data = { foo: {}, bar: ['cc'] }
  242. let props3: Data = { baz: { ccc: true } }
  243. expect(mergeProps(props1, props2, props3)).toMatchObject({
  244. foo: {},
  245. bar: ['cc'],
  246. baz: { ccc: true }
  247. })
  248. })
  249. })
  250. describe('dynamic children', () => {
  251. test('with patchFlags', () => {
  252. const hoist = createVNode('div')
  253. let vnode1
  254. const vnode = (openBlock(),
  255. createBlock('div', null, [
  256. hoist,
  257. (vnode1 = createVNode('div', null, 'text', PatchFlags.TEXT))
  258. ]))
  259. expect(vnode.dynamicChildren).toStrictEqual([vnode1])
  260. })
  261. test('should not track vnodes with only HYDRATE_EVENTS flag', () => {
  262. const hoist = createVNode('div')
  263. const vnode = (openBlock(),
  264. createBlock('div', null, [
  265. hoist,
  266. createVNode('div', null, 'text', PatchFlags.HYDRATE_EVENTS)
  267. ]))
  268. expect(vnode.dynamicChildren).toStrictEqual([])
  269. })
  270. test('many times call openBlock', () => {
  271. const hoist = createVNode('div')
  272. let vnode1, vnode2, vnode3
  273. const vnode = (openBlock(),
  274. createBlock('div', null, [
  275. hoist,
  276. (vnode1 = createVNode('div', null, 'text', PatchFlags.TEXT)),
  277. (vnode2 = (openBlock(),
  278. createBlock('div', null, [
  279. hoist,
  280. (vnode3 = createVNode('div', null, 'text', PatchFlags.TEXT))
  281. ])))
  282. ]))
  283. expect(vnode.dynamicChildren).toStrictEqual([vnode1, vnode2])
  284. expect(vnode2.dynamicChildren).toStrictEqual([vnode3])
  285. })
  286. test('with stateful component', () => {
  287. const hoist = createVNode('div')
  288. let vnode1
  289. const vnode = (openBlock(),
  290. createBlock('div', null, [
  291. hoist,
  292. (vnode1 = createVNode({}, null, 'text'))
  293. ]))
  294. expect(vnode.dynamicChildren).toStrictEqual([vnode1])
  295. })
  296. test('with functional component', () => {
  297. const hoist = createVNode('div')
  298. let vnode1
  299. const vnode = (openBlock(),
  300. createBlock('div', null, [
  301. hoist,
  302. (vnode1 = createVNode(() => {}, null, 'text'))
  303. ]))
  304. expect(vnode.dynamicChildren).toStrictEqual([vnode1])
  305. })
  306. test('with suspense', () => {
  307. const hoist = createVNode('div')
  308. let vnode1
  309. const vnode = (openBlock(),
  310. createBlock('div', null, [
  311. hoist,
  312. (vnode1 = createVNode(() => {}, null, 'text'))
  313. ]))
  314. expect(vnode.dynamicChildren).toStrictEqual([vnode1])
  315. })
  316. // #1039
  317. // <component :is="foo">{{ bar }}</component>
  318. // - content is compiled as slot
  319. // - dynamic component reoslves to plain element, but as a block
  320. // - block creation disables its own tracking, accidentally causing the
  321. // slot content (called during the block node creation) to be missed
  322. test('element block should track normalized slot children', () => {
  323. const hoist = createVNode('div')
  324. let vnode1
  325. const vnode = (openBlock(),
  326. createBlock('div', null, {
  327. default: () => {
  328. return [
  329. hoist,
  330. (vnode1 = createVNode('div', null, 'text', PatchFlags.TEXT))
  331. ]
  332. }
  333. }))
  334. expect(vnode.dynamicChildren).toStrictEqual([vnode1])
  335. })
  336. })
  337. describe('transformVNodeArgs', () => {
  338. afterEach(() => {
  339. // reset
  340. transformVNodeArgs()
  341. })
  342. test('no-op pass through', () => {
  343. transformVNodeArgs(args => args)
  344. const vnode = createVNode('div', { id: 'foo' }, 'hello')
  345. expect(vnode).toMatchObject({
  346. type: 'div',
  347. props: { id: 'foo' },
  348. children: 'hello',
  349. shapeFlag: ShapeFlags.ELEMENT | ShapeFlags.TEXT_CHILDREN
  350. })
  351. })
  352. test('direct override', () => {
  353. transformVNodeArgs(() => ['div', { id: 'foo' }, 'hello'])
  354. const vnode = createVNode('p')
  355. expect(vnode).toMatchObject({
  356. type: 'div',
  357. props: { id: 'foo' },
  358. children: 'hello',
  359. shapeFlag: ShapeFlags.ELEMENT | ShapeFlags.TEXT_CHILDREN
  360. })
  361. })
  362. test('receive component instance as 2nd arg', () => {
  363. transformVNodeArgs((args, instance) => {
  364. if (instance) {
  365. return ['h1', null, instance.type.name]
  366. } else {
  367. return args
  368. }
  369. })
  370. const App = {
  371. // this will be the name of the component in the h1
  372. name: 'Root Component',
  373. render() {
  374. return h('p') // this will be overwritten by the transform
  375. }
  376. }
  377. const root = nodeOps.createElement('div')
  378. createApp(App).mount(root)
  379. expect(serializeInner(root)).toBe('<h1>Root Component</h1>')
  380. })
  381. })
  382. })