apiLifecycle.spec.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  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('onBeforeUnmount', async () => {
  123. const toggle = ref(true)
  124. const fn = vi.fn(() => {
  125. expect(host.innerHTML).toBe('<div></div><!--if-->')
  126. })
  127. const { render, host } = define({
  128. setup() {
  129. const n0 = createIf(
  130. () => toggle.value,
  131. () => createComponent(Child),
  132. )
  133. return n0
  134. },
  135. })
  136. const Child = {
  137. setup() {
  138. onBeforeUnmount(fn)
  139. const t0 = template('<div></div>')
  140. const n0 = t0()
  141. return n0
  142. },
  143. }
  144. render()
  145. toggle.value = false
  146. await nextTick()
  147. expect(fn).toHaveBeenCalledTimes(1)
  148. expect(host.innerHTML).toBe('<!--if-->')
  149. })
  150. it('onUnmounted', async () => {
  151. const toggle = ref(true)
  152. const fn = vi.fn(() => {
  153. expect(host.innerHTML).toBe('<!--if-->')
  154. })
  155. const { render, host } = define({
  156. setup() {
  157. const n0 = createIf(
  158. () => toggle.value,
  159. () => createComponent(Child),
  160. )
  161. return n0
  162. },
  163. })
  164. const Child = {
  165. setup() {
  166. onUnmounted(fn)
  167. const t0 = template('<div></div>')
  168. const n0 = t0()
  169. return n0
  170. },
  171. }
  172. render()
  173. toggle.value = false
  174. await nextTick()
  175. expect(fn).toHaveBeenCalledTimes(1)
  176. expect(host.innerHTML).toBe('<!--if-->')
  177. })
  178. it('onBeforeUnmount in onMounted', async () => {
  179. const toggle = ref(true)
  180. const fn = vi.fn(() => {
  181. expect(host.innerHTML).toBe('<div></div><!--if-->')
  182. })
  183. const { render, host } = define({
  184. setup() {
  185. const n0 = createIf(
  186. () => toggle.value,
  187. () => createComponent(Child),
  188. )
  189. return n0
  190. },
  191. })
  192. const Child = {
  193. setup() {
  194. onMounted(() => {
  195. onBeforeUnmount(fn)
  196. })
  197. const t0 = template('<div></div>')
  198. const n0 = t0()
  199. return n0
  200. },
  201. }
  202. render()
  203. toggle.value = false
  204. await nextTick()
  205. expect(fn).toHaveBeenCalledTimes(1)
  206. expect(host.innerHTML).toBe('<!--if-->')
  207. })
  208. it('lifecycle call order', async () => {
  209. const count = ref(0)
  210. const toggle = ref(true)
  211. const calls: string[] = []
  212. const { render } = define({
  213. setup() {
  214. onBeforeMount(() => calls.push('root onBeforeMount'))
  215. onMounted(() => calls.push('root onMounted'))
  216. onBeforeUpdate(() => calls.push('root onBeforeUpdate'))
  217. onUpdated(() => calls.push('root onUpdated'))
  218. onBeforeUnmount(() => calls.push('root onBeforeUnmount'))
  219. onUnmounted(() => calls.push('root onUnmounted'))
  220. const n0 = createIf(
  221. () => toggle.value,
  222. () => createComponent(Mid, { count: () => count.value }),
  223. )
  224. return n0
  225. },
  226. })
  227. const Mid = {
  228. props: ['count'],
  229. setup(props: any) {
  230. onBeforeMount(() => calls.push('mid onBeforeMount'))
  231. onMounted(() => calls.push('mid onMounted'))
  232. onBeforeUpdate(() => calls.push('mid onBeforeUpdate'))
  233. onUpdated(() => calls.push('mid onUpdated'))
  234. onBeforeUnmount(() => calls.push('mid onBeforeUnmount'))
  235. onUnmounted(() => calls.push('mid onUnmounted'))
  236. const n0 = createComponent(Child, { count: () => props.count })
  237. return n0
  238. },
  239. }
  240. const Child = {
  241. props: ['count'],
  242. setup(props: any) {
  243. onBeforeMount(() => calls.push('child onBeforeMount'))
  244. onMounted(() => calls.push('child onMounted'))
  245. onBeforeUpdate(() => calls.push('child onBeforeUpdate'))
  246. onUpdated(() => calls.push('child onUpdated'))
  247. onBeforeUnmount(() => calls.push('child onBeforeUnmount'))
  248. onUnmounted(() => calls.push('child onUnmounted'))
  249. const t0 = template('<div></div>')
  250. const n0 = t0()
  251. renderEffect(() => setText(n0, props.count))
  252. return n0
  253. },
  254. }
  255. // mount
  256. const ctx = render()
  257. expect(calls).toEqual([
  258. 'root onBeforeMount',
  259. 'mid onBeforeMount',
  260. 'child onBeforeMount',
  261. 'child onMounted',
  262. 'mid onMounted',
  263. 'root onMounted',
  264. ])
  265. calls.length = 0
  266. // update
  267. count.value++
  268. await nextTick()
  269. // only child updated
  270. expect(calls).toEqual(['child onBeforeUpdate', 'child onUpdated'])
  271. calls.length = 0
  272. // unmount
  273. ctx.app.unmount()
  274. await nextTick()
  275. expect(calls).toEqual([
  276. 'root onBeforeUnmount',
  277. 'mid onBeforeUnmount',
  278. 'child onBeforeUnmount',
  279. 'child onUnmounted',
  280. 'mid onUnmounted',
  281. 'root onUnmounted',
  282. ])
  283. })
  284. it('onRenderTracked', async () => {
  285. const events: DebuggerEvent[] = []
  286. const onTrack = vi.fn((e: DebuggerEvent) => {
  287. events.push(e)
  288. })
  289. const obj = reactive({ foo: 1, bar: 2 })
  290. const { render } = define({
  291. setup() {
  292. onRenderTracked(onTrack)
  293. const n0 = createTextNode()
  294. renderEffect(() => {
  295. setText(n0, [obj.foo, 'bar' in obj, Object.keys(obj).join('')])
  296. })
  297. return n0
  298. },
  299. })
  300. render()
  301. expect(onTrack).toHaveBeenCalledTimes(3)
  302. expect(events).toMatchObject([
  303. {
  304. target: obj,
  305. type: TrackOpTypes.GET,
  306. key: 'foo',
  307. },
  308. {
  309. target: obj,
  310. type: TrackOpTypes.HAS,
  311. key: 'bar',
  312. },
  313. {
  314. target: obj,
  315. type: TrackOpTypes.ITERATE,
  316. key: ITERATE_KEY,
  317. },
  318. ])
  319. })
  320. it('onRenderTrigger', async () => {
  321. const events: DebuggerEvent[] = []
  322. const onTrigger = vi.fn((e: DebuggerEvent) => {
  323. events.push(e)
  324. })
  325. const obj = reactive<{
  326. foo: number
  327. bar?: number
  328. }>({ foo: 1, bar: 2 })
  329. const { render } = define({
  330. setup() {
  331. onRenderTriggered(onTrigger)
  332. const n0 = createTextNode()
  333. renderEffect(() => {
  334. setText(n0, [obj.foo, 'bar' in obj, Object.keys(obj).join('')])
  335. })
  336. return n0
  337. },
  338. })
  339. render()
  340. obj.foo++
  341. await nextTick()
  342. expect(onTrigger).toHaveBeenCalledTimes(1)
  343. expect(events[0]).toMatchObject({
  344. type: TriggerOpTypes.SET,
  345. key: 'foo',
  346. oldValue: 1,
  347. newValue: 2,
  348. })
  349. delete obj.bar
  350. await nextTick()
  351. expect(onTrigger).toHaveBeenCalledTimes(2)
  352. expect(events[1]).toMatchObject({
  353. type: TriggerOpTypes.DELETE,
  354. key: 'bar',
  355. oldValue: 2,
  356. })
  357. ;(obj as any).baz = 3
  358. await nextTick()
  359. expect(onTrigger).toHaveBeenCalledTimes(3)
  360. expect(events[2]).toMatchObject({
  361. type: TriggerOpTypes.ADD,
  362. key: 'baz',
  363. newValue: 3,
  364. })
  365. })
  366. it('runs shared hook fn for each instance', async () => {
  367. const fn = vi.fn()
  368. const toggle = ref(true)
  369. const { render } = define({
  370. setup() {
  371. return createIf(
  372. () => toggle.value,
  373. () => [createComponent(Child), createComponent(Child)],
  374. )
  375. },
  376. })
  377. const Child = {
  378. setup() {
  379. onBeforeMount(fn)
  380. onBeforeUnmount(fn)
  381. return template('<div></div>')()
  382. },
  383. }
  384. render()
  385. expect(fn).toHaveBeenCalledTimes(2)
  386. toggle.value = false
  387. await nextTick()
  388. expect(fn).toHaveBeenCalledTimes(4)
  389. })
  390. // #136
  391. it('should trigger updated hooks across components. (parent -> child)', async () => {
  392. const handleUpdated = vi.fn()
  393. const handleUpdatedChild = vi.fn()
  394. const count = ref(0)
  395. const { render, host } = define({
  396. setup() {
  397. onUpdated(() => handleUpdated())
  398. const n0 = createTextNode()
  399. renderEffect(() => setText(n0, count.value))
  400. const n1 = createComponent(Child, { count: () => count.value })
  401. return [n0, n1]
  402. },
  403. })
  404. const Child = {
  405. props: { count: Number },
  406. setup() {
  407. onUpdated(() => handleUpdatedChild())
  408. const props = currentInstance!.props
  409. const n2 = createTextNode()
  410. renderEffect(() => setText(n2, props.count))
  411. return n2
  412. },
  413. }
  414. render()
  415. expect(host.innerHTML).toBe('00')
  416. expect(handleUpdated).toHaveBeenCalledTimes(0)
  417. expect(handleUpdatedChild).toHaveBeenCalledTimes(0)
  418. count.value++
  419. await nextTick()
  420. expect(host.innerHTML).toBe('11')
  421. expect(handleUpdated).toHaveBeenCalledTimes(1)
  422. expect(handleUpdatedChild).toHaveBeenCalledTimes(1)
  423. })
  424. // #136
  425. it('should trigger updated hooks across components. (child -> parent)', async () => {
  426. const handleUpdated = vi.fn()
  427. const handleUpdatedChild = vi.fn()
  428. const key: InjectionKey<Ref<number>> = Symbol()
  429. const { render, host } = define({
  430. setup() {
  431. const count = ref(0)
  432. provide(key, count)
  433. onUpdated(() => handleUpdated())
  434. const n0 = createTextNode()
  435. renderEffect(() => setText(n0, count.value))
  436. const n1 = createComponent(Child, { count: () => count.value })
  437. return [n0, n1]
  438. },
  439. })
  440. let update: any
  441. const Child = {
  442. props: { count: Number },
  443. setup() {
  444. onUpdated(() => handleUpdatedChild())
  445. const count = inject(key)!
  446. update = () => count.value++
  447. const n2 = createTextNode()
  448. renderEffect(() => setText(n2, count.value))
  449. return n2
  450. },
  451. }
  452. render()
  453. expect(host.innerHTML).toBe('00')
  454. expect(handleUpdated).toHaveBeenCalledTimes(0)
  455. expect(handleUpdatedChild).toHaveBeenCalledTimes(0)
  456. update()
  457. await nextTick()
  458. expect(host.innerHTML).toBe('11')
  459. expect(handleUpdated).toHaveBeenCalledTimes(1)
  460. expect(handleUpdatedChild).toHaveBeenCalledTimes(1)
  461. })
  462. })