scheduler.spec.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798
  1. import {
  2. type SchedulerJob,
  3. SchedulerJobFlags,
  4. flushPostFlushCbs,
  5. flushPreFlushCbs,
  6. nextTick,
  7. queueJob,
  8. queuePostFlushCb,
  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, 10)
  48. queueJob(job3, 1)
  49. }
  50. const job2 = () => {
  51. calls.push('job2')
  52. queueJob(job4)
  53. queueJob(job5)
  54. }
  55. const job3 = () => {
  56. calls.push('job3')
  57. }
  58. const job4 = () => {
  59. calls.push('job4')
  60. }
  61. const job5 = () => {
  62. calls.push('job5')
  63. }
  64. queueJob(job1)
  65. expect(calls).toEqual([])
  66. await nextTick()
  67. expect(calls).toEqual(['job1', 'job3', 'job2', 'job4', 'job5'])
  68. })
  69. it('should dedupe queued jobs', async () => {
  70. const calls: string[] = []
  71. const job1 = () => {
  72. calls.push('job1')
  73. }
  74. const job2 = () => {
  75. calls.push('job2')
  76. }
  77. queueJob(job1)
  78. queueJob(job2)
  79. queueJob(job1)
  80. queueJob(job2)
  81. expect(calls).toEqual([])
  82. await nextTick()
  83. expect(calls).toEqual(['job1', 'job2'])
  84. })
  85. it('queueJob while flushing', async () => {
  86. const calls: string[] = []
  87. const job1 = () => {
  88. calls.push('job1')
  89. // job2 will be executed after job1 at the same tick
  90. queueJob(job2)
  91. }
  92. const job2 = () => {
  93. calls.push('job2')
  94. }
  95. queueJob(job1)
  96. await nextTick()
  97. expect(calls).toEqual(['job1', 'job2'])
  98. })
  99. })
  100. describe('pre flush jobs', () => {
  101. it('queueJob inside preFlushCb', async () => {
  102. const calls: string[] = []
  103. const job1 = () => {
  104. calls.push('job1')
  105. }
  106. const cb1: SchedulerJob = () => {
  107. // queueJob in postFlushCb
  108. calls.push('cb1')
  109. queueJob(job1)
  110. }
  111. queueJob(cb1, undefined, true)
  112. await nextTick()
  113. expect(calls).toEqual(['cb1', 'job1'])
  114. })
  115. it('queueJob & preFlushCb inside preFlushCb', async () => {
  116. const calls: string[] = []
  117. const job1 = () => {
  118. calls.push('job1')
  119. }
  120. const cb1: SchedulerJob = () => {
  121. calls.push('cb1')
  122. queueJob(job1, 1)
  123. // cb2 should execute before the job
  124. queueJob(cb2, 1, true)
  125. queueJob(cb3, 1, true)
  126. }
  127. const cb2: SchedulerJob = () => {
  128. calls.push('cb2')
  129. }
  130. const cb3: SchedulerJob = () => {
  131. calls.push('cb3')
  132. }
  133. queueJob(cb1, undefined, true)
  134. await nextTick()
  135. expect(calls).toEqual(['cb1', 'cb2', 'cb3', 'job1'])
  136. })
  137. it('should insert jobs after pre jobs with the same id', async () => {
  138. const calls: string[] = []
  139. const job1: SchedulerJob = () => {
  140. calls.push('job1')
  141. }
  142. const job2: SchedulerJob = () => {
  143. calls.push('job2')
  144. queueJob(job5, 2)
  145. queueJob(job6, 2, true)
  146. }
  147. const job3: SchedulerJob = () => {
  148. calls.push('job3')
  149. }
  150. const job4: SchedulerJob = () => {
  151. calls.push('job4')
  152. }
  153. const job5: SchedulerJob = () => {
  154. calls.push('job5')
  155. }
  156. const job6: SchedulerJob = () => {
  157. calls.push('job6')
  158. }
  159. // We need several jobs to test this properly, otherwise
  160. // findInsertionIndex can yield the correct index by chance
  161. queueJob(job4, 3, true)
  162. queueJob(job2, 2, true)
  163. queueJob(job3, 2, true)
  164. queueJob(job1, 1, true)
  165. await nextTick()
  166. expect(calls).toEqual(['job1', 'job2', 'job3', 'job6', 'job5', 'job4'])
  167. })
  168. it('preFlushCb inside queueJob', async () => {
  169. const calls: string[] = []
  170. const job1 = () => {
  171. // the only case where a pre-flush cb can be queued inside a job is
  172. // when updating the props of a child component. This is handled
  173. // directly inside `updateComponentPreRender` to avoid non atomic
  174. // cb triggers (#1763)
  175. queueJob(cb1, undefined, true)
  176. queueJob(cb2, undefined, true)
  177. flushPreFlushCbs()
  178. calls.push('job1')
  179. }
  180. const cb1: SchedulerJob = () => {
  181. calls.push('cb1')
  182. // a cb triggers its parent job, which should be skipped
  183. queueJob(job1)
  184. }
  185. const cb2: SchedulerJob = () => {
  186. calls.push('cb2')
  187. }
  188. queueJob(job1)
  189. await nextTick()
  190. expect(calls).toEqual(['cb1', 'cb2', 'job1'])
  191. })
  192. it('should insert pre jobs without ids first during flushing', async () => {
  193. const calls: string[] = []
  194. const job1: SchedulerJob = () => {
  195. calls.push('job1')
  196. queueJob(job3, undefined, true)
  197. queueJob(job4, undefined, true)
  198. }
  199. // job1 has no id
  200. const job2: SchedulerJob = () => {
  201. calls.push('job2')
  202. }
  203. const job3: SchedulerJob = () => {
  204. calls.push('job3')
  205. }
  206. // job3 has no id
  207. const job4: SchedulerJob = () => {
  208. calls.push('job4')
  209. }
  210. // job4 has no id
  211. queueJob(job1, undefined, true)
  212. queueJob(job2, 1, true)
  213. await nextTick()
  214. expect(calls).toEqual(['job1', 'job3', 'job4', 'job2'])
  215. })
  216. // #3806
  217. it('queue preFlushCb inside postFlushCb', async () => {
  218. const spy = vi.fn()
  219. const cb: SchedulerJob = () => spy()
  220. queuePostFlushCb(() => {
  221. queueJob(cb, undefined, true)
  222. })
  223. await nextTick()
  224. expect(spy).toHaveBeenCalled()
  225. })
  226. })
  227. describe('queuePostFlushCb', () => {
  228. it('basic usage', async () => {
  229. const calls: string[] = []
  230. const cb1 = () => {
  231. calls.push('cb1')
  232. }
  233. const cb2 = () => {
  234. calls.push('cb2')
  235. }
  236. const cb3 = () => {
  237. calls.push('cb3')
  238. }
  239. queuePostFlushCb([cb1, cb2])
  240. queuePostFlushCb(cb3)
  241. expect(calls).toEqual([])
  242. await nextTick()
  243. expect(calls).toEqual(['cb1', 'cb2', 'cb3'])
  244. })
  245. it('should dedupe queued postFlushCb', async () => {
  246. const calls: string[] = []
  247. const cb1 = () => {
  248. calls.push('cb1')
  249. }
  250. const cb2 = () => {
  251. calls.push('cb2')
  252. }
  253. const cb3 = () => {
  254. calls.push('cb3')
  255. }
  256. queuePostFlushCb([cb1, cb2])
  257. queuePostFlushCb(cb3)
  258. queuePostFlushCb([cb1, cb3])
  259. queuePostFlushCb(cb2)
  260. expect(calls).toEqual([])
  261. await nextTick()
  262. expect(calls).toEqual(['cb1', 'cb2', 'cb3'])
  263. })
  264. it('queuePostFlushCb while flushing', async () => {
  265. const calls: string[] = []
  266. const cb1 = () => {
  267. calls.push('cb1')
  268. // cb2 will be executed after cb1 at the same tick
  269. queuePostFlushCb(cb2)
  270. }
  271. const cb2 = () => {
  272. calls.push('cb2')
  273. }
  274. queuePostFlushCb(cb1)
  275. await nextTick()
  276. expect(calls).toEqual(['cb1', 'cb2'])
  277. })
  278. })
  279. describe('queueJob w/ queuePostFlushCb', () => {
  280. it('queueJob inside postFlushCb', async () => {
  281. const calls: string[] = []
  282. const job1 = () => {
  283. calls.push('job1')
  284. }
  285. const cb1 = () => {
  286. // queueJob in postFlushCb
  287. calls.push('cb1')
  288. queueJob(job1)
  289. }
  290. queuePostFlushCb(cb1)
  291. await nextTick()
  292. expect(calls).toEqual(['cb1', 'job1'])
  293. })
  294. it('queueJob & postFlushCb inside postFlushCb', async () => {
  295. const calls: string[] = []
  296. const job1 = () => {
  297. calls.push('job1')
  298. }
  299. const cb1 = () => {
  300. calls.push('cb1')
  301. queuePostFlushCb(cb2)
  302. // job1 will executed before cb2
  303. // Job has higher priority than postFlushCb
  304. queueJob(job1)
  305. }
  306. const cb2 = () => {
  307. calls.push('cb2')
  308. }
  309. queuePostFlushCb(cb1)
  310. await nextTick()
  311. expect(calls).toEqual(['cb1', 'job1', 'cb2'])
  312. })
  313. it('postFlushCb inside queueJob', async () => {
  314. const calls: string[] = []
  315. const job1 = () => {
  316. calls.push('job1')
  317. // postFlushCb in queueJob
  318. queuePostFlushCb(cb1)
  319. }
  320. const cb1 = () => {
  321. calls.push('cb1')
  322. }
  323. queueJob(job1)
  324. await nextTick()
  325. expect(calls).toEqual(['job1', 'cb1'])
  326. })
  327. it('queueJob & postFlushCb inside queueJob', async () => {
  328. const calls: string[] = []
  329. const job1 = () => {
  330. calls.push('job1')
  331. // cb1 will executed after job2
  332. // Job has higher priority than postFlushCb
  333. queuePostFlushCb(cb1)
  334. queueJob(job2)
  335. }
  336. const job2 = () => {
  337. calls.push('job2')
  338. }
  339. const cb1 = () => {
  340. calls.push('cb1')
  341. }
  342. queueJob(job1)
  343. await nextTick()
  344. expect(calls).toEqual(['job1', 'job2', 'cb1'])
  345. })
  346. it('nested queueJob w/ postFlushCb', async () => {
  347. const calls: string[] = []
  348. const job1 = () => {
  349. calls.push('job1')
  350. queuePostFlushCb(cb1)
  351. queueJob(job2)
  352. }
  353. const job2 = () => {
  354. calls.push('job2')
  355. queuePostFlushCb(cb2)
  356. }
  357. const cb1 = () => {
  358. calls.push('cb1')
  359. }
  360. const cb2 = () => {
  361. calls.push('cb2')
  362. }
  363. queueJob(job1)
  364. await nextTick()
  365. expect(calls).toEqual(['job1', 'job2', 'cb1', 'cb2'])
  366. })
  367. test('jobs added during post flush are ordered correctly', async () => {
  368. const calls: string[] = []
  369. const job1: SchedulerJob = () => {
  370. calls.push('job1')
  371. }
  372. const job2: SchedulerJob = () => {
  373. calls.push('job2')
  374. }
  375. queuePostFlushCb(() => {
  376. queueJob(job2, 2)
  377. queueJob(job1, 1)
  378. })
  379. await nextTick()
  380. expect(calls).toEqual(['job1', 'job2'])
  381. })
  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. const job3 = () => calls.push('job3')
  389. const job4: SchedulerJob = () => calls.push('job4')
  390. const job5: SchedulerJob = () => calls.push('job5')
  391. // job5 has no id
  392. queueJob(job1)
  393. queueJob(job2, 2)
  394. queueJob(job3, 1)
  395. queueJob(job4, 2, true)
  396. queueJob(job5, undefined, true)
  397. await nextTick()
  398. expect(calls).toEqual(['job5', 'job3', 'job4', '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. const cb3 = () => calls.push('cb3')
  406. queuePostFlushCb(cb1)
  407. queuePostFlushCb(cb2, 2)
  408. queuePostFlushCb(cb3, 1)
  409. await nextTick()
  410. expect(calls).toEqual(['cb3', 'cb2', 'cb1'])
  411. })
  412. // #1595
  413. test('avoid duplicate postFlushCb invocation', async () => {
  414. const calls: string[] = []
  415. const cb1 = () => {
  416. calls.push('cb1')
  417. queuePostFlushCb(cb2)
  418. }
  419. const cb2 = () => {
  420. calls.push('cb2')
  421. }
  422. queuePostFlushCb(cb1)
  423. queuePostFlushCb(cb2)
  424. await nextTick()
  425. expect(calls).toEqual(['cb1', 'cb2'])
  426. })
  427. test('nextTick should capture scheduler flush errors', async () => {
  428. const err = new Error('test')
  429. queueJob(() => {
  430. throw err
  431. })
  432. try {
  433. await nextTick()
  434. } catch (e: any) {
  435. expect(e).toBe(err)
  436. }
  437. expect(
  438. `Unhandled error during execution of scheduler flush`,
  439. ).toHaveBeenWarned()
  440. // this one should no longer error
  441. await nextTick()
  442. })
  443. test('jobs can be re-queued after an error', async () => {
  444. const err = new Error('test')
  445. let shouldThrow = true
  446. const job1: SchedulerJob = vi.fn(() => {
  447. if (shouldThrow) {
  448. shouldThrow = false
  449. throw err
  450. }
  451. })
  452. const job2: SchedulerJob = vi.fn()
  453. queueJob(job1, 1)
  454. queueJob(job2, 2)
  455. try {
  456. await nextTick()
  457. } catch (e: any) {
  458. expect(e).toBe(err)
  459. }
  460. expect(
  461. `Unhandled error during execution of scheduler flush`,
  462. ).toHaveBeenWarned()
  463. expect(job1).toHaveBeenCalledTimes(1)
  464. expect(job2).toHaveBeenCalledTimes(0)
  465. queueJob(job1, 1)
  466. queueJob(job2, 2)
  467. await nextTick()
  468. expect(job1).toHaveBeenCalledTimes(2)
  469. expect(job2).toHaveBeenCalledTimes(1)
  470. })
  471. test('should prevent self-triggering jobs by default', async () => {
  472. let count = 0
  473. const job = () => {
  474. if (count < 3) {
  475. count++
  476. queueJob(job)
  477. }
  478. }
  479. queueJob(job)
  480. await nextTick()
  481. // only runs once - a job cannot queue itself
  482. expect(count).toBe(1)
  483. })
  484. test('should allow explicitly marked jobs to trigger itself', async () => {
  485. // normal job
  486. let count = 0
  487. const job: SchedulerJob = () => {
  488. if (count < 3) {
  489. count++
  490. queueJob(job)
  491. }
  492. }
  493. job.flags! |= SchedulerJobFlags.ALLOW_RECURSE
  494. queueJob(job)
  495. await nextTick()
  496. expect(count).toBe(3)
  497. // post cb
  498. const cb: SchedulerJob = () => {
  499. if (count < 5) {
  500. count++
  501. queuePostFlushCb(cb)
  502. }
  503. }
  504. cb.flags! |= SchedulerJobFlags.ALLOW_RECURSE
  505. queuePostFlushCb(cb)
  506. await nextTick()
  507. expect(count).toBe(5)
  508. })
  509. test('recursive jobs can only be queued once non-recursively', async () => {
  510. const job: SchedulerJob = vi.fn()
  511. job.flags = SchedulerJobFlags.ALLOW_RECURSE
  512. queueJob(job, 1)
  513. queueJob(job, 1)
  514. await nextTick()
  515. expect(job).toHaveBeenCalledTimes(1)
  516. })
  517. test('recursive jobs can only be queued once recursively', async () => {
  518. let recurse = true
  519. const job: SchedulerJob = vi.fn(() => {
  520. if (recurse) {
  521. queueJob(job, 1)
  522. queueJob(job, 1)
  523. recurse = false
  524. }
  525. })
  526. job.flags = SchedulerJobFlags.ALLOW_RECURSE
  527. queueJob(job, 1)
  528. await nextTick()
  529. expect(job).toHaveBeenCalledTimes(2)
  530. })
  531. test(`recursive jobs can't be re-queued by other jobs`, async () => {
  532. let recurse = true
  533. const job1: SchedulerJob = () => {
  534. if (recurse) {
  535. // job2 is already queued, so this shouldn't do anything
  536. queueJob(job2, 2)
  537. recurse = false
  538. }
  539. }
  540. const job2: SchedulerJob = vi.fn(() => {
  541. if (recurse) {
  542. queueJob(job1, 1)
  543. queueJob(job2, 2)
  544. }
  545. })
  546. job2.flags = SchedulerJobFlags.ALLOW_RECURSE
  547. queueJob(job2, 2)
  548. await nextTick()
  549. expect(job2).toHaveBeenCalledTimes(2)
  550. })
  551. test('jobs are de-duplicated correctly when calling flushPreFlushCbs', async () => {
  552. let recurse = true
  553. const job1: SchedulerJob = vi.fn(() => {
  554. queueJob(job3, 3, true)
  555. queueJob(job3, 3, true)
  556. flushPreFlushCbs()
  557. })
  558. const job2: SchedulerJob = vi.fn(() => {
  559. if (recurse) {
  560. // job2 does not allow recurse, so this shouldn't do anything
  561. queueJob(job2, 2, true)
  562. // job3 is already queued, so this shouldn't do anything
  563. queueJob(job3, 3, true)
  564. recurse = false
  565. }
  566. })
  567. const job3: SchedulerJob = vi.fn(() => {
  568. if (recurse) {
  569. queueJob(job2, 2, true)
  570. queueJob(job3, 3, true)
  571. // The jobs are already queued, so these should have no effect
  572. queueJob(job2, 2, true)
  573. queueJob(job3, 3, true)
  574. }
  575. })
  576. job3.flags = SchedulerJobFlags.ALLOW_RECURSE
  577. queueJob(job1, 1, true)
  578. await nextTick()
  579. expect(job1).toHaveBeenCalledTimes(1)
  580. expect(job2).toHaveBeenCalledTimes(1)
  581. expect(job3).toHaveBeenCalledTimes(2)
  582. })
  583. // #1947 flushPostFlushCbs should handle nested calls
  584. // e.g. app.mount inside app.mount
  585. test('flushPostFlushCbs', async () => {
  586. let count = 0
  587. const queueAndFlush = (hook: Function) => {
  588. queuePostFlushCb(hook)
  589. flushPostFlushCbs()
  590. }
  591. queueAndFlush(() => {
  592. queueAndFlush(() => {
  593. count++
  594. })
  595. })
  596. await nextTick()
  597. expect(count).toBe(1)
  598. })
  599. // #910
  600. test('should not run stopped reactive effects', async () => {
  601. const spy = vi.fn()
  602. // simulate parent component that toggles child
  603. const job1 = () => {
  604. // @ts-expect-error
  605. job2.flags! |= SchedulerJobFlags.DISPOSED
  606. }
  607. // simulate child that's triggered by the same reactive change that
  608. // triggers its toggle
  609. const job2 = () => spy()
  610. expect(spy).toHaveBeenCalledTimes(0)
  611. queueJob(job1)
  612. queueJob(job2)
  613. await nextTick()
  614. // should not be called
  615. expect(spy).toHaveBeenCalledTimes(0)
  616. })
  617. it('flushPreFlushCbs inside a pre job', async () => {
  618. const spy = vi.fn()
  619. const job: SchedulerJob = () => {
  620. spy()
  621. flushPreFlushCbs()
  622. }
  623. queueJob(job, undefined, true)
  624. await nextTick()
  625. expect(spy).toHaveBeenCalledTimes(1)
  626. })
  627. test('flushPreFlushCbs inside a post job', async () => {
  628. const calls: string[] = []
  629. const callsAfterFlush: string[] = []
  630. const job1: SchedulerJob = () => {
  631. calls.push('job1')
  632. }
  633. const job2: SchedulerJob = () => {
  634. calls.push('job2')
  635. }
  636. queuePostFlushCb(() => {
  637. queueJob(job2, 2, true)
  638. queueJob(job1, 1, true)
  639. // e.g. nested app.mount() call
  640. flushPreFlushCbs()
  641. callsAfterFlush.push(...calls)
  642. })
  643. await nextTick()
  644. expect(callsAfterFlush).toEqual(['job1', 'job2'])
  645. expect(calls).toEqual(['job1', 'job2'])
  646. })
  647. it('nextTick should return promise', async () => {
  648. const fn = vi.fn(() => {
  649. return 1
  650. })
  651. const p = nextTick(fn)
  652. expect(p).toBeInstanceOf(Promise)
  653. expect(await p).toBe(1)
  654. expect(fn).toHaveBeenCalledTimes(1)
  655. })
  656. // #10003
  657. test('nested flushPostFlushCbs', async () => {
  658. const calls: string[] = []
  659. const cb1 = () => calls.push('cb1')
  660. // cb1 has no id
  661. const cb2 = () => calls.push('cb2')
  662. const queueAndFlush = (hook: Function) => {
  663. queuePostFlushCb(hook)
  664. flushPostFlushCbs()
  665. }
  666. queueAndFlush(() => {
  667. queuePostFlushCb(cb1)
  668. queuePostFlushCb(cb2, -1)
  669. flushPostFlushCbs()
  670. })
  671. await nextTick()
  672. expect(calls).toEqual(['cb2', 'cb1'])
  673. })
  674. test('error in postFlush cb should not cause nextTick to stuck in rejected state forever', async () => {
  675. queuePostFlushCb(() => {
  676. throw 'err'
  677. })
  678. await expect(nextTick).rejects.toThrow('err')
  679. await expect(nextTick()).resolves.toBeUndefined()
  680. })
  681. })