scheduler.spec.ts 14 KB

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