550 lines
28 KiB
TypeScript
550 lines
28 KiB
TypeScript
import { changeField, openSelect } from '@/test-utils/select'
|
||
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
||
import BrandKnowledgeDashboard from './BrandKnowledgeDashboard'
|
||
import { BRAND_KNOWLEDGE_TYPE_OPTIONS } from '@/lib/brand-knowledge-file-types'
|
||
|
||
const mockPrepareBrandKnowledgePreviewInBrowser = jest.fn()
|
||
const mockChunkUpload = jest.fn()
|
||
|
||
jest.mock('@/lib/brand-knowledge-image-browser', () => ({
|
||
prepareBrandKnowledgePreviewInBrowser: (...args: unknown[]) => mockPrepareBrandKnowledgePreviewInBrowser(...args),
|
||
canUseBrandKnowledgeLegacyServerPreview: jest.fn(async () => false),
|
||
}))
|
||
|
||
jest.mock('@/lib/chunk-upload', () => ({
|
||
chunkUpload: (...args: unknown[]) => mockChunkUpload(...args),
|
||
generateFileId: (file: File) => `file-${file.name}-${file.size}-${file.lastModified}`,
|
||
}))
|
||
|
||
jest.mock('next-auth/react', () => ({
|
||
useSession: () => ({ data: { user: { name: '本地超级管理员' } } }),
|
||
signOut: jest.fn(),
|
||
}))
|
||
|
||
describe('BrandKnowledgeDashboard', () => {
|
||
beforeEach(() => {
|
||
localStorage.clear()
|
||
Object.defineProperty(URL, 'createObjectURL', { configurable: true, value: jest.fn(() => 'blob:test-preview') })
|
||
Object.defineProperty(URL, 'revokeObjectURL', { configurable: true, value: jest.fn() })
|
||
Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText: jest.fn().mockResolvedValue(undefined) } })
|
||
mockChunkUpload.mockReset()
|
||
mockChunkUpload.mockResolvedValue({
|
||
uploadId: 'upload-knowledge-1',
|
||
existingFilePath: '/uploads/temp/upload-knowledge-1/merged-brand-guide.pdf',
|
||
fileName: 'brand-guide.pdf',
|
||
fileSize: 12,
|
||
})
|
||
mockPrepareBrandKnowledgePreviewInBrowser.mockResolvedValue([
|
||
{
|
||
file: new File(['slice one'], '预览图-001.webp', { type: 'image/webp' }),
|
||
mimeType: 'image/webp',
|
||
width: 1600,
|
||
height: 1500,
|
||
kind: 'slice',
|
||
sourceName: '预览图.png',
|
||
sourceIndex: 0,
|
||
sliceIndex: 0,
|
||
sliceCount: 2,
|
||
},
|
||
{
|
||
file: new File(['slice two'], '预览图-002.webp', { type: 'image/webp' }),
|
||
mimeType: 'image/webp',
|
||
width: 1600,
|
||
height: 900,
|
||
kind: 'slice',
|
||
sourceName: '预览图.png',
|
||
sourceIndex: 0,
|
||
sliceIndex: 1,
|
||
sliceCount: 2,
|
||
},
|
||
])
|
||
global.fetch = jest.fn().mockImplementation((input: RequestInfo | URL) => {
|
||
if (String(input) === '/api/product-categories') {
|
||
return Promise.resolve({ ok: true, json: async () => [{ id: 'category-1', name: '床类' }] } as Response)
|
||
}
|
||
return Promise.resolve({ ok: true, json: async () => ({ success: true, data: [] }) } as Response)
|
||
})
|
||
})
|
||
afterEach(() => { jest.restoreAllMocks() })
|
||
|
||
it('shows the real upload entry and empty state', async () => {
|
||
render(<BrandKnowledgeDashboard />)
|
||
|
||
const main = screen.getByRole('main')
|
||
expect(within(main).queryByRole('heading', { level: 1, name: '品牌知识库' })).not.toBeInTheDocument()
|
||
expect(within(main).queryByText('可上传')).not.toBeInTheDocument()
|
||
expect(screen.getByRole('searchbox', { name: '搜索品牌知识' })).toBeInTheDocument()
|
||
expect(screen.getByRole('navigation', { name: '知识类型' })).toBeInTheDocument()
|
||
expect(screen.getAllByRole('button').map((button) => button.textContent)).toEqual(expect.arrayContaining([...BRAND_KNOWLEDGE_TYPE_OPTIONS]))
|
||
expect(screen.getByRole('button', { name: '上传知识' })).toBeEnabled()
|
||
await waitFor(() => expect(screen.getByText('品牌知识库暂未上传内容')).toBeInTheDocument())
|
||
expect(screen.getByLabelText('共 0 条')).toHaveTextContent('共 0 条')
|
||
expect(screen.queryByRole('heading', { name: '知识内容' })).not.toBeInTheDocument()
|
||
expect(screen.queryByText('人员上传的知识文件将按类型保存,并可在此下载')).not.toBeInTheDocument()
|
||
})
|
||
|
||
it('filters the empty state by type and search', async () => {
|
||
render(<BrandKnowledgeDashboard />)
|
||
|
||
await waitFor(() => expect(screen.getByText('品牌知识库暂未上传内容')).toBeInTheDocument())
|
||
fireEvent.click(screen.getByRole('button', { name: '主图规范' }))
|
||
expect(screen.getByText('主图规范暂未上传内容')).toBeInTheDocument()
|
||
|
||
changeField(screen.getByRole('searchbox', { name: '搜索品牌知识' }), { target: { value: '索菲亚' } })
|
||
expect(screen.getByText('没有找到与“索菲亚”匹配的知识内容')).toBeInTheDocument()
|
||
})
|
||
|
||
it('searches by knowledge type when the title uses a different template spelling', async () => {
|
||
global.fetch = jest.fn().mockResolvedValue({
|
||
ok: true,
|
||
json: async () => ({
|
||
success: true,
|
||
total: 1,
|
||
data: [{
|
||
id: 'knowledge-template-1',
|
||
title: '升降桌主图模版-索菲亚',
|
||
knowledgeType: '主图模板',
|
||
brand: '索菲亚',
|
||
productCategoryName: '书桌',
|
||
fileName: '升降桌主图模板.psd',
|
||
mimeType: 'image/vnd.adobe.photoshop',
|
||
fileSize: 1024,
|
||
downloadCount: 0,
|
||
createdAt: '2026-09-21T00:00:00.000Z',
|
||
uploadedBy: '无邪',
|
||
downloadUrl: '/api/department-dashboard/brand-knowledge/knowledge-template-1/file',
|
||
previewUrl: null,
|
||
previewImageName: null,
|
||
likeCount: 0,
|
||
isLiked: false,
|
||
isFavorited: false,
|
||
}],
|
||
}),
|
||
} as Response)
|
||
|
||
render(<BrandKnowledgeDashboard />)
|
||
|
||
expect(await screen.findByRole('heading', { level: 3, name: '升降桌主图模版-索菲亚' })).toBeInTheDocument()
|
||
changeField(screen.getByRole('searchbox', { name: '搜索品牌知识' }), { target: { value: '主图模板' } })
|
||
expect(screen.getByRole('heading', { level: 3, name: '升降桌主图模版-索菲亚' })).toBeInTheDocument()
|
||
})
|
||
|
||
it('shows uploaded knowledge with the material-library card hierarchy', async () => {
|
||
global.fetch = jest.fn().mockResolvedValue({
|
||
ok: true,
|
||
json: async () => ({
|
||
success: true,
|
||
total: 23_081,
|
||
data: [{
|
||
id: 'knowledge-1',
|
||
title: '索菲亚图像影像规范',
|
||
knowledgeType: '影像规范',
|
||
brand: '索菲亚',
|
||
productCategoryName: '衣柜',
|
||
fileName: '索菲亚图像影像规范.rar',
|
||
mimeType: 'application/vnd.rar',
|
||
fileSize: 668886630,
|
||
downloadCount: 4,
|
||
createdAt: '2026-09-12T00:00:00.000Z',
|
||
uploadedBy: '无邪',
|
||
downloadUrl: '/api/department-dashboard/brand-knowledge/knowledge-1/file',
|
||
previewUrl: '/api/department-dashboard/brand-knowledge/knowledge-1/preview',
|
||
previewImageName: 'preview.png',
|
||
likeCount: 0,
|
||
isLiked: false,
|
||
isFavorited: false,
|
||
}],
|
||
}),
|
||
} as Response)
|
||
|
||
render(<BrandKnowledgeDashboard />)
|
||
|
||
const preview = await screen.findByRole('img', { name: '预览图:索菲亚图像影像规范' })
|
||
expect(screen.getByLabelText('共 23081 条')).toHaveTextContent('共 23,081 条')
|
||
expect(preview).toHaveClass('object-cover', 'object-center')
|
||
expect(screen.getByRole('link', { name: '查看索菲亚图像影像规范详情' })).toHaveAttribute(
|
||
'href',
|
||
'/workspaces/assets/brand-knowledge/knowledge-1',
|
||
)
|
||
expect(screen.getByRole('heading', { level: 3, name: '索菲亚图像影像规范' })).toBeInTheDocument()
|
||
expect(screen.getByText(/637\.90 MB/)).toBeInTheDocument()
|
||
expect(screen.getByText(/无邪/)).toBeInTheDocument()
|
||
expect(screen.getByText(/2026\.09\.12/)).toBeInTheDocument()
|
||
const knowledgeList = screen.getByRole('list', { name: '品牌知识内容列表' })
|
||
expect(knowledgeList).toHaveClass('grid-cols-1', 'sm:grid-cols-2', 'lg:grid-cols-4')
|
||
expect(within(knowledgeList).getByText('影像规范')).toBeInTheDocument()
|
||
expect(within(knowledgeList).getByText('索菲亚')).toBeInTheDocument()
|
||
expect(within(knowledgeList).getByText('衣柜')).toBeInTheDocument()
|
||
expect(screen.queryByText('索菲亚图像影像规范.rar')).not.toBeInTheDocument()
|
||
const copyIdButton = screen.getByRole('button', { name: '复制作品ID knowledge-1' })
|
||
expect(copyIdButton).toHaveTextContent('作品ID knowledge-1')
|
||
fireEvent.click(copyIdButton)
|
||
await waitFor(() => expect(navigator.clipboard.writeText).toHaveBeenCalledWith('knowledge-1'))
|
||
await waitFor(() => expect(screen.getByRole('status')).toHaveTextContent('作品ID已复制'))
|
||
expect(screen.getByRole('link', { name: '下载索菲亚图像影像规范' })).toHaveAttribute(
|
||
'href',
|
||
'/api/department-dashboard/brand-knowledge/knowledge-1/file',
|
||
)
|
||
expect(screen.getByRole('button', { name: '分享索菲亚图像影像规范' })).toBeInTheDocument()
|
||
expect(screen.getByRole('button', { name: '点赞索菲亚图像影像规范' })).toHaveAttribute('aria-pressed', 'false')
|
||
expect(screen.getByRole('button', { name: '收藏索菲亚图像影像规范' })).toHaveAttribute('aria-pressed', 'false')
|
||
expect(screen.queryByRole('button', { name: '编辑索菲亚图像影像规范' })).not.toBeInTheDocument()
|
||
expect(screen.queryByRole('button', { name: '删除索菲亚图像影像规范' })).not.toBeInTheDocument()
|
||
})
|
||
|
||
it('opens the material-library share dialog, copies the deep link, and persists engagement state', async () => {
|
||
const item = {
|
||
id: 'knowledge-1',
|
||
title: '索菲亚图像影像规范',
|
||
knowledgeType: '影像规范',
|
||
brand: '索菲亚',
|
||
productCategoryName: '衣柜',
|
||
fileName: '索菲亚图像影像规范.rar',
|
||
mimeType: 'application/vnd.rar',
|
||
fileSize: 668886630,
|
||
downloadCount: 4,
|
||
createdAt: '2026-09-12T00:00:00.000Z',
|
||
uploadedBy: '无邪',
|
||
downloadUrl: '/api/department-dashboard/brand-knowledge/knowledge-1/file',
|
||
previewUrl: '/api/department-dashboard/brand-knowledge/knowledge-1/preview',
|
||
previewImageName: 'preview.png',
|
||
likeCount: 0,
|
||
isLiked: false,
|
||
isFavorited: false,
|
||
}
|
||
global.fetch = jest.fn().mockImplementation((input: RequestInfo | URL, init?: RequestInit) => {
|
||
if (String(input).endsWith('/like') && init?.method === 'PUT') {
|
||
return Promise.resolve({ ok: true, json: async () => ({ success: true, liked: true, likeCount: 1 }) } as Response)
|
||
}
|
||
if (String(input).endsWith('/favorite') && init?.method === 'PUT') {
|
||
return Promise.resolve({ ok: true, json: async () => ({ success: true, isFavorited: true }) } as Response)
|
||
}
|
||
return Promise.resolve({ ok: true, json: async () => ({ success: true, data: [item] }) } as Response)
|
||
})
|
||
|
||
render(<BrandKnowledgeDashboard />)
|
||
|
||
fireEvent.click(await screen.findByRole('button', { name: '分享索菲亚图像影像规范' }))
|
||
const shareDialog = screen.getByRole('dialog', { name: '分享链接' })
|
||
expect(within(shareDialog).getByTestId('brand-knowledge-share-url')).toHaveTextContent(
|
||
'/workspaces/assets/brand-knowledge/knowledge-1',
|
||
)
|
||
expect(navigator.clipboard.writeText).not.toHaveBeenCalled()
|
||
fireEvent.click(within(shareDialog).getByRole('button', { name: '复制链接' }))
|
||
await waitFor(() => expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
|
||
expect.stringMatching(/\/workspaces\/assets\/brand-knowledge\/knowledge-1$/),
|
||
))
|
||
await waitFor(() => expect(screen.getByRole('status')).toHaveTextContent('分享链接已复制到剪贴板!'))
|
||
expect(screen.queryByRole('dialog', { name: '分享链接' })).not.toBeInTheDocument()
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '点赞索菲亚图像影像规范' }))
|
||
await waitFor(() => expect(screen.getByRole('button', { name: '取消点赞索菲亚图像影像规范' })).toHaveTextContent('点赞 1'))
|
||
expect(global.fetch).toHaveBeenCalledWith(
|
||
'/api/department-dashboard/brand-knowledge/knowledge-1/like',
|
||
expect.objectContaining({ method: 'PUT', body: JSON.stringify({ active: true }) }),
|
||
)
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '收藏索菲亚图像影像规范' }))
|
||
await waitFor(() => expect(screen.getByRole('button', { name: '取消收藏索菲亚图像影像规范' })).toHaveTextContent('已收藏'))
|
||
expect(global.fetch).toHaveBeenCalledWith(
|
||
'/api/department-dashboard/brand-knowledge/knowledge-1/favorite',
|
||
expect.objectContaining({ method: 'PUT', body: JSON.stringify({ active: true }) }),
|
||
)
|
||
})
|
||
|
||
it('shows edit and delete only for super administrators and persists both actions', async () => {
|
||
const item = {
|
||
id: 'knowledge-1', title: '索菲亚图像影像规范', knowledgeType: '影像规范', brand: '索菲亚',
|
||
productCategoryName: '衣柜', fileName: 'guide.pdf', mimeType: 'application/pdf', fileSize: 10,
|
||
downloadCount: 0, createdAt: '2026-09-12T00:00:00.000Z', uploadedBy: '无邪',
|
||
downloadUrl: '/download', previewUrl: null, previewImageName: null, likeCount: 0,
|
||
isLiked: false, isFavorited: false,
|
||
}
|
||
global.fetch = jest.fn().mockImplementation((input: RequestInfo | URL, init?: RequestInit) => {
|
||
if (String(input) === '/api/product-categories') {
|
||
return Promise.resolve({ ok: true, json: async () => [{ id: 'category-1', name: '床类' }] } as Response)
|
||
}
|
||
if (init?.method === 'PATCH') {
|
||
const metadata = JSON.parse(String(init.body))
|
||
return Promise.resolve({ ok: true, json: async () => ({ success: true, data: { id: item.id, ...metadata } }) } as Response)
|
||
}
|
||
if (init?.method === 'DELETE') {
|
||
return Promise.resolve({ ok: true, json: async () => ({ success: true }) } as Response)
|
||
}
|
||
return Promise.resolve({ ok: true, json: async () => ({ success: true, data: [item], permissions: { canManage: true } }) } as Response)
|
||
})
|
||
|
||
render(<BrandKnowledgeDashboard />)
|
||
|
||
fireEvent.click(await screen.findByRole('button', { name: '编辑索菲亚图像影像规范' }))
|
||
const editDialog = screen.getByRole('dialog', { name: '编辑品牌知识' })
|
||
changeField(within(editDialog).getByLabelText('知识标题'), { target: { value: '更新后的规范' } })
|
||
changeField(within(editDialog).getByLabelText('知识类型'), { target: { value: '主图模板' } })
|
||
changeField(within(editDialog).getByLabelText('关联品牌'), { target: { value: '亿家达' } })
|
||
openSelect(within(editDialog).getByLabelText('品类'))
|
||
await screen.findByRole('option', { name: '床类' })
|
||
changeField(within(editDialog).getByLabelText('品类'), { target: { value: '床类' } })
|
||
fireEvent.click(within(editDialog).getByRole('button', { name: '保存修改' }))
|
||
await waitFor(() => expect(global.fetch).toHaveBeenCalledWith(
|
||
'/api/department-dashboard/brand-knowledge/knowledge-1',
|
||
expect.objectContaining({
|
||
method: 'PATCH',
|
||
body: JSON.stringify({
|
||
title: '更新后的规范',
|
||
knowledgeType: '主图模板',
|
||
brand: '亿家达',
|
||
productCategoryName: '床类',
|
||
}),
|
||
}),
|
||
))
|
||
expect(await screen.findByRole('heading', { level: 3, name: '更新后的规范' })).toBeInTheDocument()
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '删除更新后的规范' }))
|
||
const deleteDialog = screen.getByRole('dialog', { name: '删除品牌知识' })
|
||
fireEvent.click(within(deleteDialog).getByRole('button', { name: '确认删除' }))
|
||
await waitFor(() => expect(screen.queryByRole('heading', { level: 3, name: '更新后的规范' })).not.toBeInTheDocument())
|
||
expect(global.fetch).toHaveBeenCalledWith(
|
||
'/api/department-dashboard/brand-knowledge/knowledge-1',
|
||
{ method: 'DELETE' },
|
||
)
|
||
})
|
||
|
||
it('submits a selected knowledge file to the independent API', async () => {
|
||
const fetchMock = global.fetch as jest.Mock
|
||
fetchMock.mockImplementation((input: RequestInfo | URL, init?: RequestInit) => {
|
||
if (String(input) === '/api/product-categories') {
|
||
return Promise.resolve({ ok: true, json: async () => [{ id: 'category-1', name: '床类' }] } as Response)
|
||
}
|
||
if (init?.method === 'POST') {
|
||
return Promise.resolve({ ok: true, json: async () => ({ success: true, data: {} }) } as Response)
|
||
}
|
||
return Promise.resolve({ ok: true, json: async () => ({ success: true, data: [] }) } as Response)
|
||
})
|
||
render(<BrandKnowledgeDashboard />)
|
||
|
||
const uploadButton = screen.getByRole('button', { name: '上传知识' })
|
||
fireEvent.click(uploadButton)
|
||
|
||
expect(screen.getByRole('dialog', { name: '上传知识' })).toBeInTheDocument()
|
||
expect(screen.getByRole('button', { name: '提交知识' })).toBeDisabled()
|
||
const knowledgeTypeSelect = screen.getByRole('dialog').querySelector('select') as HTMLSelectElement
|
||
expect(Array.from(knowledgeTypeSelect.querySelectorAll('option')).map((option) => option.textContent)).toEqual([...BRAND_KNOWLEDGE_TYPE_OPTIONS])
|
||
expect(Array.from(knowledgeTypeSelect.options).map((option) => option.value)).toEqual([
|
||
'主图规范', '主图模板', '详情页规范', '详情页模板', 'sku规范',
|
||
'sku模板', '主图视频规范', '主图视频模板', '影像规范', '品牌VI',
|
||
])
|
||
|
||
changeField(screen.getByLabelText('知识标题'), { target: { value: '品牌手册' } })
|
||
openSelect(screen.getByLabelText(/品类/))
|
||
await waitFor(() => expect(screen.getByRole('option', { name: '床类' })).toBeInTheDocument())
|
||
changeField(screen.getByRole('combobox', { name: /品类/ }), { target: { value: '床类' } })
|
||
const knowledgeFile = new File(['品牌资料'], '品牌手册.pdf', { type: 'application/pdf' })
|
||
const previewFile = new File(['fake image'], '预览图.png', { type: 'image/png' })
|
||
changeField(screen.getByLabelText('选择知识文件'), { target: { files: [knowledgeFile] } })
|
||
changeField(screen.getByLabelText('选择预览图片'), { target: { files: [previewFile] } })
|
||
expect(screen.getByText('品牌手册.pdf')).toBeInTheDocument()
|
||
expect(await screen.findByText('预览图.png')).toBeInTheDocument()
|
||
expect(await screen.findByRole('img', { name: '预览图:预览图.png' })).toHaveClass('object-contain', 'object-center')
|
||
expect(screen.getByText(/已切为 2 片/)).toBeInTheDocument()
|
||
expect(screen.getByRole('button', { name: '提交知识' })).toBeEnabled()
|
||
fireEvent.click(screen.getByRole('button', { name: '提交知识' }))
|
||
await waitFor(() => expect(fetchMock).toHaveBeenCalledWith('/api/department-dashboard/brand-knowledge', expect.objectContaining({ method: 'POST', body: expect.any(FormData) })))
|
||
expect(mockChunkUpload).toHaveBeenCalledWith(
|
||
knowledgeFile,
|
||
{ uploadPurpose: 'brand-knowledge' },
|
||
expect.objectContaining({
|
||
maxRetries: 5,
|
||
onProgress: expect.any(Function),
|
||
onStageChange: expect.any(Function),
|
||
isCancelled: expect.any(Function),
|
||
onActiveXhrChange: expect.any(Function),
|
||
}),
|
||
)
|
||
const postCall = fetchMock.mock.calls.find(([, init]) => init?.method === 'POST')
|
||
expect(postCall).toBeDefined()
|
||
const submittedFormData = postCall?.[1]?.body as FormData
|
||
expect(submittedFormData.get('productCategoryName')).toBe('床类')
|
||
expect(submittedFormData.get('file')).toBeNull()
|
||
expect(submittedFormData.get('uploadId')).toBe('upload-knowledge-1')
|
||
expect(submittedFormData.get('existingFilePath')).toBe('/uploads/temp/upload-knowledge-1/merged-brand-guide.pdf')
|
||
expect(submittedFormData.get('originalFileName')).toBe('品牌手册.pdf')
|
||
expect(submittedFormData.get('fileSize')).toBe(String(knowledgeFile.size))
|
||
expect(submittedFormData.get('previewImage')).toBeNull()
|
||
expect(submittedFormData.get('previewImageName')).toBe('预览图.png')
|
||
expect(submittedFormData.get('previewImageSliceCount')).toBe('2')
|
||
expect(submittedFormData.getAll('previewImageSlices').map((entry) => (entry as File).name)).toEqual([
|
||
'预览图-001.webp',
|
||
'预览图-002.webp',
|
||
])
|
||
|
||
await waitFor(() => expect(screen.queryByRole('dialog', { name: '上传知识' })).not.toBeInTheDocument())
|
||
expect(uploadButton).toHaveFocus()
|
||
})
|
||
|
||
it('labels a processed preview with one output as a single image', async () => {
|
||
mockPrepareBrandKnowledgePreviewInBrowser.mockResolvedValueOnce([{
|
||
file: new File(['single preview'], '预览图.webp', { type: 'image/webp' }),
|
||
mimeType: 'image/webp',
|
||
width: 1_000,
|
||
height: 3_000,
|
||
kind: 'slice',
|
||
sourceName: '预览图.png',
|
||
sourceIndex: 0,
|
||
sliceIndex: 0,
|
||
sliceCount: 1,
|
||
}])
|
||
render(<BrandKnowledgeDashboard />)
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '上传知识' }))
|
||
const previewFile = new File(['fake image'], '预览图.png', { type: 'image/png' })
|
||
changeField(screen.getByLabelText('选择预览图片'), { target: { files: [previewFile] } })
|
||
|
||
expect(await screen.findByText('10 B · 单张预览')).toBeInTheDocument()
|
||
expect(screen.getByRole('status')).toHaveTextContent('图片高度不超过 3000px,已保留为单张预览')
|
||
expect(screen.queryByText(/已切为 1 片/)).not.toBeInTheDocument()
|
||
})
|
||
|
||
it.each([
|
||
[413, '上传内容超过服务器允许大小(413)'],
|
||
[429, '当前上传任务较多(429)'],
|
||
[502, '上传服务网关异常(502)'],
|
||
[503, '上传服务暂不可用(503)'],
|
||
[504, '服务器处理超时(504)'],
|
||
])('shows an actionable message for a non-JSON %i upload response', async (status, expectedMessage) => {
|
||
const fetchMock = global.fetch as jest.Mock
|
||
fetchMock.mockImplementation((input: RequestInfo | URL, init?: RequestInit) => {
|
||
if (String(input) === '/api/product-categories') {
|
||
return Promise.resolve({ ok: true, json: async () => [] } as Response)
|
||
}
|
||
if (init?.method === 'POST') {
|
||
return Promise.resolve({
|
||
ok: false,
|
||
status,
|
||
json: async () => { throw new Error('non-JSON upstream response') },
|
||
} as unknown as Response)
|
||
}
|
||
return Promise.resolve({ ok: true, json: async () => ({ success: true, data: [] }) } as Response)
|
||
})
|
||
render(<BrandKnowledgeDashboard />)
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '上传知识' }))
|
||
changeField(screen.getByLabelText('知识标题'), { target: { value: '品牌手册' } })
|
||
changeField(screen.getByLabelText('选择知识文件'), {
|
||
target: { files: [new File(['guide'], '品牌手册.pdf', { type: 'application/pdf' })] },
|
||
})
|
||
fireEvent.click(screen.getByRole('button', { name: '提交知识' }))
|
||
|
||
expect(await screen.findByRole('alert')).toHaveTextContent(expectedMessage)
|
||
expect(screen.getByRole('button', { name: '重新上传' })).toBeEnabled()
|
||
})
|
||
|
||
it('retries finalize without uploading the completed main file again', async () => {
|
||
const fetchMock = global.fetch as jest.Mock
|
||
let postCount = 0
|
||
fetchMock.mockImplementation((input: RequestInfo | URL, init?: RequestInit) => {
|
||
if (String(input) === '/api/product-categories') {
|
||
return Promise.resolve({ ok: true, json: async () => [] } as Response)
|
||
}
|
||
if (init?.method === 'POST') {
|
||
postCount += 1
|
||
if (postCount === 1) {
|
||
return Promise.resolve({
|
||
ok: false,
|
||
status: 504,
|
||
json: async () => { throw new Error('gateway timeout') },
|
||
} as unknown as Response)
|
||
}
|
||
return Promise.resolve({ ok: true, status: 201, json: async () => ({ success: true, data: {} }) } as Response)
|
||
}
|
||
return Promise.resolve({ ok: true, json: async () => ({ success: true, data: [] }) } as Response)
|
||
})
|
||
render(<BrandKnowledgeDashboard />)
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '上传知识' }))
|
||
changeField(screen.getByLabelText('知识标题'), { target: { value: '品牌手册' } })
|
||
changeField(screen.getByLabelText('选择知识文件'), {
|
||
target: { files: [new File(['guide'], '品牌手册.pdf', { type: 'application/pdf' })] },
|
||
})
|
||
fireEvent.click(screen.getByRole('button', { name: '提交知识' }))
|
||
expect(await screen.findByText(/服务器处理超时(504)/)).toBeInTheDocument()
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '重新上传' }))
|
||
await waitFor(() => expect(screen.queryByRole('dialog', { name: '上传知识' })).not.toBeInTheDocument())
|
||
expect(mockChunkUpload).toHaveBeenCalledTimes(1)
|
||
expect(postCount).toBe(2)
|
||
})
|
||
|
||
it('restores a completed main-file upload after remount and removes it after successful creation', async () => {
|
||
const fetchMock = global.fetch as jest.Mock
|
||
let finalizeCount = 0
|
||
fetchMock.mockImplementation((input: RequestInfo | URL, init?: RequestInit) => {
|
||
if (String(input) === '/api/product-categories') {
|
||
return Promise.resolve({ ok: true, json: async () => [] } as Response)
|
||
}
|
||
if (init?.method === 'POST') {
|
||
finalizeCount += 1
|
||
if (finalizeCount === 1) {
|
||
return Promise.resolve({
|
||
ok: false,
|
||
status: 504,
|
||
json: async () => { throw new Error('gateway timeout') },
|
||
} as unknown as Response)
|
||
}
|
||
return Promise.resolve({ ok: true, status: 201, json: async () => ({ success: true, data: {} }) } as Response)
|
||
}
|
||
return Promise.resolve({ ok: true, json: async () => ({ success: true, data: [] }) } as Response)
|
||
})
|
||
const file = new File(['guide'], '品牌手册.pdf', {
|
||
type: 'application/pdf',
|
||
lastModified: 123456,
|
||
})
|
||
|
||
const firstRender = render(<BrandKnowledgeDashboard />)
|
||
fireEvent.click(screen.getByRole('button', { name: '上传知识' }))
|
||
changeField(screen.getByLabelText('知识标题'), { target: { value: '品牌手册' } })
|
||
changeField(screen.getByLabelText('选择知识文件'), { target: { files: [file] } })
|
||
fireEvent.click(screen.getByRole('button', { name: '提交知识' }))
|
||
expect(await screen.findByText(/服务器处理超时(504)/)).toBeInTheDocument()
|
||
expect(localStorage.length).toBe(1)
|
||
firstRender.unmount()
|
||
|
||
render(<BrandKnowledgeDashboard />)
|
||
fireEvent.click(screen.getByRole('button', { name: '上传知识' }))
|
||
changeField(screen.getByLabelText('知识标题'), { target: { value: '品牌手册' } })
|
||
changeField(screen.getByLabelText('选择知识文件'), { target: { files: [file] } })
|
||
expect(within(screen.getByRole('dialog', { name: '上传知识' })).getByRole('status')).toHaveTextContent('已恢复这个文件完成的分片上传')
|
||
fireEvent.click(screen.getByRole('button', { name: '提交知识' }))
|
||
|
||
await waitFor(() => expect(screen.queryByRole('dialog', { name: '上传知识' })).not.toBeInTheDocument())
|
||
expect(mockChunkUpload).toHaveBeenCalledTimes(1)
|
||
expect(finalizeCount).toBe(2)
|
||
expect(localStorage.length).toBe(0)
|
||
})
|
||
|
||
it('shows chunk progress and aborts active requests when the user cancels', async () => {
|
||
const abort = jest.fn()
|
||
mockChunkUpload.mockImplementation((_file, _metadata, options) => new Promise((_resolve, reject) => {
|
||
abort.mockImplementation(() => reject(new Error('上传已取消')))
|
||
options.onProgress(37, 2 * 1024 * 1024)
|
||
options.onActiveXhrChange(new Set([{ abort }]))
|
||
}))
|
||
render(<BrandKnowledgeDashboard />)
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '上传知识' }))
|
||
changeField(screen.getByLabelText('知识标题'), { target: { value: '品牌手册' } })
|
||
changeField(screen.getByLabelText('选择知识文件'), {
|
||
target: { files: [new File(['guide'], '品牌手册.pdf', { type: 'application/pdf' })] },
|
||
})
|
||
fireEvent.click(screen.getByRole('button', { name: '提交知识' }))
|
||
|
||
const progress = await screen.findByRole('progressbar', { name: '主文件上传进度' })
|
||
expect(progress).toHaveAttribute('aria-valuenow', '37')
|
||
expect(progress).toHaveTextContent('2.0 MB/s')
|
||
fireEvent.click(screen.getByRole('button', { name: '取消上传' }))
|
||
|
||
expect(abort).toHaveBeenCalledTimes(1)
|
||
expect(await screen.findByRole('alert')).toHaveTextContent('上传已取消')
|
||
expect(screen.getByRole('button', { name: '重新上传' })).toBeEnabled()
|
||
})
|
||
})
|