apiLifecycle.spec.ts 13 KB

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