vnode.spec.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  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. })
  173. describe('mergeProps', () => {
  174. test('class', () => {
  175. let props1: Data = { class: 'c' }
  176. let props2: Data = { class: ['cc'] }
  177. let props3: Data = { class: [{ ccc: true }] }
  178. let props4: Data = { class: { cccc: true } }
  179. expect(mergeProps(props1, props2, props3, props4)).toMatchObject({
  180. class: 'c cc ccc cccc'
  181. })
  182. })
  183. test('style', () => {
  184. let props1: Data = {
  185. style: {
  186. color: 'red',
  187. fontSize: 10
  188. }
  189. }
  190. let props2: Data = {
  191. style: [
  192. {
  193. color: 'blue',
  194. width: '200px'
  195. },
  196. {
  197. width: '300px',
  198. height: '300px',
  199. fontSize: 30
  200. }
  201. ]
  202. }
  203. expect(mergeProps(props1, props2)).toMatchObject({
  204. style: {
  205. color: 'blue',
  206. width: '300px',
  207. height: '300px',
  208. fontSize: 30
  209. }
  210. })
  211. })
  212. test('handlers', () => {
  213. let clickHander1 = function() {}
  214. let clickHander2 = function() {}
  215. let focusHander2 = function() {}
  216. let props1: Data = { onClick: clickHander1 }
  217. let props2: Data = { onClick: clickHander2, onFocus: focusHander2 }
  218. expect(mergeProps(props1, props2)).toMatchObject({
  219. onClick: [clickHander1, clickHander2],
  220. onFocus: focusHander2
  221. })
  222. })
  223. test('default', () => {
  224. let props1: Data = { foo: 'c' }
  225. let props2: Data = { foo: {}, bar: ['cc'] }
  226. let props3: Data = { baz: { ccc: true } }
  227. expect(mergeProps(props1, props2, props3)).toMatchObject({
  228. foo: {},
  229. bar: ['cc'],
  230. baz: { ccc: true }
  231. })
  232. })
  233. })
  234. describe('dynamic children', () => {
  235. test('with patchFlags', () => {
  236. const hoist = createVNode('div')
  237. let vnode1
  238. const vnode = (openBlock(),
  239. createBlock('div', null, [
  240. hoist,
  241. (vnode1 = createVNode('div', null, 'text', PatchFlags.TEXT))
  242. ]))
  243. expect(vnode.dynamicChildren).toStrictEqual([vnode1])
  244. })
  245. test('should not track vnodes with only HYDRATE_EVENTS flag', () => {
  246. const hoist = createVNode('div')
  247. const vnode = (openBlock(),
  248. createBlock('div', null, [
  249. hoist,
  250. createVNode('div', null, 'text', PatchFlags.HYDRATE_EVENTS)
  251. ]))
  252. expect(vnode.dynamicChildren).toStrictEqual([])
  253. })
  254. test('many times call openBlock', () => {
  255. const hoist = createVNode('div')
  256. let vnode1, vnode2, vnode3
  257. const vnode = (openBlock(),
  258. createBlock('div', null, [
  259. hoist,
  260. (vnode1 = createVNode('div', null, 'text', PatchFlags.TEXT)),
  261. (vnode2 = (openBlock(),
  262. createBlock('div', null, [
  263. hoist,
  264. (vnode3 = createVNode('div', null, 'text', PatchFlags.TEXT))
  265. ])))
  266. ]))
  267. expect(vnode.dynamicChildren).toStrictEqual([vnode1, vnode2])
  268. expect(vnode2.dynamicChildren).toStrictEqual([vnode3])
  269. })
  270. test('with stateful component', () => {
  271. const hoist = createVNode('div')
  272. let vnode1
  273. const vnode = (openBlock(),
  274. createBlock('div', null, [
  275. hoist,
  276. (vnode1 = createVNode({}, null, 'text'))
  277. ]))
  278. expect(vnode.dynamicChildren).toStrictEqual([vnode1])
  279. })
  280. test('with functional component', () => {
  281. const hoist = createVNode('div')
  282. let vnode1
  283. const vnode = (openBlock(),
  284. createBlock('div', null, [
  285. hoist,
  286. (vnode1 = createVNode(() => {}, null, 'text'))
  287. ]))
  288. expect(vnode.dynamicChildren).toStrictEqual([vnode1])
  289. })
  290. test('with suspense', () => {
  291. const hoist = createVNode('div')
  292. let vnode1
  293. const vnode = (openBlock(),
  294. createBlock('div', null, [
  295. hoist,
  296. (vnode1 = createVNode(() => {}, null, 'text'))
  297. ]))
  298. expect(vnode.dynamicChildren).toStrictEqual([vnode1])
  299. })
  300. })
  301. describe('transformVNodeArgs', () => {
  302. afterEach(() => {
  303. // reset
  304. transformVNodeArgs()
  305. })
  306. test('no-op pass through', () => {
  307. transformVNodeArgs(args => args)
  308. const vnode = createVNode('div', { id: 'foo' }, 'hello')
  309. expect(vnode).toMatchObject({
  310. type: 'div',
  311. props: { id: 'foo' },
  312. children: 'hello',
  313. shapeFlag: ShapeFlags.ELEMENT | ShapeFlags.TEXT_CHILDREN
  314. })
  315. })
  316. test('direct override', () => {
  317. transformVNodeArgs(() => ['div', { id: 'foo' }, 'hello'])
  318. const vnode = createVNode('p')
  319. expect(vnode).toMatchObject({
  320. type: 'div',
  321. props: { id: 'foo' },
  322. children: 'hello',
  323. shapeFlag: ShapeFlags.ELEMENT | ShapeFlags.TEXT_CHILDREN
  324. })
  325. })
  326. test('receive component instance as 2nd arg', () => {
  327. transformVNodeArgs((args, instance) => {
  328. if (instance) {
  329. return ['h1', null, instance.type.name]
  330. } else {
  331. return args
  332. }
  333. })
  334. const App = {
  335. // this will be the name of the component in the h1
  336. name: 'Root Component',
  337. render() {
  338. return h('p') // this will be overwritten by the transform
  339. }
  340. }
  341. const root = nodeOps.createElement('div')
  342. createApp(App).mount(root)
  343. expect(serializeInner(root)).toBe('<h1>Root Component</h1>')
  344. })
  345. })
  346. })