gongxiangp2/lib/__tests__/performance-survey-image-browser.test.ts
2026-08-02 01:04:11 +08:00

178 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
PERFORMANCE_SURVEY_IMAGE_WORKER_TIMEOUT_MS,
preparePerformanceSurveyImagesInBrowser,
} from '@/lib/performance-survey-image-browser'
class FakeWorker {
static instances: FakeWorker[] = []
static postMessageError: Error | null = null
onmessage: ((event: MessageEvent) => void) | null = null
onerror: ((event: ErrorEvent) => void) | null = null
onmessageerror: ((event: MessageEvent) => void) | null = null
postMessage = jest.fn((_message: unknown) => {
if (FakeWorker.postMessageError) throw FakeWorker.postMessageError
})
terminate = jest.fn()
constructor() {
FakeWorker.instances.push(this)
}
}
describe('performance-survey-image-browser', () => {
const originalWorker = Object.getOwnPropertyDescriptor(globalThis, 'Worker')
const originalOffscreenCanvas = Object.getOwnPropertyDescriptor(globalThis, 'OffscreenCanvas')
const originalCreateImageBitmap = Object.getOwnPropertyDescriptor(globalThis, 'createImageBitmap')
beforeEach(() => {
jest.useFakeTimers()
FakeWorker.instances = []
FakeWorker.postMessageError = null
Object.defineProperty(globalThis, 'Worker', {
configurable: true,
writable: true,
value: FakeWorker,
})
Object.defineProperty(globalThis, 'OffscreenCanvas', {
configurable: true,
writable: true,
value: class FakeOffscreenCanvas {},
})
Object.defineProperty(globalThis, 'createImageBitmap', {
configurable: true,
writable: true,
value: jest.fn(),
})
})
afterEach(() => {
jest.useRealTimers()
if (originalWorker) Object.defineProperty(globalThis, 'Worker', originalWorker)
else delete (globalThis as { Worker?: unknown }).Worker
if (originalOffscreenCanvas) Object.defineProperty(globalThis, 'OffscreenCanvas', originalOffscreenCanvas)
else delete (globalThis as { OffscreenCanvas?: unknown }).OffscreenCanvas
if (originalCreateImageBitmap) Object.defineProperty(globalThis, 'createImageBitmap', originalCreateImageBitmap)
else delete (globalThis as { createImageBitmap?: unknown }).createImageBitmap
})
test('用户取消会立即终止 Worker,且不会被识别为服务器兜底错误', async () => {
const controller = new AbortController()
const removeListenerSpy = jest.spyOn(controller.signal, 'removeEventListener')
const promise = preparePerformanceSurveyImagesInBrowser(
[new File(['image'], 'photo.jpg', { type: 'image/jpeg' })],
undefined,
{ profile: 'task_detail_preview', signal: controller.signal, timeoutMs: 60_000 },
)
const rejection = expect(promise).rejects.toMatchObject({ name: 'AbortError' })
const worker = FakeWorker.instances[0]
expect(worker).toBeDefined()
controller.abort()
await rejection
expect(worker.terminate).toHaveBeenCalledTimes(1)
expect(removeListenerSpy).toHaveBeenCalled()
expect(jest.getTimerCount()).toBe(0)
})
test('watchdog 超时会终止 Worker 并返回独立 WORKER_TIMEOUT', async () => {
expect(PERFORMANCE_SURVEY_IMAGE_WORKER_TIMEOUT_MS).toBe(10 * 60 * 1000)
const promise = preparePerformanceSurveyImagesInBrowser(
[new File(['image'], 'slow.jpg', { type: 'image/jpeg' })],
undefined,
{ profile: 'task_detail_preview', timeoutMs: 1_000 },
)
const rejection = expect(promise).rejects.toMatchObject({
code: 'WORKER_TIMEOUT',
message: expect.stringContaining('将改由服务器安全处理'),
})
await jest.advanceTimersByTimeAsync(1_000)
await rejection
const worker = FakeWorker.instances[0]
expect(worker.terminate).toHaveBeenCalledTimes(1)
expect(worker.onmessage).toBeNull()
expect(worker.onerror).toBeNull()
expect(worker.onmessageerror).toBeNull()
expect(jest.getTimerCount()).toBe(0)
})
test('成功返回时也清理 watchdog、Abort listener 和 Worker', async () => {
const controller = new AbortController()
const removeListenerSpy = jest.spyOn(controller.signal, 'removeEventListener')
const promise = preparePerformanceSurveyImagesInBrowser(
[new File(['image'], 'success.jpg', { type: 'image/jpeg' })],
undefined,
{ profile: 'task_detail_preview', signal: controller.signal, timeoutMs: 60_000 },
)
const worker = FakeWorker.instances[0]
const request = worker.postMessage.mock.calls[0][0] as { id: string }
worker.onmessage?.({
data: {
id: request.id,
ok: true,
images: [{
blob: new Blob(['preview'], { type: 'image/webp' }),
fileName: 'success-preview.webp',
mimeType: 'image/webp',
width: 800,
height: 600,
kind: 'preview',
sourceName: 'success.jpg',
sourceIndex: 0,
sliceIndex: 0,
sliceCount: 1,
}],
},
} as MessageEvent)
await expect(promise).resolves.toEqual([
expect.objectContaining({
file: expect.any(File),
sourceName: 'success.jpg',
}),
])
expect(worker.terminate).toHaveBeenCalledTimes(1)
expect(removeListenerSpy).toHaveBeenCalled()
expect(jest.getTimerCount()).toBe(0)
})
test('postMessage 失败时清理 watchdog、Abort listener 和 Worker', async () => {
FakeWorker.postMessageError = new Error('structured clone failed')
const controller = new AbortController()
const removeListenerSpy = jest.spyOn(controller.signal, 'removeEventListener')
const promise = preparePerformanceSurveyImagesInBrowser(
[new File(['image'], 'clone-error.jpg', { type: 'image/jpeg' })],
undefined,
{ profile: 'task_detail_preview', signal: controller.signal, timeoutMs: 60_000 },
)
await expect(promise).rejects.toMatchObject({ code: 'WORKER_UNAVAILABLE' })
const worker = FakeWorker.instances[0]
expect(worker.terminate).toHaveBeenCalledTimes(1)
expect(removeListenerSpy).toHaveBeenCalled()
expect(jest.getTimerCount()).toBe(0)
})
test('Worker 运行错误时只结算一次并清理资源', async () => {
const promise = preparePerformanceSurveyImagesInBrowser(
[new File(['image'], 'worker-error.jpg', { type: 'image/jpeg' })],
undefined,
{ profile: 'task_detail_preview', timeoutMs: 60_000 },
)
const worker = FakeWorker.instances[0]
const handleError = worker.onerror
handleError?.({} as ErrorEvent)
handleError?.({} as ErrorEvent)
await expect(promise).rejects.toMatchObject({ code: 'WORKER_UNAVAILABLE' })
expect(worker.terminate).toHaveBeenCalledTimes(1)
expect(jest.getTimerCount()).toBe(0)
})
})