apiLifecycle.spec.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. import {
  2. type DebuggerEvent,
  3. type InjectionKey,
  4. type Ref,
  5. TrackOpTypes,
  6. TriggerOpTypes,
  7. currentInstance,
  8. inject,
  9. nextTick,
  10. onBeforeMount,
  11. onBeforeUnmount,
  12. onBeforeUpdate,
  13. onMounted,
  14. onRenderTracked,
  15. onRenderTriggered,
  16. onUnmounted,
  17. onUpdated,
  18. provide,
  19. reactive,
  20. ref,
  21. } from '@vue/runtime-dom'
  22. import {
  23. createComponent,
  24. createFor,
  25. createIf,
  26. createTextNode,
  27. insert,
  28. renderEffect,
  29. template,
  30. } from '../src'
  31. import { makeRender } from './_utils'
  32. import { ITERATE_KEY } from '@vue/reactivity'
  33. import { setElementText } from '../src/dom/prop'
  34. const define = makeRender<any>()
  35. describe('api: lifecycle hooks', () => {
  36. it('onBeforeMount', () => {
  37. const fn = vi.fn(() => {
  38. expect(host.innerHTML).toBe(``)
  39. })
  40. const { render, host } = define({
  41. setup() {
  42. onBeforeMount(fn)
  43. return []
  44. },
  45. })
  46. render()
  47. expect(fn).toHaveBeenCalledTimes(1)
  48. })
  49. it('onMounted', () => {
  50. const fn = vi.fn(() => {
  51. expect(host.innerHTML).toBe(``)
  52. })
  53. const { render, host } = define({
  54. setup() {
  55. onMounted(fn)
  56. return []
  57. },
  58. })
  59. render()
  60. expect(fn).toHaveBeenCalledTimes(1)
  61. })
  62. it('onBeforeUpdate', async () => {
  63. const count = ref(0)
  64. const fn = vi.fn(() => {
  65. expect(host.innerHTML).toBe('0')
  66. })
  67. const { render, host } = define({
  68. setup() {
  69. onBeforeUpdate(fn)
  70. const n0 = createTextNode()
  71. renderEffect(() => {
  72. setElementText(n0, count.value)
  73. })
  74. return n0
  75. },
  76. })
  77. render()
  78. count.value++
  79. await nextTick()
  80. expect(fn).toHaveBeenCalledTimes(1)
  81. expect(host.innerHTML).toBe('1')
  82. })
  83. it('state mutation in onBeforeUpdate', async () => {
  84. const count = ref(0)
  85. const fn = vi.fn(() => {
  86. expect(host.innerHTML).toBe('0')
  87. count.value++
  88. })
  89. const renderSpy = vi.fn()
  90. const { render, host } = define({
  91. setup() {
  92. onBeforeUpdate(fn)
  93. const n0 = createTextNode()
  94. renderEffect(() => {
  95. renderSpy()
  96. setElementText(n0, count.value)
  97. })
  98. return n0
  99. },
  100. })
  101. render()
  102. expect(renderSpy).toHaveBeenCalledTimes(1)
  103. })
  104. it('onUpdated', async () => {
  105. const count = ref(0)
  106. const fn = vi.fn(() => {
  107. expect(host.innerHTML).toBe('1')
  108. })
  109. const { render, host } = define({
  110. setup() {
  111. onUpdated(fn)
  112. const n0 = createTextNode()
  113. renderEffect(() => {
  114. setElementText(n0, count.value)
  115. })
  116. return n0
  117. },
  118. })
  119. render()
  120. count.value++
  121. await nextTick()
  122. expect(fn).toHaveBeenCalledTimes(1)
  123. })
  124. it('onBeforeUnmount', async () => {
  125. const toggle = ref(true)
  126. const fn = vi.fn(() => {
  127. expect(host.innerHTML).toBe('<div></div><!--if-->')
  128. })
  129. const { render, host } = define({
  130. setup() {
  131. const n0 = createIf(
  132. () => toggle.value,
  133. () => createComponent(Child),
  134. )
  135. return n0
  136. },
  137. })
  138. const Child = {
  139. setup() {
  140. onBeforeUnmount(fn)
  141. const t0 = template('<div></div>')
  142. const n0 = t0()
  143. return n0
  144. },
  145. }
  146. render()
  147. toggle.value = false
  148. await nextTick()
  149. expect(fn).toHaveBeenCalledTimes(1)
  150. expect(host.innerHTML).toBe('<!--if-->')
  151. })
  152. it('onUnmounted', async () => {
  153. const toggle = ref(true)
  154. const fn = vi.fn(() => {
  155. expect(host.innerHTML).toBe('<!--if-->')
  156. })
  157. const { render, host } = define({
  158. setup() {
  159. const n0 = createIf(
  160. () => toggle.value,
  161. () => createComponent(Child),
  162. )
  163. return n0
  164. },
  165. })
  166. const Child = {
  167. setup() {
  168. onUnmounted(fn)
  169. const t0 = template('<div></div>')
  170. const n0 = t0()
  171. return n0
  172. },
  173. }
  174. render()
  175. toggle.value = false
  176. await nextTick()
  177. expect(fn).toHaveBeenCalledTimes(1)
  178. expect(host.innerHTML).toBe('<!--if-->')
  179. })
  180. it('onBeforeUnmount in onMounted', async () => {
  181. const toggle = ref(true)
  182. const fn = vi.fn(() => {
  183. expect(host.innerHTML).toBe('<div></div><!--if-->')
  184. })
  185. const { render, host } = define({
  186. setup() {
  187. const n0 = createIf(
  188. () => toggle.value,
  189. () => createComponent(Child),
  190. )
  191. return n0
  192. },
  193. })
  194. const Child = {
  195. setup() {
  196. onMounted(() => {
  197. onBeforeUnmount(fn)
  198. })
  199. const t0 = template('<div></div>')
  200. const n0 = t0()
  201. return n0
  202. },
  203. }
  204. render()
  205. toggle.value = false
  206. await nextTick()
  207. expect(fn).toHaveBeenCalledTimes(1)
  208. expect(host.innerHTML).toBe('<!--if-->')
  209. })
  210. it('lifecycle call order', async () => {
  211. const count = ref(0)
  212. const toggle = ref(true)
  213. const calls: string[] = []
  214. const { render } = define({
  215. setup() {
  216. onBeforeMount(() => calls.push('root onBeforeMount'))
  217. onMounted(() => calls.push('root onMounted'))
  218. onBeforeUpdate(() => calls.push('root onBeforeUpdate'))
  219. onUpdated(() => calls.push('root onUpdated'))
  220. onBeforeUnmount(() => calls.push('root onBeforeUnmount'))
  221. onUnmounted(() => calls.push('root onUnmounted'))
  222. const n0 = createIf(
  223. () => toggle.value,
  224. () => createComponent(Mid, { count: () => count.value }),
  225. )
  226. return n0
  227. },
  228. })
  229. const Mid = {
  230. props: ['count'],
  231. setup(props: any) {
  232. onBeforeMount(() => calls.push('mid onBeforeMount'))
  233. onMounted(() => calls.push('mid onMounted'))
  234. onBeforeUpdate(() => calls.push('mid onBeforeUpdate'))
  235. onUpdated(() => calls.push('mid onUpdated'))
  236. onBeforeUnmount(() => calls.push('mid onBeforeUnmount'))
  237. onUnmounted(() => calls.push('mid onUnmounted'))
  238. const n0 = createComponent(Child, { count: () => props.count })
  239. return n0
  240. },
  241. }
  242. const Child = {
  243. props: ['count'],
  244. setup(props: any) {
  245. onBeforeMount(() => calls.push('child onBeforeMount'))
  246. onMounted(() => calls.push('child onMounted'))
  247. onBeforeUpdate(() => calls.push('child onBeforeUpdate'))
  248. onUpdated(() => calls.push('child onUpdated'))
  249. onBeforeUnmount(() => calls.push('child onBeforeUnmount'))
  250. onUnmounted(() => calls.push('child onUnmounted'))
  251. const t0 = template('<div></div>')
  252. const n0 = t0()
  253. renderEffect(() => setElementText(n0, props.count))
  254. return n0
  255. },
  256. }
  257. // mount
  258. const ctx = render()
  259. expect(calls).toEqual([
  260. 'root onBeforeMount',
  261. 'mid onBeforeMount',
  262. 'child onBeforeMount',
  263. 'child onMounted',
  264. 'mid onMounted',
  265. 'root onMounted',
  266. ])
  267. calls.length = 0
  268. // update
  269. count.value++
  270. await nextTick()
  271. // only child updated
  272. expect(calls).toEqual(['child onBeforeUpdate', 'child onUpdated'])
  273. calls.length = 0
  274. // unmount
  275. ctx.app.unmount()
  276. await nextTick()
  277. expect(calls).toEqual([
  278. 'root onBeforeUnmount',
  279. 'mid onBeforeUnmount',
  280. 'child onBeforeUnmount',
  281. 'child onUnmounted',
  282. 'mid onUnmounted',
  283. 'root onUnmounted',
  284. ])
  285. })
  286. it('onRenderTracked', async () => {
  287. const events: DebuggerEvent[] = []
  288. const onTrack = vi.fn((e: DebuggerEvent) => {
  289. events.push(e)
  290. })
  291. const obj = reactive({ foo: 1, bar: 2 })
  292. const { render } = define({
  293. setup() {
  294. onRenderTracked(onTrack)
  295. const n0 = createTextNode()
  296. renderEffect(() => {
  297. setElementText(n0, [obj.foo, 'bar' in obj, Object.keys(obj).join('')])
  298. })
  299. return n0
  300. },
  301. })
  302. render()
  303. expect(onTrack).toHaveBeenCalledTimes(3)
  304. expect(events).toMatchObject([
  305. {
  306. target: obj,
  307. type: TrackOpTypes.GET,
  308. key: 'foo',
  309. },
  310. {
  311. target: obj,
  312. type: TrackOpTypes.HAS,
  313. key: 'bar',
  314. },
  315. {
  316. target: obj,
  317. type: TrackOpTypes.ITERATE,
  318. key: ITERATE_KEY,
  319. },
  320. ])
  321. })
  322. it('onRenderTrigger', async () => {
  323. const events: DebuggerEvent[] = []
  324. const onTrigger = vi.fn((e: DebuggerEvent) => {
  325. events.push(e)
  326. })
  327. const obj = reactive<{
  328. foo: number
  329. bar?: number
  330. }>({ foo: 1, bar: 2 })
  331. const { render } = define({
  332. setup() {
  333. onRenderTriggered(onTrigger)
  334. const n0 = createTextNode()
  335. renderEffect(() => {
  336. setElementText(n0, [obj.foo, 'bar' in obj, Object.keys(obj).join('')])
  337. })
  338. return n0
  339. },
  340. })
  341. render()
  342. obj.foo++
  343. await nextTick()
  344. expect(onTrigger).toHaveBeenCalledTimes(1)
  345. expect(events[0]).toMatchObject({
  346. type: TriggerOpTypes.SET,
  347. key: 'foo',
  348. oldValue: 1,
  349. newValue: 2,
  350. })
  351. delete obj.bar
  352. await nextTick()
  353. expect(onTrigger).toHaveBeenCalledTimes(2)
  354. expect(events[1]).toMatchObject({
  355. type: TriggerOpTypes.DELETE,
  356. key: 'bar',
  357. oldValue: 2,
  358. })
  359. ;(obj as any).baz = 3
  360. await nextTick()
  361. expect(onTrigger).toHaveBeenCalledTimes(3)
  362. expect(events[2]).toMatchObject({
  363. type: TriggerOpTypes.ADD,
  364. key: 'baz',
  365. newValue: 3,
  366. })
  367. })
  368. it('runs shared hook fn for each instance', async () => {
  369. const fn = vi.fn()
  370. const toggle = ref(true)
  371. const { render } = define({
  372. setup() {
  373. return createIf(
  374. () => toggle.value,
  375. () => [createComponent(Child), createComponent(Child)],
  376. )
  377. },
  378. })
  379. const Child = {
  380. setup() {
  381. onBeforeMount(fn)
  382. onBeforeUnmount(fn)
  383. return template('<div></div>')()
  384. },
  385. }
  386. render()
  387. expect(fn).toHaveBeenCalledTimes(2)
  388. toggle.value = false
  389. await nextTick()
  390. expect(fn).toHaveBeenCalledTimes(4)
  391. })
  392. // #136
  393. it('should trigger updated hooks across components. (parent -> child)', async () => {
  394. const handleUpdated = vi.fn()
  395. const handleUpdatedChild = vi.fn()
  396. const count = ref(0)
  397. const { render, host } = define({
  398. setup() {
  399. onUpdated(() => handleUpdated())
  400. const n0 = createTextNode()
  401. renderEffect(() => setElementText(n0, count.value))
  402. const n1 = createComponent(Child, { count: () => count.value })
  403. return [n0, n1]
  404. },
  405. })
  406. const Child = {
  407. props: { count: Number },
  408. setup() {
  409. onUpdated(() => handleUpdatedChild())
  410. const props = currentInstance!.props
  411. const n2 = createTextNode()
  412. renderEffect(() => setElementText(n2, props.count))
  413. return n2
  414. },
  415. }
  416. render()
  417. expect(host.innerHTML).toBe('00')
  418. expect(handleUpdated).toHaveBeenCalledTimes(0)
  419. expect(handleUpdatedChild).toHaveBeenCalledTimes(0)
  420. count.value++
  421. await nextTick()
  422. expect(host.innerHTML).toBe('11')
  423. expect(handleUpdated).toHaveBeenCalledTimes(1)
  424. expect(handleUpdatedChild).toHaveBeenCalledTimes(1)
  425. })
  426. // #136
  427. it('should trigger updated hooks across components. (child -> parent)', async () => {
  428. const handleUpdated = vi.fn()
  429. const handleUpdatedChild = vi.fn()
  430. const key: InjectionKey<Ref<number>> = Symbol()
  431. const { render, host } = define({
  432. setup() {
  433. const count = ref(0)
  434. provide(key, count)
  435. onUpdated(() => handleUpdated())
  436. const n0 = createTextNode()
  437. renderEffect(() => setElementText(n0, count.value))
  438. const n1 = createComponent(Child, { count: () => count.value })
  439. return [n0, n1]
  440. },
  441. })
  442. let update: any
  443. const Child = {
  444. props: { count: Number },
  445. setup() {
  446. onUpdated(() => handleUpdatedChild())
  447. const count = inject(key)!
  448. update = () => count.value++
  449. const n2 = createTextNode()
  450. renderEffect(() => setElementText(n2, count.value))
  451. return n2
  452. },
  453. }
  454. render()
  455. expect(host.innerHTML).toBe('00')
  456. expect(handleUpdated).toHaveBeenCalledTimes(0)
  457. expect(handleUpdatedChild).toHaveBeenCalledTimes(0)
  458. update()
  459. await nextTick()
  460. expect(host.innerHTML).toBe('11')
  461. expect(handleUpdated).toHaveBeenCalledTimes(1)
  462. expect(handleUpdatedChild).toHaveBeenCalledTimes(1)
  463. })
  464. test('unmount hooks when nested in if block', async () => {
  465. const toggle = ref(true)
  466. const fn = vi.fn(() => {
  467. expect(host.innerHTML).toBe('<div><span></span></div><!--if-->')
  468. })
  469. const fn2 = vi.fn(() => {
  470. expect(host.innerHTML).toBe('<!--if-->')
  471. })
  472. const { render, host } = define({
  473. setup() {
  474. const n0 = createIf(
  475. () => toggle.value,
  476. () => {
  477. const n1 = document.createElement('div')
  478. const n2 = createComponent(Child)
  479. insert(n2, n1)
  480. return n1
  481. },
  482. )
  483. return n0
  484. },
  485. })
  486. const Child = {
  487. setup() {
  488. onBeforeUnmount(fn)
  489. onUnmounted(fn2)
  490. const t0 = template('<span></span>')
  491. const n0 = t0()
  492. return n0
  493. },
  494. }
  495. render()
  496. toggle.value = false
  497. await nextTick()
  498. expect(fn).toHaveBeenCalledTimes(1)
  499. expect(fn2).toHaveBeenCalledTimes(1)
  500. expect(host.innerHTML).toBe('<!--if-->')
  501. })
  502. test('unmount hooks when nested in for blocks', async () => {
  503. const list = ref([1])
  504. const fn = vi.fn(() => {
  505. expect(host.innerHTML).toBe('<div><span></span></div><!--for-->')
  506. })
  507. const fn2 = vi.fn(() => {
  508. expect(host.innerHTML).toBe('<!--for-->')
  509. })
  510. const { render, host } = define({
  511. setup() {
  512. const n0 = createFor(
  513. () => list.value,
  514. () => {
  515. const n1 = document.createElement('div')
  516. const n2 = createComponent(Child)
  517. insert(n2, n1)
  518. return n1
  519. },
  520. )
  521. return n0
  522. },
  523. })
  524. const Child = {
  525. setup() {
  526. onBeforeUnmount(fn)
  527. onUnmounted(fn2)
  528. const t0 = template('<span></span>')
  529. const n0 = t0()
  530. return n0
  531. },
  532. }
  533. render()
  534. list.value.pop()
  535. await nextTick()
  536. expect(fn).toHaveBeenCalledTimes(1)
  537. expect(fn2).toHaveBeenCalledTimes(1)
  538. expect(host.innerHTML).toBe('<!--for-->')
  539. })
  540. })