scheduler.spec.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  1. import { vi } from 'vitest'
  2. import {
  3. queueJob,
  4. nextTick,
  5. queuePostFlushCb,
  6. invalidateJob,
  7. flushPostFlushCbs,
  8. flushPreFlushCbs
  9. } from '../src/scheduler'
  10. describe('scheduler', () => {
  11. it('nextTick', async () => {
  12. const calls: string[] = []
  13. const dummyThen = Promise.resolve().then()
  14. const job1 = () => {
  15. calls.push('job1')
  16. }
  17. const job2 = () => {
  18. calls.push('job2')
  19. }
  20. nextTick(job1)
  21. job2()
  22. expect(calls.length).toBe(1)
  23. await dummyThen
  24. // job1 will be pushed in nextTick
  25. expect(calls.length).toBe(2)
  26. expect(calls).toMatchObject(['job2', 'job1'])
  27. })
  28. describe('queueJob', () => {
  29. it('basic usage', async () => {
  30. const calls: string[] = []
  31. const job1 = () => {
  32. calls.push('job1')
  33. }
  34. const job2 = () => {
  35. calls.push('job2')
  36. }
  37. queueJob(job1)
  38. queueJob(job2)
  39. expect(calls).toEqual([])
  40. await nextTick()
  41. expect(calls).toEqual(['job1', 'job2'])
  42. })
  43. it("should insert jobs in ascending order of job's id when flushing", async () => {
  44. const calls: string[] = []
  45. const job1 = () => {
  46. calls.push('job1')
  47. queueJob(job2)
  48. queueJob(job3)
  49. }
  50. const job2 = () => {
  51. calls.push('job2')
  52. queueJob(job4)
  53. queueJob(job5)
  54. }
  55. job2.id = 10
  56. const job3 = () => {
  57. calls.push('job3')
  58. }
  59. job3.id = 1
  60. const job4 = () => {
  61. calls.push('job4')
  62. }
  63. const job5 = () => {
  64. calls.push('job5')
  65. }
  66. queueJob(job1)
  67. expect(calls).toEqual([])
  68. await nextTick()
  69. expect(calls).toEqual(['job1', 'job3', 'job2', 'job4', 'job5'])
  70. })
  71. it('should dedupe queued jobs', async () => {
  72. const calls: string[] = []
  73. const job1 = () => {
  74. calls.push('job1')
  75. }
  76. const job2 = () => {
  77. calls.push('job2')
  78. }
  79. queueJob(job1)
  80. queueJob(job2)
  81. queueJob(job1)
  82. queueJob(job2)
  83. expect(calls).toEqual([])
  84. await nextTick()
  85. expect(calls).toEqual(['job1', 'job2'])
  86. })
  87. it('queueJob while flushing', async () => {
  88. const calls: string[] = []
  89. const job1 = () => {
  90. calls.push('job1')
  91. // job2 will be executed after job1 at the same tick
  92. queueJob(job2)
  93. }
  94. const job2 = () => {
  95. calls.push('job2')
  96. }
  97. queueJob(job1)
  98. await nextTick()
  99. expect(calls).toEqual(['job1', 'job2'])
  100. })
  101. })
  102. describe('pre flush jobs', () => {
  103. it('queueJob inside preFlushCb', async () => {
  104. const calls: string[] = []
  105. const job1 = () => {
  106. calls.push('job1')
  107. }
  108. const cb1 = () => {
  109. // queueJob in postFlushCb
  110. calls.push('cb1')
  111. queueJob(job1)
  112. }
  113. cb1.pre = true
  114. queueJob(cb1)
  115. await nextTick()
  116. expect(calls).toEqual(['cb1', 'job1'])
  117. })
  118. it('queueJob & preFlushCb inside preFlushCb', async () => {
  119. const calls: string[] = []
  120. const job1 = () => {
  121. calls.push('job1')
  122. }
  123. job1.id = 1
  124. const cb1 = () => {
  125. calls.push('cb1')
  126. queueJob(job1)
  127. // cb2 should execute before the job
  128. queueJob(cb2)
  129. }
  130. cb1.pre = true
  131. const cb2 = () => {
  132. calls.push('cb2')
  133. }
  134. cb2.pre = true
  135. cb2.id = 1
  136. queueJob(cb1)
  137. await nextTick()
  138. expect(calls).toEqual(['cb1', 'cb2', 'job1'])
  139. })
  140. it('preFlushCb inside queueJob', async () => {
  141. const calls: string[] = []
  142. const job1 = () => {
  143. // the only case where a pre-flush cb can be queued inside a job is
  144. // when updating the props of a child component. This is handled
  145. // directly inside `updateComponentPreRender` to avoid non atomic
  146. // cb triggers (#1763)
  147. queueJob(cb1)
  148. queueJob(cb2)
  149. flushPreFlushCbs()
  150. calls.push('job1')
  151. }
  152. const cb1 = () => {
  153. calls.push('cb1')
  154. // a cb triggers its parent job, which should be skipped
  155. queueJob(job1)
  156. }
  157. cb1.pre = true
  158. const cb2 = () => {
  159. calls.push('cb2')
  160. }
  161. cb2.pre = true
  162. queueJob(job1)
  163. await nextTick()
  164. expect(calls).toEqual(['cb1', 'cb2', 'job1'])
  165. })
  166. // #3806
  167. it('queue preFlushCb inside postFlushCb', async () => {
  168. const spy = vi.fn()
  169. const cb = () => spy()
  170. cb.pre = true
  171. queuePostFlushCb(() => {
  172. queueJob(cb)
  173. })
  174. await nextTick()
  175. expect(spy).toHaveBeenCalled()
  176. })
  177. })
  178. describe('queuePostFlushCb', () => {
  179. it('basic usage', async () => {
  180. const calls: string[] = []
  181. const cb1 = () => {
  182. calls.push('cb1')
  183. }
  184. const cb2 = () => {
  185. calls.push('cb2')
  186. }
  187. const cb3 = () => {
  188. calls.push('cb3')
  189. }
  190. queuePostFlushCb([cb1, cb2])
  191. queuePostFlushCb(cb3)
  192. expect(calls).toEqual([])
  193. await nextTick()
  194. expect(calls).toEqual(['cb1', 'cb2', 'cb3'])
  195. })
  196. it('should dedupe queued postFlushCb', async () => {
  197. const calls: string[] = []
  198. const cb1 = () => {
  199. calls.push('cb1')
  200. }
  201. const cb2 = () => {
  202. calls.push('cb2')
  203. }
  204. const cb3 = () => {
  205. calls.push('cb3')
  206. }
  207. queuePostFlushCb([cb1, cb2])
  208. queuePostFlushCb(cb3)
  209. queuePostFlushCb([cb1, cb3])
  210. queuePostFlushCb(cb2)
  211. expect(calls).toEqual([])
  212. await nextTick()
  213. expect(calls).toEqual(['cb1', 'cb2', 'cb3'])
  214. })
  215. it('queuePostFlushCb while flushing', async () => {
  216. const calls: string[] = []
  217. const cb1 = () => {
  218. calls.push('cb1')
  219. // cb2 will be executed after cb1 at the same tick
  220. queuePostFlushCb(cb2)
  221. }
  222. const cb2 = () => {
  223. calls.push('cb2')
  224. }
  225. queuePostFlushCb(cb1)
  226. await nextTick()
  227. expect(calls).toEqual(['cb1', 'cb2'])
  228. })
  229. })
  230. describe('queueJob w/ queuePostFlushCb', () => {
  231. it('queueJob inside postFlushCb', async () => {
  232. const calls: string[] = []
  233. const job1 = () => {
  234. calls.push('job1')
  235. }
  236. const cb1 = () => {
  237. // queueJob in postFlushCb
  238. calls.push('cb1')
  239. queueJob(job1)
  240. }
  241. queuePostFlushCb(cb1)
  242. await nextTick()
  243. expect(calls).toEqual(['cb1', 'job1'])
  244. })
  245. it('queueJob & postFlushCb inside postFlushCb', async () => {
  246. const calls: string[] = []
  247. const job1 = () => {
  248. calls.push('job1')
  249. }
  250. const cb1 = () => {
  251. calls.push('cb1')
  252. queuePostFlushCb(cb2)
  253. // job1 will executed before cb2
  254. // Job has higher priority than postFlushCb
  255. queueJob(job1)
  256. }
  257. const cb2 = () => {
  258. calls.push('cb2')
  259. }
  260. queuePostFlushCb(cb1)
  261. await nextTick()
  262. expect(calls).toEqual(['cb1', 'job1', 'cb2'])
  263. })
  264. it('postFlushCb inside queueJob', async () => {
  265. const calls: string[] = []
  266. const job1 = () => {
  267. calls.push('job1')
  268. // postFlushCb in queueJob
  269. queuePostFlushCb(cb1)
  270. }
  271. const cb1 = () => {
  272. calls.push('cb1')
  273. }
  274. queueJob(job1)
  275. await nextTick()
  276. expect(calls).toEqual(['job1', 'cb1'])
  277. })
  278. it('queueJob & postFlushCb inside queueJob', async () => {
  279. const calls: string[] = []
  280. const job1 = () => {
  281. calls.push('job1')
  282. // cb1 will executed after job2
  283. // Job has higher priority than postFlushCb
  284. queuePostFlushCb(cb1)
  285. queueJob(job2)
  286. }
  287. const job2 = () => {
  288. calls.push('job2')
  289. }
  290. const cb1 = () => {
  291. calls.push('cb1')
  292. }
  293. queueJob(job1)
  294. await nextTick()
  295. expect(calls).toEqual(['job1', 'job2', 'cb1'])
  296. })
  297. it('nested queueJob w/ postFlushCb', async () => {
  298. const calls: string[] = []
  299. const job1 = () => {
  300. calls.push('job1')
  301. queuePostFlushCb(cb1)
  302. queueJob(job2)
  303. }
  304. const job2 = () => {
  305. calls.push('job2')
  306. queuePostFlushCb(cb2)
  307. }
  308. const cb1 = () => {
  309. calls.push('cb1')
  310. }
  311. const cb2 = () => {
  312. calls.push('cb2')
  313. }
  314. queueJob(job1)
  315. await nextTick()
  316. expect(calls).toEqual(['job1', 'job2', 'cb1', 'cb2'])
  317. })
  318. })
  319. test('invalidateJob', async () => {
  320. const calls: string[] = []
  321. const job1 = () => {
  322. calls.push('job1')
  323. invalidateJob(job2)
  324. job2()
  325. }
  326. const job2 = () => {
  327. calls.push('job2')
  328. }
  329. const job3 = () => {
  330. calls.push('job3')
  331. }
  332. const job4 = () => {
  333. calls.push('job4')
  334. }
  335. // queue all jobs
  336. queueJob(job1)
  337. queueJob(job2)
  338. queueJob(job3)
  339. queuePostFlushCb(job4)
  340. expect(calls).toEqual([])
  341. await nextTick()
  342. // job2 should be called only once
  343. expect(calls).toEqual(['job1', 'job2', 'job3', 'job4'])
  344. })
  345. test('sort job based on id', async () => {
  346. const calls: string[] = []
  347. const job1 = () => calls.push('job1')
  348. // job1 has no id
  349. const job2 = () => calls.push('job2')
  350. job2.id = 2
  351. const job3 = () => calls.push('job3')
  352. job3.id = 1
  353. queueJob(job1)
  354. queueJob(job2)
  355. queueJob(job3)
  356. await nextTick()
  357. expect(calls).toEqual(['job3', 'job2', 'job1'])
  358. })
  359. test('sort SchedulerCbs based on id', async () => {
  360. const calls: string[] = []
  361. const cb1 = () => calls.push('cb1')
  362. // cb1 has no id
  363. const cb2 = () => calls.push('cb2')
  364. cb2.id = 2
  365. const cb3 = () => calls.push('cb3')
  366. cb3.id = 1
  367. queuePostFlushCb(cb1)
  368. queuePostFlushCb(cb2)
  369. queuePostFlushCb(cb3)
  370. await nextTick()
  371. expect(calls).toEqual(['cb3', 'cb2', 'cb1'])
  372. })
  373. // #1595
  374. test('avoid duplicate postFlushCb invocation', async () => {
  375. const calls: string[] = []
  376. const cb1 = () => {
  377. calls.push('cb1')
  378. queuePostFlushCb(cb2)
  379. }
  380. const cb2 = () => {
  381. calls.push('cb2')
  382. }
  383. queuePostFlushCb(cb1)
  384. queuePostFlushCb(cb2)
  385. await nextTick()
  386. expect(calls).toEqual(['cb1', 'cb2'])
  387. })
  388. test('nextTick should capture scheduler flush errors', async () => {
  389. const err = new Error('test')
  390. queueJob(() => {
  391. throw err
  392. })
  393. try {
  394. await nextTick()
  395. } catch (e: any) {
  396. expect(e).toBe(err)
  397. }
  398. expect(
  399. `Unhandled error during execution of scheduler flush`
  400. ).toHaveBeenWarned()
  401. // this one should no longer error
  402. await nextTick()
  403. })
  404. test('should prevent self-triggering jobs by default', async () => {
  405. let count = 0
  406. const job = () => {
  407. if (count < 3) {
  408. count++
  409. queueJob(job)
  410. }
  411. }
  412. queueJob(job)
  413. await nextTick()
  414. // only runs once - a job cannot queue itself
  415. expect(count).toBe(1)
  416. })
  417. test('should allow explicitly marked jobs to trigger itself', async () => {
  418. // normal job
  419. let count = 0
  420. const job = () => {
  421. if (count < 3) {
  422. count++
  423. queueJob(job)
  424. }
  425. }
  426. job.allowRecurse = true
  427. queueJob(job)
  428. await nextTick()
  429. expect(count).toBe(3)
  430. // post cb
  431. const cb = () => {
  432. if (count < 5) {
  433. count++
  434. queuePostFlushCb(cb)
  435. }
  436. }
  437. cb.allowRecurse = true
  438. queuePostFlushCb(cb)
  439. await nextTick()
  440. expect(count).toBe(5)
  441. })
  442. // #1947 flushPostFlushCbs should handle nested calls
  443. // e.g. app.mount inside app.mount
  444. test('flushPostFlushCbs', async () => {
  445. let count = 0
  446. const queueAndFlush = (hook: Function) => {
  447. queuePostFlushCb(hook)
  448. flushPostFlushCbs()
  449. }
  450. queueAndFlush(() => {
  451. queueAndFlush(() => {
  452. count++
  453. })
  454. })
  455. await nextTick()
  456. expect(count).toBe(1)
  457. })
  458. // #910
  459. test('should not run stopped reactive effects', async () => {
  460. const spy = vi.fn()
  461. // simulate parent component that toggles child
  462. const job1 = () => {
  463. // @ts-ignore
  464. job2.active = false
  465. }
  466. // simulate child that's triggered by the same reactive change that
  467. // triggers its toggle
  468. const job2 = () => spy()
  469. expect(spy).toHaveBeenCalledTimes(0)
  470. queueJob(job1)
  471. queueJob(job2)
  472. await nextTick()
  473. // should not be called
  474. expect(spy).toHaveBeenCalledTimes(0)
  475. })
  476. it('flushPreFlushCbs inside a pre job', async () => {
  477. const spy = vi.fn()
  478. const job = () => {
  479. spy()
  480. flushPreFlushCbs()
  481. }
  482. job.pre = true
  483. queueJob(job)
  484. await nextTick()
  485. expect(spy).toHaveBeenCalledTimes(1)
  486. })
  487. })