scheduler.spec.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  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. })
  207. describe('queuePostFlushCb', () => {
  208. it('basic usage', async () => {
  209. const calls: string[] = []
  210. const cb1 = () => {
  211. calls.push('cb1')
  212. }
  213. const cb2 = () => {
  214. calls.push('cb2')
  215. }
  216. const cb3 = () => {
  217. calls.push('cb3')
  218. }
  219. queuePostFlushCb([cb1, cb2])
  220. queuePostFlushCb(cb3)
  221. expect(calls).toEqual([])
  222. await nextTick()
  223. expect(calls).toEqual(['cb1', 'cb2', 'cb3'])
  224. })
  225. it('should dedupe queued postFlushCb', async () => {
  226. const calls: string[] = []
  227. const cb1 = () => {
  228. calls.push('cb1')
  229. }
  230. const cb2 = () => {
  231. calls.push('cb2')
  232. }
  233. const cb3 = () => {
  234. calls.push('cb3')
  235. }
  236. queuePostFlushCb([cb1, cb2])
  237. queuePostFlushCb(cb3)
  238. queuePostFlushCb([cb1, cb3])
  239. queuePostFlushCb(cb2)
  240. expect(calls).toEqual([])
  241. await nextTick()
  242. expect(calls).toEqual(['cb1', 'cb2', 'cb3'])
  243. })
  244. it('queuePostFlushCb while flushing', async () => {
  245. const calls: string[] = []
  246. const cb1 = () => {
  247. calls.push('cb1')
  248. // cb2 will be executed after cb1 at the same tick
  249. queuePostFlushCb(cb2)
  250. }
  251. const cb2 = () => {
  252. calls.push('cb2')
  253. }
  254. queuePostFlushCb(cb1)
  255. await nextTick()
  256. expect(calls).toEqual(['cb1', 'cb2'])
  257. })
  258. })
  259. describe('queueJob w/ queuePostFlushCb', () => {
  260. it('queueJob inside postFlushCb', async () => {
  261. const calls: string[] = []
  262. const job1 = () => {
  263. calls.push('job1')
  264. }
  265. const cb1 = () => {
  266. // queueJob in postFlushCb
  267. calls.push('cb1')
  268. queueJob(job1)
  269. }
  270. queuePostFlushCb(cb1)
  271. await nextTick()
  272. expect(calls).toEqual(['cb1', 'job1'])
  273. })
  274. it('queueJob & postFlushCb inside postFlushCb', async () => {
  275. const calls: string[] = []
  276. const job1 = () => {
  277. calls.push('job1')
  278. }
  279. const cb1 = () => {
  280. calls.push('cb1')
  281. queuePostFlushCb(cb2)
  282. // job1 will executed before cb2
  283. // Job has higher priority than postFlushCb
  284. queueJob(job1)
  285. }
  286. const cb2 = () => {
  287. calls.push('cb2')
  288. }
  289. queuePostFlushCb(cb1)
  290. await nextTick()
  291. expect(calls).toEqual(['cb1', 'job1', 'cb2'])
  292. })
  293. it('postFlushCb inside queueJob', async () => {
  294. const calls: string[] = []
  295. const job1 = () => {
  296. calls.push('job1')
  297. // postFlushCb in queueJob
  298. queuePostFlushCb(cb1)
  299. }
  300. const cb1 = () => {
  301. calls.push('cb1')
  302. }
  303. queueJob(job1)
  304. await nextTick()
  305. expect(calls).toEqual(['job1', 'cb1'])
  306. })
  307. it('queueJob & postFlushCb inside queueJob', async () => {
  308. const calls: string[] = []
  309. const job1 = () => {
  310. calls.push('job1')
  311. // cb1 will executed after job2
  312. // Job has higher priority than postFlushCb
  313. queuePostFlushCb(cb1)
  314. queueJob(job2)
  315. }
  316. const job2 = () => {
  317. calls.push('job2')
  318. }
  319. const cb1 = () => {
  320. calls.push('cb1')
  321. }
  322. queueJob(job1)
  323. await nextTick()
  324. expect(calls).toEqual(['job1', 'job2', 'cb1'])
  325. })
  326. it('nested queueJob w/ postFlushCb', async () => {
  327. const calls: string[] = []
  328. const job1 = () => {
  329. calls.push('job1')
  330. queuePostFlushCb(cb1)
  331. queueJob(job2)
  332. }
  333. const job2 = () => {
  334. calls.push('job2')
  335. queuePostFlushCb(cb2)
  336. }
  337. const cb1 = () => {
  338. calls.push('cb1')
  339. }
  340. const cb2 = () => {
  341. calls.push('cb2')
  342. }
  343. queueJob(job1)
  344. await nextTick()
  345. expect(calls).toEqual(['job1', 'job2', 'cb1', 'cb2'])
  346. })
  347. })
  348. test('invalidateJob', async () => {
  349. const calls: string[] = []
  350. const job1 = () => {
  351. calls.push('job1')
  352. invalidateJob(job2)
  353. job2()
  354. }
  355. const job2 = () => {
  356. calls.push('job2')
  357. }
  358. const job3 = () => {
  359. calls.push('job3')
  360. }
  361. const job4 = () => {
  362. calls.push('job4')
  363. }
  364. // queue all jobs
  365. queueJob(job1)
  366. queueJob(job2)
  367. queueJob(job3)
  368. queuePostFlushCb(job4)
  369. expect(calls).toEqual([])
  370. await nextTick()
  371. // job2 should be called only once
  372. expect(calls).toEqual(['job1', 'job2', 'job3', 'job4'])
  373. })
  374. test('sort job based on id', async () => {
  375. const calls: string[] = []
  376. const job1 = () => calls.push('job1')
  377. // job1 has no id
  378. const job2 = () => calls.push('job2')
  379. job2.id = 2
  380. const job3 = () => calls.push('job3')
  381. job3.id = 1
  382. queueJob(job1)
  383. queueJob(job2)
  384. queueJob(job3)
  385. await nextTick()
  386. expect(calls).toEqual(['job3', 'job2', 'job1'])
  387. })
  388. test('sort SchedulerCbs based on id', async () => {
  389. const calls: string[] = []
  390. const cb1 = () => calls.push('cb1')
  391. // cb1 has no id
  392. const cb2 = () => calls.push('cb2')
  393. cb2.id = 2
  394. const cb3 = () => calls.push('cb3')
  395. cb3.id = 1
  396. queuePostFlushCb(cb1)
  397. queuePostFlushCb(cb2)
  398. queuePostFlushCb(cb3)
  399. await nextTick()
  400. expect(calls).toEqual(['cb3', 'cb2', 'cb1'])
  401. })
  402. // #1595
  403. test('avoid duplicate postFlushCb invocation', async () => {
  404. const calls: string[] = []
  405. const cb1 = () => {
  406. calls.push('cb1')
  407. queuePostFlushCb(cb2)
  408. }
  409. const cb2 = () => {
  410. calls.push('cb2')
  411. }
  412. queuePostFlushCb(cb1)
  413. queuePostFlushCb(cb2)
  414. await nextTick()
  415. expect(calls).toEqual(['cb1', 'cb2'])
  416. })
  417. test('nextTick should capture scheduler flush errors', async () => {
  418. const err = new Error('test')
  419. queueJob(() => {
  420. throw err
  421. })
  422. try {
  423. await nextTick()
  424. } catch (e) {
  425. expect(e).toBe(err)
  426. }
  427. expect(
  428. `Unhandled error during execution of scheduler flush`
  429. ).toHaveBeenWarned()
  430. // this one should no longer error
  431. await nextTick()
  432. })
  433. test('should prevent self-triggering jobs by default', async () => {
  434. let count = 0
  435. const job = () => {
  436. if (count < 3) {
  437. count++
  438. queueJob(job)
  439. }
  440. }
  441. queueJob(job)
  442. await nextTick()
  443. // only runs once - a job cannot queue itself
  444. expect(count).toBe(1)
  445. })
  446. test('should allow explicitly marked jobs to trigger itself', async () => {
  447. // normal job
  448. let count = 0
  449. const job = () => {
  450. if (count < 3) {
  451. count++
  452. queueJob(job)
  453. }
  454. }
  455. job.allowRecurse = true
  456. queueJob(job)
  457. await nextTick()
  458. expect(count).toBe(3)
  459. // post cb
  460. const cb = () => {
  461. if (count < 5) {
  462. count++
  463. queuePostFlushCb(cb)
  464. }
  465. }
  466. cb.allowRecurse = true
  467. queuePostFlushCb(cb)
  468. await nextTick()
  469. expect(count).toBe(5)
  470. })
  471. test('should prevent duplicate queue', async () => {
  472. let count = 0
  473. const job = () => {
  474. count++
  475. }
  476. job.cb = true
  477. queueJob(job)
  478. queueJob(job)
  479. await nextTick()
  480. expect(count).toBe(1)
  481. })
  482. // #1947 flushPostFlushCbs should handle nested calls
  483. // e.g. app.mount inside app.mount
  484. test('flushPostFlushCbs', async () => {
  485. let count = 0
  486. const queueAndFlush = (hook: Function) => {
  487. queuePostFlushCb(hook)
  488. flushPostFlushCbs()
  489. }
  490. queueAndFlush(() => {
  491. queueAndFlush(() => {
  492. count++
  493. })
  494. })
  495. await nextTick()
  496. expect(count).toBe(1)
  497. })
  498. })