rendererComponent.spec.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. import {
  2. type Ref,
  3. type SetupContext,
  4. type VNode,
  5. h,
  6. inject,
  7. nextTick,
  8. nodeOps,
  9. provide,
  10. ref,
  11. render,
  12. serializeInner,
  13. watch,
  14. } from '@vue/runtime-test'
  15. describe('renderer: component', () => {
  16. test('should update parent(hoc) component host el when child component self update', async () => {
  17. const value = ref(true)
  18. let parentVnode: VNode
  19. let childVnode1: VNode
  20. let childVnode2: VNode
  21. const Parent = {
  22. render: () => {
  23. // let Parent first rerender
  24. return (parentVnode = h(Child))
  25. },
  26. }
  27. const Child = {
  28. render: () => {
  29. return value.value
  30. ? (childVnode1 = h('div'))
  31. : (childVnode2 = h('span'))
  32. },
  33. }
  34. const root = nodeOps.createElement('div')
  35. render(h(Parent), root)
  36. expect(serializeInner(root)).toBe(`<div></div>`)
  37. expect(parentVnode!.el).toBe(childVnode1!.el)
  38. value.value = false
  39. await nextTick()
  40. expect(serializeInner(root)).toBe(`<span></span>`)
  41. expect(parentVnode!.el).toBe(childVnode2!.el)
  42. })
  43. it('should create a component with props', () => {
  44. const Comp = {
  45. render: () => {
  46. return h('div')
  47. },
  48. }
  49. const root = nodeOps.createElement('div')
  50. render(h(Comp, { id: 'foo', class: 'bar' }), root)
  51. expect(serializeInner(root)).toBe(`<div id="foo" class="bar"></div>`)
  52. })
  53. it('should create a component with direct text children', () => {
  54. const Comp = {
  55. render: () => {
  56. return h('div', 'test')
  57. },
  58. }
  59. const root = nodeOps.createElement('div')
  60. render(h(Comp, { id: 'foo', class: 'bar' }), root)
  61. expect(serializeInner(root)).toBe(`<div id="foo" class="bar">test</div>`)
  62. })
  63. it('should update an Component tag which is already mounted', () => {
  64. const Comp1 = {
  65. render: () => {
  66. return h('div', 'foo')
  67. },
  68. }
  69. const root = nodeOps.createElement('div')
  70. render(h(Comp1), root)
  71. expect(serializeInner(root)).toBe('<div>foo</div>')
  72. const Comp2 = {
  73. render: () => {
  74. return h('span', 'foo')
  75. },
  76. }
  77. render(h(Comp2), root)
  78. expect(serializeInner(root)).toBe('<span>foo</span>')
  79. })
  80. // #2072
  81. it('should not update Component if only changed props are declared emit listeners', () => {
  82. const Comp1 = {
  83. emits: ['foo'],
  84. updated: vi.fn(),
  85. render: () => null,
  86. }
  87. const root = nodeOps.createElement('div')
  88. render(
  89. h(Comp1, {
  90. onFoo: () => {},
  91. }),
  92. root,
  93. )
  94. render(
  95. h(Comp1, {
  96. onFoo: () => {},
  97. }),
  98. root,
  99. )
  100. expect(Comp1.updated).not.toHaveBeenCalled()
  101. })
  102. // #2043
  103. test('component child synchronously updating parent state should trigger parent re-render', async () => {
  104. const App = {
  105. setup() {
  106. const n = ref(0)
  107. provide('foo', n)
  108. return () => {
  109. return [h('div', n.value), h(Child)]
  110. }
  111. },
  112. }
  113. const Child = {
  114. setup() {
  115. const n = inject<Ref<number>>('foo')!
  116. n.value++
  117. return () => {
  118. return h('div', n.value)
  119. }
  120. },
  121. }
  122. const root = nodeOps.createElement('div')
  123. render(h(App), root)
  124. expect(serializeInner(root)).toBe(`<div>0</div><div>1</div>`)
  125. await nextTick()
  126. expect(serializeInner(root)).toBe(`<div>1</div><div>1</div>`)
  127. })
  128. // #2170
  129. test('instance.$el should be exposed to watch options', async () => {
  130. function returnThis(this: any, _arg: any) {
  131. return this
  132. }
  133. const propWatchSpy = vi.fn(returnThis)
  134. const dataWatchSpy = vi.fn(returnThis)
  135. let instance: any
  136. const Comp = {
  137. props: {
  138. testProp: String,
  139. },
  140. data() {
  141. return {
  142. testData: undefined,
  143. }
  144. },
  145. watch: {
  146. testProp() {
  147. // @ts-expect-error
  148. propWatchSpy(this.$el)
  149. },
  150. testData() {
  151. // @ts-expect-error
  152. dataWatchSpy(this.$el)
  153. },
  154. },
  155. created() {
  156. instance = this
  157. },
  158. render() {
  159. return h('div')
  160. },
  161. }
  162. const root = nodeOps.createElement('div')
  163. render(h(Comp), root)
  164. await nextTick()
  165. expect(propWatchSpy).not.toHaveBeenCalled()
  166. expect(dataWatchSpy).not.toHaveBeenCalled()
  167. render(h(Comp, { testProp: 'prop ' }), root)
  168. await nextTick()
  169. expect(propWatchSpy).toHaveBeenCalledWith(instance.$el)
  170. instance.testData = 1
  171. await nextTick()
  172. expect(dataWatchSpy).toHaveBeenCalledWith(instance.$el)
  173. })
  174. // #2200
  175. test('component child updating parent state in pre-flush should trigger parent re-render', async () => {
  176. const outer = ref(0)
  177. const App = {
  178. setup() {
  179. const inner = ref(0)
  180. return () => {
  181. return [
  182. h('div', inner.value),
  183. h(Child, {
  184. value: outer.value,
  185. onUpdate: (val: number) => (inner.value = val),
  186. }),
  187. ]
  188. }
  189. },
  190. }
  191. const Child = {
  192. props: ['value'],
  193. setup(props: any, { emit }: SetupContext) {
  194. watch(
  195. () => props.value,
  196. (val: number) => emit('update', val),
  197. )
  198. return () => {
  199. return h('div', props.value)
  200. }
  201. },
  202. }
  203. const root = nodeOps.createElement('div')
  204. render(h(App), root)
  205. expect(serializeInner(root)).toBe(`<div>0</div><div>0</div>`)
  206. outer.value++
  207. await nextTick()
  208. expect(serializeInner(root)).toBe(`<div>1</div><div>1</div>`)
  209. })
  210. test('child only updates once when triggered in multiple ways', async () => {
  211. const a = ref(0)
  212. const calls: string[] = []
  213. const Parent = {
  214. setup() {
  215. return () => {
  216. calls.push('render parent')
  217. return h(Child, { count: a.value }, () => a.value)
  218. }
  219. },
  220. }
  221. const Child = {
  222. props: ['count'],
  223. setup(props: any) {
  224. return () => {
  225. calls.push('render child')
  226. return `${props.count} - ${a.value}`
  227. }
  228. },
  229. }
  230. render(h(Parent), nodeOps.createElement('div'))
  231. expect(calls).toEqual(['render parent', 'render child'])
  232. // This will trigger child rendering directly, as well as via a prop change
  233. a.value++
  234. await nextTick()
  235. expect(calls).toEqual([
  236. 'render parent',
  237. 'render child',
  238. 'render parent',
  239. 'render child',
  240. ])
  241. })
  242. // #7745
  243. test(`an earlier update doesn't lead to excessive subsequent updates`, async () => {
  244. const globalCount = ref(0)
  245. const parentCount = ref(0)
  246. const calls: string[] = []
  247. const Root = {
  248. setup() {
  249. return () => {
  250. calls.push('render root')
  251. return h(Parent, { count: globalCount.value })
  252. }
  253. },
  254. }
  255. const Parent = {
  256. props: ['count'],
  257. setup(props: any) {
  258. return () => {
  259. calls.push('render parent')
  260. return [
  261. `${globalCount.value} - ${props.count}`,
  262. h(Child, { count: parentCount.value }),
  263. ]
  264. }
  265. },
  266. }
  267. const Child = {
  268. props: ['count'],
  269. setup(props: any) {
  270. watch(
  271. () => props.count,
  272. () => {
  273. calls.push('child watcher')
  274. globalCount.value = props.count
  275. },
  276. )
  277. return () => {
  278. calls.push('render child')
  279. }
  280. },
  281. }
  282. render(h(Root), nodeOps.createElement('div'))
  283. expect(calls).toEqual(['render root', 'render parent', 'render child'])
  284. parentCount.value++
  285. await nextTick()
  286. expect(calls).toEqual([
  287. 'render root',
  288. 'render parent',
  289. 'render child',
  290. 'render parent',
  291. 'child watcher',
  292. 'render child',
  293. 'render root',
  294. 'render parent',
  295. ])
  296. })
  297. // #2521
  298. test('should pause tracking deps when initializing legacy options', async () => {
  299. let childInstance = null as any
  300. const Child = {
  301. props: ['foo'],
  302. data() {
  303. return {
  304. count: 0,
  305. }
  306. },
  307. watch: {
  308. foo: {
  309. immediate: true,
  310. handler() {
  311. ;(this as any).count
  312. },
  313. },
  314. },
  315. created() {
  316. childInstance = this as any
  317. childInstance.count
  318. },
  319. render() {
  320. return h('h1', (this as any).count)
  321. },
  322. }
  323. const App = {
  324. setup() {
  325. return () => h(Child)
  326. },
  327. updated: vi.fn(),
  328. }
  329. const root = nodeOps.createElement('div')
  330. render(h(App), root)
  331. expect(App.updated).toHaveBeenCalledTimes(0)
  332. childInstance.count++
  333. await nextTick()
  334. expect(App.updated).toHaveBeenCalledTimes(0)
  335. })
  336. describe('render with access caches', () => {
  337. // #3297
  338. test('should not set the access cache in the data() function (production mode)', () => {
  339. const Comp = {
  340. data() {
  341. ;(this as any).foo
  342. return { foo: 1 }
  343. },
  344. render() {
  345. return h('h1', (this as any).foo)
  346. },
  347. }
  348. const root = nodeOps.createElement('div')
  349. __DEV__ = false
  350. render(h(Comp), root)
  351. __DEV__ = true
  352. expect(serializeInner(root)).toBe(`<h1>1</h1>`)
  353. })
  354. })
  355. test('the component VNode should be cloned when reusing it', () => {
  356. const App = {
  357. render() {
  358. const c = [h(Comp)]
  359. return [c, c, c]
  360. },
  361. }
  362. const ids: number[] = []
  363. const Comp = {
  364. render: () => h('h1'),
  365. beforeUnmount() {
  366. ids.push((this as any).$.uid)
  367. },
  368. }
  369. const root = nodeOps.createElement('div')
  370. render(h(App), root)
  371. expect(serializeInner(root)).toBe(`<h1></h1><h1></h1><h1></h1>`)
  372. render(null, root)
  373. expect(serializeInner(root)).toBe(``)
  374. expect(ids).toEqual([ids[0], ids[0] + 1, ids[0] + 2])
  375. })
  376. test('child component props update should not lead to double update', async () => {
  377. const text = ref(0)
  378. const spy = vi.fn()
  379. const App = {
  380. render() {
  381. return h(Comp, { text: text.value })
  382. },
  383. }
  384. const Comp = {
  385. props: ['text'],
  386. render(this: any) {
  387. spy()
  388. return h('h1', this.text)
  389. },
  390. }
  391. const root = nodeOps.createElement('div')
  392. render(h(App), root)
  393. expect(serializeInner(root)).toBe(`<h1>0</h1>`)
  394. expect(spy).toHaveBeenCalledTimes(1)
  395. text.value++
  396. await nextTick()
  397. expect(serializeInner(root)).toBe(`<h1>1</h1>`)
  398. expect(spy).toHaveBeenCalledTimes(2)
  399. })
  400. it('should warn accessing `this` in a <script setup> template', () => {
  401. const App = {
  402. setup() {
  403. return {
  404. __isScriptSetup: true,
  405. }
  406. },
  407. render(this: any) {
  408. return this.$attrs.id
  409. },
  410. }
  411. const root = nodeOps.createElement('div')
  412. render(h(App), root)
  413. expect(
  414. `Property '$attrs' was accessed via 'this'. Avoid using 'this' in templates.`,
  415. ).toHaveBeenWarned()
  416. })
  417. })