134 lines
5.4 KiB
TypeScript
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)
|
|
})
|
|
})
|