598 lines
23 KiB
TypeScript
598 lines
23 KiB
TypeScript
import { act, createEvent, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||
import userEvent from '@testing-library/user-event'
|
||
|
||
import ProductPackageUploader from '@/components/upload/ProductPackageUploader'
|
||
import { PerformanceSurveyBrowserImageError } from '@/lib/performance-survey-image-error'
|
||
import { TASK_DETAIL_UPLOAD_LIMITS } from '@/lib/task-detail-upload-policy'
|
||
|
||
const mockChunkUpload = jest.fn()
|
||
const mockPrepareTaskDetailImagePreview = jest.fn()
|
||
const mockPollTaskDetailUploadJob = jest.fn()
|
||
|
||
jest.mock('@/lib/chunk-upload', () => ({
|
||
chunkUpload: (...args: unknown[]) => mockChunkUpload(...args),
|
||
}))
|
||
|
||
jest.mock('@/lib/task-detail-image-browser', () => ({
|
||
prepareTaskDetailImagePreview: (...args: unknown[]) => mockPrepareTaskDetailImagePreview(...args),
|
||
}))
|
||
|
||
jest.mock('@/lib/task-detail-upload-status', () => ({
|
||
pollTaskDetailUploadJob: (...args: unknown[]) => mockPollTaskDetailUploadJob(...args),
|
||
TaskDetailUploadJobError: class TaskDetailUploadJobError extends Error {
|
||
jobStatus = 'failed'
|
||
},
|
||
}))
|
||
|
||
jest.mock('@/components/upload/DetailImageSorter', () => ({
|
||
__esModule: true,
|
||
default: () => <div data-testid="detail-image-sorter" />,
|
||
}))
|
||
|
||
jest.mock('@/components/upload/UploadForm', () => ({
|
||
__esModule: true,
|
||
default: ({
|
||
onProductModelChange,
|
||
onCategoryChange,
|
||
}: {
|
||
onProductModelChange: (value: string) => void
|
||
onCategoryChange: (value: string) => void
|
||
}) => (
|
||
<div data-testid="upload-form">
|
||
<button type="button" onClick={() => onProductModelChange('SPU-TEST')}>设置测试 SPU</button>
|
||
<button type="button" onClick={() => onCategoryChange('测试店铺')}>设置测试店铺</button>
|
||
</div>
|
||
),
|
||
}))
|
||
|
||
jest.mock('@/components/upload/category-handlers', () => ({
|
||
addCategory: jest.fn(),
|
||
addScene: jest.fn(),
|
||
}))
|
||
|
||
const defaultProps = {
|
||
onSuccess: jest.fn(),
|
||
onCancel: jest.fn(),
|
||
categories: [],
|
||
scenes: [],
|
||
platforms: [],
|
||
productCategories: [],
|
||
onRefreshCategories: jest.fn().mockResolvedValue(undefined),
|
||
onRefreshScenes: jest.fn().mockResolvedValue(undefined),
|
||
}
|
||
|
||
function createDeferred<T>() {
|
||
let resolve!: (value: T | PromiseLike<T>) => void
|
||
let reject!: (reason?: unknown) => void
|
||
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
|
||
resolve = resolvePromise
|
||
reject = rejectPromise
|
||
})
|
||
return { promise, resolve, reject }
|
||
}
|
||
|
||
function usersResponse() {
|
||
return {
|
||
ok: true,
|
||
json: async () => [{ id: 'requester-1', username: 'operator-a', displayName: '运营甲' }],
|
||
}
|
||
}
|
||
|
||
async function fillRequiredTaskFields(
|
||
user: ReturnType<typeof userEvent.setup>,
|
||
files: File | File[],
|
||
) {
|
||
const requesterInput = await screen.findByPlaceholderText('搜索发起本次出图需求的运营人员')
|
||
await user.click(requesterInput)
|
||
await user.type(requesterInput, '运营甲')
|
||
await user.click(screen.getByRole('option', { name: /运营甲/ }))
|
||
await user.click(screen.getByRole('button', { name: '设置测试 SPU' }))
|
||
await user.click(screen.getByRole('button', { name: '设置测试店铺' }))
|
||
await user.selectOptions(screen.getByLabelText(/出图方式/), '实拍')
|
||
await user.upload(screen.getByLabelText('主图'), files)
|
||
}
|
||
|
||
const capabilityError = () => new PerformanceSurveyBrowserImageError(
|
||
'BROWSER_DECODE_UNAVAILABLE',
|
||
'当前 Edge 环境无法解码这张合法图片',
|
||
)
|
||
|
||
function installUploadFlowFetch(completeResponse?: Promise<unknown>) {
|
||
const fetchMock = jest.fn((input: RequestInfo | URL) => {
|
||
const url = String(input)
|
||
if (url === '/api/users/list') return Promise.resolve(usersResponse())
|
||
if (url === '/api/materials/upload/product-package/init') {
|
||
return Promise.resolve({ ok: true, json: async () => ({ data: { materialId: 'material-1' } }) })
|
||
}
|
||
if (url === '/api/materials/upload/product-package/complete' && completeResponse) {
|
||
return completeResponse
|
||
}
|
||
if (url === '/api/materials/upload/product-package/abort') {
|
||
return Promise.resolve({ ok: true, json: async () => ({ success: true }) })
|
||
}
|
||
return Promise.reject(new Error(`测试未处理请求:${url}`))
|
||
})
|
||
global.fetch = fetchMock as unknown as typeof fetch
|
||
return fetchMock
|
||
}
|
||
|
||
describe('ProductPackageUploader', () => {
|
||
const originalFetch = global.fetch
|
||
|
||
beforeEach(() => {
|
||
jest.clearAllMocks()
|
||
mockChunkUpload.mockResolvedValue({ uploadId: 'upload-original-1' })
|
||
mockPollTaskDetailUploadJob.mockResolvedValue({
|
||
status: 'completed',
|
||
progress: 100,
|
||
stage: '处理完成',
|
||
})
|
||
global.fetch = jest.fn().mockResolvedValue({
|
||
ok: true,
|
||
json: async () => [
|
||
{
|
||
id: 'requester-1',
|
||
username: 'operator-a',
|
||
displayName: '运营甲',
|
||
},
|
||
],
|
||
}) as jest.Mock
|
||
|
||
Object.defineProperty(URL, 'createObjectURL', {
|
||
configurable: true,
|
||
writable: true,
|
||
value: jest.fn(() => 'blob:mock'),
|
||
})
|
||
|
||
Object.defineProperty(URL, 'revokeObjectURL', {
|
||
configurable: true,
|
||
writable: true,
|
||
value: jest.fn(),
|
||
})
|
||
})
|
||
|
||
afterEach(() => {
|
||
jest.restoreAllMocks()
|
||
})
|
||
|
||
afterAll(() => {
|
||
global.fetch = originalFetch
|
||
})
|
||
|
||
test('任务-详情源文件选择框支持 psb/tif/tiff', async () => {
|
||
const user = userEvent.setup()
|
||
render(<ProductPackageUploader {...defaultProps} />)
|
||
await screen.findByPlaceholderText('搜索发起本次出图需求的运营人员')
|
||
|
||
const input = screen.getByLabelText(/PSD源文件/) as HTMLInputElement
|
||
expect(input.accept).toBe('.psd,.psb,.tif,.tiff,.zip,.rar,.7z')
|
||
|
||
const files = [
|
||
new File(['a'], 'sample.psb', { type: 'application/octet-stream' }),
|
||
new File(['b'], 'sample.tif', { type: 'image/tiff' }),
|
||
new File(['c'], 'sample.tiff', { type: 'image/tiff' }),
|
||
]
|
||
|
||
await user.upload(input, files)
|
||
|
||
expect(screen.getByText('sample.psb')).toBeInTheDocument()
|
||
expect(screen.getByText('sample.tif')).toBeInTheDocument()
|
||
expect(screen.getByText('sample.tiff')).toBeInTheDocument()
|
||
})
|
||
|
||
test('空的任务-详情源文件会被直接拦截并提示', async () => {
|
||
const user = userEvent.setup()
|
||
render(<ProductPackageUploader {...defaultProps} />)
|
||
await screen.findByPlaceholderText('搜索发起本次出图需求的运营人员')
|
||
|
||
const input = screen.getByLabelText(/PSD源文件/) as HTMLInputElement
|
||
const emptyFile = new File([], 'empty.psd', { type: 'application/octet-stream' })
|
||
|
||
await user.upload(input, emptyFile)
|
||
|
||
expect(screen.getByText('以下文件为空,无法上传:empty.psd')).toBeInTheDocument()
|
||
expect(screen.queryByText('empty.psd')).not.toBeInTheDocument()
|
||
})
|
||
|
||
test('应提供任务详情出图方式选择项,且默认为必填', async () => {
|
||
render(<ProductPackageUploader {...defaultProps} />)
|
||
await screen.findByPlaceholderText('搜索发起本次出图需求的运营人员')
|
||
|
||
const select = screen.getByLabelText(/出图方式/) as HTMLSelectElement
|
||
expect(select.value).toBe('')
|
||
expect(select).toBeRequired()
|
||
expect(screen.getByRole('option', { name: '请选择出图方式' })).toBeInTheDocument()
|
||
expect(screen.getByRole('option', { name: '实拍' })).toBeInTheDocument()
|
||
expect(screen.getByRole('option', { name: '渲染' })).toBeInTheDocument()
|
||
expect(screen.getByRole('option', { name: 'AI' })).toBeInTheDocument()
|
||
})
|
||
|
||
test('应提供必填的需求运营搜索框并允许选择人员', async () => {
|
||
const user = userEvent.setup()
|
||
render(<ProductPackageUploader {...defaultProps} />)
|
||
|
||
const input = await screen.findByPlaceholderText('搜索发起本次出图需求的运营人员')
|
||
await user.click(input)
|
||
await user.type(input, '运营甲')
|
||
await user.click(screen.getByRole('option', { name: /运营甲/ }))
|
||
|
||
expect(input).toHaveValue('运营甲 (operator-a)')
|
||
expect(screen.getByText('记录发起本次出图需求的运营人员,与素材上传人可以不同。')).toBeInTheDocument()
|
||
})
|
||
|
||
test('未选择需求运营时提交应被前端拦截', async () => {
|
||
const user = userEvent.setup()
|
||
render(<ProductPackageUploader {...defaultProps} />)
|
||
|
||
await screen.findByPlaceholderText('搜索发起本次出图需求的运营人员')
|
||
await user.click(screen.getByRole('button', { name: '提交' }))
|
||
|
||
expect(screen.getByText('请选择需求运营人员')).toBeInTheDocument()
|
||
expect(global.fetch).toHaveBeenCalledTimes(1)
|
||
})
|
||
|
||
test('文件经过主图上传框内部按钮时应保持拖拽高亮', async () => {
|
||
render(<ProductPackageUploader {...defaultProps} />)
|
||
await screen.findByPlaceholderText('搜索发起本次出图需求的运营人员')
|
||
|
||
const input = screen.getByLabelText('主图') as HTMLInputElement
|
||
const dropZone = input.parentElement as HTMLDivElement
|
||
const addButton = dropZone.querySelector('button')
|
||
|
||
expect(addButton).not.toBeNull()
|
||
|
||
Object.defineProperty(dropZone, 'getBoundingClientRect', {
|
||
configurable: true,
|
||
value: () => ({
|
||
x: 0,
|
||
y: 0,
|
||
top: 0,
|
||
right: 100,
|
||
bottom: 100,
|
||
left: 0,
|
||
width: 100,
|
||
height: 100,
|
||
toJSON: () => ({}),
|
||
}),
|
||
})
|
||
|
||
fireEvent.dragEnter(dropZone, {
|
||
clientX: 20,
|
||
clientY: 20,
|
||
dataTransfer: { files: [], types: ['Files'] },
|
||
})
|
||
expect(dropZone).toHaveClass('border-blue-500')
|
||
|
||
const internalDragLeave = createEvent.dragLeave(dropZone)
|
||
Object.defineProperties(internalDragLeave, {
|
||
clientX: { value: 50 },
|
||
clientY: { value: 50 },
|
||
relatedTarget: { value: addButton },
|
||
dataTransfer: { value: { files: [], types: ['Files'] } },
|
||
})
|
||
fireEvent(dropZone, internalDragLeave)
|
||
expect(dropZone).toHaveClass('border-blue-500')
|
||
|
||
const outerDragLeave = createEvent.dragLeave(dropZone)
|
||
Object.defineProperties(outerDragLeave, {
|
||
clientX: { value: 120 },
|
||
clientY: { value: 50 },
|
||
relatedTarget: { value: document.body },
|
||
dataTransfer: { value: { files: [], types: ['Files'] } },
|
||
})
|
||
fireEvent(dropZone, outerDragLeave)
|
||
expect(dropZone).not.toHaveClass('border-blue-500')
|
||
})
|
||
|
||
test('任务图片选择框只允许浏览器处理链明确支持的格式', async () => {
|
||
render(<ProductPackageUploader {...defaultProps} />)
|
||
await screen.findByPlaceholderText('搜索发起本次出图需求的运营人员')
|
||
|
||
expect((screen.getByLabelText('主图') as HTMLInputElement).accept)
|
||
.toBe('image/jpeg,image/png,image/gif,image/webp')
|
||
expect((screen.getByLabelText('SKU图') as HTMLInputElement).accept)
|
||
.toBe('image/jpeg,image/png,image/gif,image/webp')
|
||
expect((screen.getByLabelText('详情图') as HTMLInputElement).accept)
|
||
.toBe('image/jpeg,image/png,image/gif,image/webp')
|
||
})
|
||
|
||
test('Edge 浏览器预览处理失败时继续上传原图并显式请求服务器兜底', async () => {
|
||
const user = userEvent.setup()
|
||
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined)
|
||
mockPrepareTaskDetailImagePreview.mockRejectedValueOnce(capabilityError())
|
||
;(global.fetch as jest.Mock)
|
||
.mockResolvedValueOnce({
|
||
ok: true,
|
||
json: async () => [{ id: 'requester-1', username: 'operator-a', displayName: '运营甲' }],
|
||
})
|
||
.mockResolvedValueOnce({ ok: true, json: async () => ({ data: { materialId: 'material-1' } }) })
|
||
.mockResolvedValueOnce({ ok: true, json: async () => ({ data: { jobId: 'job-1' } }) })
|
||
|
||
render(<ProductPackageUploader {...defaultProps} />)
|
||
await fillRequiredTaskFields(
|
||
user,
|
||
new File(['image'], 'edge-photo.jpg', { type: 'image/jpeg' }),
|
||
)
|
||
await user.click(screen.getByRole('button', { name: '提交' }))
|
||
|
||
await waitFor(() => expect(mockPollTaskDetailUploadJob).toHaveBeenCalledWith(
|
||
'job-1',
|
||
expect.objectContaining({ signal: expect.any(AbortSignal) }),
|
||
))
|
||
|
||
expect(mockChunkUpload).toHaveBeenCalledTimes(1)
|
||
expect(mockChunkUpload.mock.calls[0][0]).toMatchObject({ name: 'edge-photo.jpg' })
|
||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||
expect.stringContaining('edge-photo.jpg'),
|
||
'当前 Edge 环境无法解码这张合法图片',
|
||
)
|
||
|
||
const completeRequest = (global.fetch as jest.Mock).mock.calls.find(
|
||
([url]) => url === '/api/materials/upload/product-package/complete',
|
||
)
|
||
expect(completeRequest).toBeDefined()
|
||
expect(JSON.parse(completeRequest[1].body)).toMatchObject({
|
||
materialId: 'material-1',
|
||
allowServerPreviewFallback: true,
|
||
assets: {
|
||
main: [expect.objectContaining({
|
||
fileName: 'edge-photo.jpg',
|
||
uploadId: 'upload-original-1',
|
||
})],
|
||
},
|
||
})
|
||
})
|
||
|
||
test('任务图片超过 60M 像素时立即阻断,不上传原图也不创建后台任务', async () => {
|
||
const user = userEvent.setup()
|
||
jest.spyOn(console, 'error').mockImplementation(() => undefined)
|
||
mockPrepareTaskDetailImagePreview.mockRejectedValueOnce(
|
||
new PerformanceSurveyBrowserImageError(
|
||
'SOURCE_PIXEL_LIMIT',
|
||
'图片「70m.jpg」像素总量超过 60,000,000,请先压缩或切图',
|
||
),
|
||
)
|
||
const fetchMock = installUploadFlowFetch()
|
||
|
||
render(<ProductPackageUploader {...defaultProps} />)
|
||
await fillRequiredTaskFields(
|
||
user,
|
||
new File(['image'], '70m.jpg', { type: 'image/jpeg' }),
|
||
)
|
||
await user.click(screen.getByRole('button', { name: '提交' }))
|
||
|
||
expect(await screen.findByText('图片「70m.jpg」像素总量超过 60,000,000,请先压缩或切图')).toBeInTheDocument()
|
||
expect(mockChunkUpload).not.toHaveBeenCalled()
|
||
expect(mockPollTaskDetailUploadJob).not.toHaveBeenCalled()
|
||
expect(fetchMock.mock.calls.some(([url]) => url === '/api/materials/upload/product-package/complete')).toBe(false)
|
||
expect(fetchMock).toHaveBeenCalledWith(
|
||
'/api/materials/upload/product-package/abort',
|
||
expect.objectContaining({
|
||
method: 'POST',
|
||
body: JSON.stringify({ materialId: 'material-1', uploadIds: [] }),
|
||
}),
|
||
)
|
||
})
|
||
|
||
test('并发首错后等待另一上传成功,再用完整 uploadId 列表统一回滚', async () => {
|
||
const user = userEvent.setup()
|
||
jest.spyOn(console, 'error').mockImplementation(() => undefined)
|
||
jest.spyOn(console, 'warn').mockImplementation(() => undefined)
|
||
mockPrepareTaskDetailImagePreview.mockRejectedValue(capabilityError())
|
||
const uploadA = createDeferred<{ uploadId: string }>()
|
||
const uploadB = createDeferred<{ uploadId: string }>()
|
||
const xhrA = { abort: jest.fn() } as unknown as XMLHttpRequest
|
||
const xhrB = { abort: jest.fn() } as unknown as XMLHttpRequest
|
||
mockChunkUpload.mockImplementation((file: File, _metadata: unknown, options: {
|
||
onActiveXhrChange?: (xhrs: Set<XMLHttpRequest>) => void
|
||
}) => {
|
||
const isA = file.name === 'a.jpg'
|
||
options.onActiveXhrChange?.(new Set([isA ? xhrA : xhrB]))
|
||
return isA ? uploadA.promise : uploadB.promise
|
||
})
|
||
const fetchMock = installUploadFlowFetch()
|
||
|
||
render(<ProductPackageUploader {...defaultProps} />)
|
||
await fillRequiredTaskFields(user, [
|
||
new File(['a'], 'a.jpg', { type: 'image/jpeg' }),
|
||
new File(['b'], 'b.jpg', { type: 'image/jpeg' }),
|
||
])
|
||
await user.click(screen.getByRole('button', { name: '提交' }))
|
||
await waitFor(() => expect(mockChunkUpload).toHaveBeenCalledTimes(2))
|
||
|
||
await act(async () => {
|
||
uploadA.reject(new Error('A 上传失败'))
|
||
await Promise.resolve()
|
||
})
|
||
await waitFor(() => {
|
||
expect(xhrA.abort).toHaveBeenCalled()
|
||
expect(xhrB.abort).toHaveBeenCalled()
|
||
})
|
||
|
||
await act(async () => {
|
||
uploadB.resolve({ uploadId: 'upload-b' })
|
||
await Promise.resolve()
|
||
})
|
||
await waitFor(() => expect(fetchMock).toHaveBeenCalledWith(
|
||
'/api/materials/upload/product-package/abort',
|
||
expect.objectContaining({
|
||
body: JSON.stringify({ materialId: 'material-1', uploadIds: ['upload-b'] }),
|
||
}),
|
||
))
|
||
expect(fetchMock.mock.calls.filter(([url]) => url === '/api/materials/upload/product-package/abort')).toHaveLength(1)
|
||
})
|
||
|
||
test('并发首错后另一上传也失败时停止重试并只执行一次素材回滚', async () => {
|
||
const user = userEvent.setup()
|
||
jest.spyOn(console, 'error').mockImplementation(() => undefined)
|
||
jest.spyOn(console, 'warn').mockImplementation(() => undefined)
|
||
mockPrepareTaskDetailImagePreview.mockRejectedValue(capabilityError())
|
||
const uploadA = createDeferred<{ uploadId: string }>()
|
||
const uploadB = createDeferred<{ uploadId: string }>()
|
||
const xhrA = { abort: jest.fn() } as unknown as XMLHttpRequest
|
||
const xhrB = { abort: jest.fn() } as unknown as XMLHttpRequest
|
||
let bOptions: { isCancelled?: () => boolean } | undefined
|
||
mockChunkUpload.mockImplementation((file: File, _metadata: unknown, options: {
|
||
isCancelled?: () => boolean
|
||
onActiveXhrChange?: (xhrs: Set<XMLHttpRequest>) => void
|
||
}) => {
|
||
const isA = file.name === 'a.jpg'
|
||
if (!isA) bOptions = options
|
||
options.onActiveXhrChange?.(new Set([isA ? xhrA : xhrB]))
|
||
return isA ? uploadA.promise : uploadB.promise
|
||
})
|
||
const fetchMock = installUploadFlowFetch()
|
||
|
||
render(<ProductPackageUploader {...defaultProps} />)
|
||
await fillRequiredTaskFields(user, [
|
||
new File(['a'], 'a.jpg', { type: 'image/jpeg' }),
|
||
new File(['b'], 'b.jpg', { type: 'image/jpeg' }),
|
||
])
|
||
await user.click(screen.getByRole('button', { name: '提交' }))
|
||
await waitFor(() => expect(mockChunkUpload).toHaveBeenCalledTimes(2))
|
||
|
||
await act(async () => {
|
||
uploadA.reject(new Error('A 上传失败'))
|
||
await Promise.resolve()
|
||
})
|
||
await waitFor(() => expect(bOptions?.isCancelled?.()).toBe(true))
|
||
expect(xhrA.abort).toHaveBeenCalled()
|
||
expect(xhrB.abort).toHaveBeenCalled()
|
||
|
||
await act(async () => {
|
||
uploadB.reject(new Error('B 随后失败'))
|
||
await Promise.resolve()
|
||
})
|
||
await waitFor(() => expect(fetchMock).toHaveBeenCalledWith(
|
||
'/api/materials/upload/product-package/abort',
|
||
expect.objectContaining({
|
||
body: JSON.stringify({ materialId: 'material-1', uploadIds: [] }),
|
||
}),
|
||
))
|
||
expect(fetchMock.mock.calls.filter(([url]) => url === '/api/materials/upload/product-package/abort')).toHaveLength(1)
|
||
})
|
||
|
||
test('complete 返回 202 前取消时标记后台已接单但不再启动轮询', async () => {
|
||
const user = userEvent.setup()
|
||
jest.spyOn(console, 'error').mockImplementation(() => undefined)
|
||
jest.spyOn(console, 'warn').mockImplementation(() => undefined)
|
||
mockPrepareTaskDetailImagePreview.mockRejectedValueOnce(capabilityError())
|
||
const completeResponse = createDeferred<unknown>()
|
||
const fetchMock = installUploadFlowFetch(completeResponse.promise)
|
||
|
||
render(<ProductPackageUploader {...defaultProps} />)
|
||
await fillRequiredTaskFields(
|
||
user,
|
||
new File(['image'], 'cancel-before-202.jpg', { type: 'image/jpeg' }),
|
||
)
|
||
await user.click(screen.getByRole('button', { name: '提交' }))
|
||
await waitFor(() => expect(fetchMock.mock.calls.some(
|
||
([url]) => url === '/api/materials/upload/product-package/complete',
|
||
)).toBe(true))
|
||
|
||
await user.click(screen.getByRole('button', { name: '取消上传' }))
|
||
await act(async () => {
|
||
completeResponse.resolve({ ok: true, json: async () => ({ data: { jobId: 'job-after-cancel' } }) })
|
||
await Promise.resolve()
|
||
})
|
||
|
||
expect(await screen.findByText('已停止等待,后台任务仍在服务器处理中,可稍后查看素材状态')).toBeInTheDocument()
|
||
expect(mockPollTaskDetailUploadJob).not.toHaveBeenCalled()
|
||
expect(fetchMock.mock.calls.some(([url]) => url === '/api/materials/upload/product-package/abort')).toBe(false)
|
||
})
|
||
|
||
test('complete 返回 202 前卸载组件时不创建后台轮询', async () => {
|
||
const user = userEvent.setup()
|
||
jest.spyOn(console, 'error').mockImplementation(() => undefined)
|
||
jest.spyOn(console, 'warn').mockImplementation(() => undefined)
|
||
mockPrepareTaskDetailImagePreview.mockRejectedValueOnce(capabilityError())
|
||
const completeResponse = createDeferred<unknown>()
|
||
const fetchMock = installUploadFlowFetch(completeResponse.promise)
|
||
|
||
const { unmount } = render(<ProductPackageUploader {...defaultProps} />)
|
||
await fillRequiredTaskFields(
|
||
user,
|
||
new File(['image'], 'unmount-before-202.jpg', { type: 'image/jpeg' }),
|
||
)
|
||
await user.click(screen.getByRole('button', { name: '提交' }))
|
||
await waitFor(() => expect(fetchMock.mock.calls.some(
|
||
([url]) => url === '/api/materials/upload/product-package/complete',
|
||
)).toBe(true))
|
||
|
||
unmount()
|
||
await act(async () => {
|
||
completeResponse.resolve({ ok: true, json: async () => ({ data: { jobId: 'job-after-unmount' } }) })
|
||
await Promise.resolve()
|
||
await Promise.resolve()
|
||
})
|
||
|
||
expect(mockPollTaskDetailUploadJob).not.toHaveBeenCalled()
|
||
expect(fetchMock.mock.calls.some(([url]) => url === '/api/materials/upload/product-package/abort')).toBe(false)
|
||
})
|
||
|
||
test('浏览器生成的预览超过服务端总量前停止上传并回滚空素材', async () => {
|
||
const user = userEvent.setup()
|
||
jest.spyOn(console, 'error').mockImplementation(() => undefined)
|
||
const mutableLimits = TASK_DETAIL_UPLOAD_LIMITS as unknown as { maxTotalPreviewCount: number }
|
||
const originalLimit = mutableLimits.maxTotalPreviewCount
|
||
mutableLimits.maxTotalPreviewCount = 1
|
||
mockPrepareTaskDetailImagePreview.mockResolvedValueOnce([
|
||
{
|
||
file: new File(['preview-1'], 'preview-1.webp', { type: 'image/webp' }),
|
||
width: 800,
|
||
height: 600,
|
||
kind: 'preview',
|
||
sourceName: 'photo.jpg',
|
||
sourceIndex: 0,
|
||
sliceIndex: 0,
|
||
sliceCount: 1,
|
||
},
|
||
{
|
||
file: new File(['preview-2'], 'preview-2.webp', { type: 'image/webp' }),
|
||
width: 800,
|
||
height: 600,
|
||
kind: 'preview',
|
||
sourceName: 'photo.jpg',
|
||
sourceIndex: 0,
|
||
sliceIndex: 0,
|
||
sliceCount: 1,
|
||
},
|
||
])
|
||
;(global.fetch as jest.Mock)
|
||
.mockResolvedValueOnce({
|
||
ok: true,
|
||
json: async () => [{ id: 'requester-1', username: 'operator-a', displayName: '运营甲' }],
|
||
})
|
||
.mockResolvedValueOnce({ ok: true, json: async () => ({ data: { materialId: 'material-1' } }) })
|
||
.mockResolvedValueOnce({ ok: true, json: async () => ({ success: true }) })
|
||
|
||
try {
|
||
render(<ProductPackageUploader {...defaultProps} />)
|
||
const requesterInput = await screen.findByPlaceholderText('搜索发起本次出图需求的运营人员')
|
||
await user.click(requesterInput)
|
||
await user.type(requesterInput, '运营甲')
|
||
await user.click(screen.getByRole('option', { name: /运营甲/ }))
|
||
await user.click(screen.getByRole('button', { name: '设置测试 SPU' }))
|
||
await user.click(screen.getByRole('button', { name: '设置测试店铺' }))
|
||
await user.selectOptions(screen.getByLabelText(/出图方式/), '实拍')
|
||
await user.upload(
|
||
screen.getByLabelText('主图'),
|
||
new File(['image'], 'photo.jpg', { type: 'image/jpeg' }),
|
||
)
|
||
await user.click(screen.getByRole('button', { name: '提交' }))
|
||
|
||
expect(await screen.findByText('本次图片生成的预览切片过多或过大,请减少详情长图数量后分批提交')).toBeInTheDocument()
|
||
expect(mockChunkUpload).not.toHaveBeenCalled()
|
||
expect(global.fetch).toHaveBeenCalledWith(
|
||
'/api/materials/upload/product-package/abort',
|
||
expect.objectContaining({
|
||
method: 'POST',
|
||
body: JSON.stringify({ materialId: 'material-1', uploadIds: [] }),
|
||
}),
|
||
)
|
||
} finally {
|
||
mutableLimits.maxTotalPreviewCount = originalLimit
|
||
}
|
||
})
|
||
})
|