encoding.spec.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. import { describe, expect, it, vi } from 'vitest'
  2. import { CompressionLimiter } from '../src/compression-limiter.ts'
  3. import { encodeFirstWithinLimit, isExhaustedEncoding } from '../src/encoding.ts'
  4. describe('lazy image encoding', () => {
  5. it('does not execute fallback qualities after the first fitting candidate', async () => {
  6. const first = vi.fn(() => Promise.resolve({ data: new Uint8Array(8), quality: 85 }))
  7. const fallback = vi.fn(() => Promise.resolve({ data: new Uint8Array(4), quality: 80 }))
  8. await expect(encodeFirstWithinLimit([first, fallback], 8)).resolves.toMatchObject({ quality: 85 })
  9. expect(first).toHaveBeenCalledTimes(1)
  10. expect(fallback).not.toHaveBeenCalled()
  11. })
  12. it('executes later candidates only after earlier candidates exceed the cap', async () => {
  13. const first = vi.fn(() => Promise.resolve({ data: new Uint8Array(12), quality: 85 }))
  14. const second = vi.fn(() => Promise.resolve({ data: new Uint8Array(7), quality: 80 }))
  15. const third = vi.fn(() => Promise.resolve({ data: new Uint8Array(5), quality: 75 }))
  16. await expect(encodeFirstWithinLimit([first, second, third], 8)).resolves.toMatchObject({ quality: 80 })
  17. expect(first).toHaveBeenCalledTimes(1)
  18. expect(second).toHaveBeenCalledTimes(1)
  19. expect(third).not.toHaveBeenCalled()
  20. })
  21. it('rejects an empty candidate list and reports the smallest exhausted candidate', async () => {
  22. await expect(encodeFirstWithinLimit([], 8)).rejects.toThrow('requires at least one candidate')
  23. const result = await encodeFirstWithinLimit([
  24. () => Promise.resolve({ data: new Uint8Array(12), quality: 85 }),
  25. () => Promise.resolve({ data: new Uint8Array(9), quality: 80 }),
  26. () => Promise.resolve({ data: new Uint8Array(10), quality: 75 }),
  27. ], 8)
  28. expect(isExhaustedEncoding(result)).toBe(true)
  29. expect(result).toMatchObject({ smallest: { quality: 80 } })
  30. expect(isExhaustedEncoding({ data: new Uint8Array(1) })).toBe(false)
  31. })
  32. })
  33. describe('CompressionLimiter', () => {
  34. it('starts at most the configured number of tasks and preserves queued progress', async () => {
  35. const limiter = new CompressionLimiter(2)
  36. const gates = Array.from({ length: 4 }, () => Promise.withResolvers<undefined>())
  37. let active = 0
  38. let maximum = 0
  39. const started: number[] = []
  40. const tasks = gates.map((gate, index) => limiter.run(async () => {
  41. active += 1
  42. maximum = Math.max(maximum, active)
  43. started.push(index)
  44. await gate.promise
  45. active -= 1
  46. return index
  47. }))
  48. await Promise.resolve()
  49. expect(started).toEqual([0, 1])
  50. gates[0]!.resolve(undefined)
  51. await tasks[0]
  52. await Promise.resolve()
  53. expect(started).toEqual([0, 1, 2])
  54. gates[1]!.resolve(undefined)
  55. gates[2]!.resolve(undefined)
  56. await Promise.all([tasks[1], tasks[2]])
  57. await Promise.resolve()
  58. expect(started).toEqual([0, 1, 2, 3])
  59. gates[3]!.resolve(undefined)
  60. await expect(Promise.all(tasks)).resolves.toEqual([0, 1, 2, 3])
  61. expect(maximum).toBe(2)
  62. })
  63. it('releases a slot when a task throws before returning a promise', async () => {
  64. const limiter = new CompressionLimiter(1)
  65. const failed = limiter.run(() => {
  66. throw new Error('synchronous setup failure')
  67. })
  68. const next = limiter.run(() => Promise.resolve('next'))
  69. await expect(failed).rejects.toThrow('synchronous setup failure')
  70. await expect(next).resolves.toBe('next')
  71. })
  72. it('normalizes a non-Error rejection and releases its slot', async () => {
  73. const limiter = new CompressionLimiter(1)
  74. // oxlint-disable-next-line typescript/prefer-promise-reject-errors -- Native bindings can reject non-Error values.
  75. const failed = limiter.run(() => Promise.reject('native failure'))
  76. const next = limiter.run(() => Promise.resolve('next'))
  77. await expect(failed).rejects.toMatchObject({
  78. message: 'Image compression task rejected with a non-Error value.',
  79. cause: 'native failure',
  80. })
  81. await expect(next).resolves.toBe('next')
  82. })
  83. })