scheduler.spec.ts 12 KB

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