apiLifecycle.spec.ts 12 KB

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