pdf-destroy-lock.test.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. import { PdfDestroyLock } from '@/features/source-editor/extensions/visual/utils/pdf-destroy-lock'
  2. import { expect } from 'chai'
  3. type Deferred<T> = {
  4. promise: Promise<T>
  5. resolve: (value: T) => void
  6. reject: (err: unknown) => void
  7. }
  8. function defer<T>(): Deferred<T> {
  9. let resolveRet!: (value: T) => void
  10. let rejectRet!: (err: unknown) => void
  11. const promise = new Promise<T>((resolve, reject) => {
  12. resolveRet = resolve
  13. rejectRet = reject
  14. })
  15. return { promise, resolve: resolveRet, reject: rejectRet }
  16. }
  17. // Flush microtasks so that .finally() handlers attached inside schedule()
  18. // run before we make assertions.
  19. const flush = () => new Promise(resolve => setTimeout(resolve, 0))
  20. describe('PdfDestroyLock', function () {
  21. describe('waitForPending', function () {
  22. it('resolves immediately when no destroys are in flight', async function () {
  23. const lock = new PdfDestroyLock()
  24. let resolved = false
  25. await lock.waitForPending().then(() => {
  26. resolved = true
  27. })
  28. expect(resolved).to.be.true
  29. })
  30. it('waits for a pending destroy to settle', async function () {
  31. const lock = new PdfDestroyLock()
  32. const deferred = defer<void>()
  33. lock.schedule(() => deferred.promise)
  34. let waited = false
  35. const wait = lock.waitForPending().then(() => {
  36. waited = true
  37. })
  38. await flush()
  39. expect(waited).to.be.false
  40. deferred.resolve()
  41. await wait
  42. expect(waited).to.be.true
  43. })
  44. it('waits for a destroy that rejects', async function () {
  45. const lock = new PdfDestroyLock()
  46. const deferred = defer<void>()
  47. // Swallow the rejection at the schedule() call site to mirror real usage,
  48. // where callers catch errors from destroy().
  49. lock.schedule(() => deferred.promise.catch(() => {}))
  50. let waited = false
  51. const wait = lock.waitForPending().then(() => {
  52. waited = true
  53. })
  54. await flush()
  55. expect(waited).to.be.false
  56. deferred.reject(new Error('boom'))
  57. await wait
  58. expect(waited).to.be.true
  59. })
  60. it('waits for destroys scheduled while waiting', async function () {
  61. const lock = new PdfDestroyLock()
  62. const first = defer<void>()
  63. const second = defer<void>()
  64. lock.schedule(() => first.promise)
  65. let waited = false
  66. const wait = lock.waitForPending().then(() => {
  67. waited = true
  68. })
  69. // Schedule a second destroy before the first completes - waitForPending
  70. // must loop and wait for this one too.
  71. lock.schedule(() => second.promise)
  72. first.resolve()
  73. await flush()
  74. expect(waited).to.be.false
  75. second.resolve()
  76. await wait
  77. expect(waited).to.be.true
  78. })
  79. it('waits for multiple concurrent destroys', async function () {
  80. const lock = new PdfDestroyLock()
  81. const first = defer<void>()
  82. const second = defer<void>()
  83. lock.schedule(() => first.promise)
  84. lock.schedule(() => second.promise)
  85. let waited = false
  86. const wait = lock.waitForPending().then(() => {
  87. waited = true
  88. })
  89. first.resolve()
  90. await flush()
  91. expect(waited).to.be.false
  92. second.resolve()
  93. await wait
  94. expect(waited).to.be.true
  95. })
  96. })
  97. describe('schedule', function () {
  98. it('invokes the destroy function immediately', function () {
  99. const lock = new PdfDestroyLock()
  100. let called = false
  101. lock.schedule(async () => {
  102. called = true
  103. })
  104. expect(called).to.be.true
  105. })
  106. it('returns the promise from the destroy function', async function () {
  107. const lock = new PdfDestroyLock()
  108. const result = await lock.schedule(async () => 'done')
  109. expect(result).to.equal('done')
  110. })
  111. it('releases the lock after a successful destroy', async function () {
  112. const lock = new PdfDestroyLock()
  113. await lock.schedule(async () => {})
  114. await flush()
  115. // waitForPending should now resolve synchronously - if the task was not
  116. // removed from the set, this would hang.
  117. let resolved = false
  118. await lock.waitForPending().then(() => {
  119. resolved = true
  120. })
  121. expect(resolved).to.be.true
  122. })
  123. it('releases the lock after a destroy that rejects', async function () {
  124. const lock = new PdfDestroyLock()
  125. // Mirror real usage: destroyFn swallows its own errors so the internal
  126. // .finally() chain in schedule() doesn't leak an unhandled rejection.
  127. await lock.schedule(() =>
  128. Promise.reject(new Error('boom')).catch(() => {})
  129. )
  130. await flush()
  131. let resolved = false
  132. await lock.waitForPending().then(() => {
  133. resolved = true
  134. })
  135. expect(resolved).to.be.true
  136. })
  137. })
  138. })