scheduler.spec.ts 12 KB

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