scheduler.spec.ts 13 KB

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