scheduler.spec.ts 13 KB

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