rendererComponent.spec.ts 12 KB

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