gongxiangp2/lib/__tests__/performance-survey-image-processing.test.ts
2026-07-13 17:59:13 +08:00

134 lines
5.4 KiB
TypeScript

import sharp from 'sharp'
import {
PERFORMANCE_SURVEY_IMAGE_LIMITS,
assertPerformanceSurveyImageDimensions,
assertPerformanceSurveyImageRequestSize,
inspectPerformanceSurveyImage,
processPerformanceSurveyImage,
processPerformanceSurveyImages,
serializePerformanceSurveyImagePayload,
} from '@/lib/performance-survey-image-processing'
describe('performance survey image processing', () => {
test('serially converts a long source image into ordered 1500px WebP slices', async () => {
const source = await sharp({
create: {
width: 24,
height: 3100,
channels: 4,
background: { r: 32, g: 96, b: 180, alpha: 1 },
},
}).png().toBuffer()
const processed = await processPerformanceSurveyImages([{
buffer: source,
mimeType: 'image/png',
sourceName: 'long-image.png',
}])
expect(processed).toHaveLength(3)
expect(processed.map((item) => item.sliceIndex)).toEqual([0, 1, 2])
const metadata = await Promise.all(processed.map((item) => sharp(item.buffer).metadata()))
expect(metadata.map((item) => item.format)).toEqual(['webp', 'webp', 'webp'])
expect(metadata.map((item) => item.height)).toEqual([1500, 1500, 100])
})
test('rejects excessive pixel and per-image slice budgets before processing', () => {
expect(() => assertPerformanceSurveyImageDimensions(10_000, 10_000, 'huge.png')).toThrow('像素总量过大')
expect(() => assertPerformanceSurveyImageDimensions(
1,
PERFORMANCE_SURVEY_IMAGE_LIMITS.sliceHeight * (PERFORMANCE_SURVEY_IMAGE_LIMITS.maxSlicesPerImage + 1),
'too-long.png',
)).toThrow(`最多允许 ${PERFORMANCE_SURVEY_IMAGE_LIMITS.maxSlicesPerImage} 个切片`)
})
test('rejects more than 12 source images before decoding', async () => {
const inputs = Array.from({ length: PERFORMANCE_SURVEY_IMAGE_LIMITS.maxSourceCount + 1 }, (_, index) => ({
buffer: Buffer.from('not-decoded'),
mimeType: 'image/png',
sourceName: `${index}.png`,
}))
await expect(processPerformanceSurveyImages(inputs)).rejects.toThrow('最多上传 60 张切片')
})
test('downscales wide sources before slicing and creates a lightweight top preview', async () => {
const source = await sharp({
create: {
width: 2000,
height: 3000,
channels: 3,
background: { r: 210, g: 220, b: 230 },
},
}).jpeg().toBuffer()
const input = { buffer: source, mimeType: 'image/jpeg', sourceName: 'wide-long.jpg' }
const plan = await inspectPerformanceSurveyImage(input)
const processed = await processPerformanceSurveyImage(input, plan)
expect(plan.outputWidth).toBe(PERFORMANCE_SURVEY_IMAGE_LIMITS.maxOutputWidth)
expect(plan.outputHeight).toBe(2400)
expect(processed.slices).toHaveLength(2)
const sliceMetadata = await Promise.all(processed.slices.map((item) => sharp(item.buffer).metadata()))
expect(sliceMetadata.map((item) => item.width)).toEqual([1600, 1600])
expect(sliceMetadata.map((item) => item.height)).toEqual([1500, 900])
const previewMetadata = await sharp(processed.preview.buffer).metadata()
expect(previewMetadata.width).toBeLessThanOrEqual(PERFORMANCE_SURVEY_IMAGE_LIMITS.previewWidth)
expect(previewMetadata.height).toBeLessThanOrEqual(PERFORMANCE_SURVEY_IMAGE_LIMITS.previewHeight)
})
test('streams processed slices to the uploader without retaining the full slice batch', async () => {
const source = await sharp({
create: {
width: 40,
height: 3100,
channels: 3,
background: { r: 80, g: 120, b: 160 },
},
}).png().toBuffer()
const input = { buffer: source, mimeType: 'image/png', sourceName: 'streamed-long.png' }
const received: number[] = []
const processed = await processPerformanceSurveyImage(input, undefined, async (slice) => {
received.push(slice.sliceIndex)
})
expect(received).toEqual([0, 1, 2])
expect(processed.slices).toEqual([])
expect(processed.preview.buffer.length).toBeGreaterThan(0)
})
test('rejects oversized multipart requests before form-data parsing', () => {
expect(() => assertPerformanceSurveyImageRequestSize(String(65 * 1024 * 1024))).toThrow('总大小不能超过 60MB')
expect(() => assertPerformanceSurveyImageRequestSize(String(60 * 1024 * 1024))).not.toThrow()
})
test('serializes structured images and groups legacy slice urls by source image', () => {
const structured = serializePerformanceSurveyImagePayload([{
version: 1,
sourceName: '详情页',
width: 750,
height: 3000,
previewUrl: 'https://s3.example/preview.webp',
previewWidth: 720,
previewHeight: 560,
slices: [
{ url: 'https://s3.example/slice-1.webp', width: 750, height: 1500 },
{ url: 'https://s3.example/slice-2.webp', width: 750, height: 1500 },
],
}])
expect(structured.imageUrls).toEqual([
'https://s3.example/slice-1.webp',
'https://s3.example/slice-2.webp',
])
expect(structured.imageGroups[0].previewUrl).toBe('https://s3.example/preview.webp')
const legacy = serializePerformanceSurveyImagePayload([
'https://s3.example/batch-1-1.webp',
'https://s3.example/batch-1-2.webp',
'https://s3.example/batch-2-1.webp',
])
expect(legacy.imageGroups).toHaveLength(2)
expect(legacy.imageGroups[0].slices).toHaveLength(2)
expect(legacy.imageGroups[1].slices).toHaveLength(1)
})
})