227 lines
8.4 KiB
TypeScript
227 lines
8.4 KiB
TypeScript
import { selectOption } from '@/test-utils/select'
|
|
/** @jest-environment jsdom */
|
|
|
|
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
import userEvent from '@testing-library/user-event'
|
|
import { useSession } from 'next-auth/react'
|
|
import EditMaterialPage from '@/app/edit/[id]/page'
|
|
|
|
const mockPush = jest.fn()
|
|
const mockBack = jest.fn()
|
|
const mockRouter = {
|
|
push: mockPush,
|
|
back: mockBack,
|
|
}
|
|
|
|
jest.mock('next-auth/react', () => ({
|
|
useSession: jest.fn(),
|
|
}))
|
|
|
|
jest.mock('next/navigation', () => ({
|
|
useRouter: () => mockRouter,
|
|
}))
|
|
|
|
jest.mock('@/components/Header', () => ({
|
|
__esModule: true,
|
|
default: () => <a href="/">返回素材库</a>,
|
|
}))
|
|
|
|
jest.mock('@/components/upload/TaskReferenceFields', () => ({
|
|
__esModule: true,
|
|
default: () => null,
|
|
}))
|
|
|
|
const material = {
|
|
id: 'material-1',
|
|
userId: 'user-1',
|
|
productModel: 'SPU-001',
|
|
reportTitle: null,
|
|
categoryName: '店铺A',
|
|
sceneName: '场景A',
|
|
platformName: 'Amazon',
|
|
productCategoryName: '',
|
|
materialType: '3D场景',
|
|
renderer: 'CR',
|
|
authorName: '测试作者',
|
|
previewUrl: null,
|
|
taskNo: null,
|
|
dingtalkApprovalNo: null,
|
|
taskStatus: null,
|
|
taskFetchedAt: null,
|
|
isFullSet: false,
|
|
taskDetailSourceType: null,
|
|
taskDetailRequesterUserId: null,
|
|
taskDetailRequesterName: null,
|
|
canEdit: true,
|
|
}
|
|
|
|
function jsonResponse(payload: unknown, status = 200) {
|
|
return Promise.resolve({
|
|
ok: status >= 200 && status < 300,
|
|
status,
|
|
json: async () => payload,
|
|
} as Response)
|
|
}
|
|
|
|
function referencePayload(url: string) {
|
|
if (url.endsWith('/api/categories')) {
|
|
return [{ id: 'category-1', name: '店铺A' }]
|
|
}
|
|
if (url.endsWith('/api/scenes')) {
|
|
return [{ id: 'scene-1', name: '场景A' }]
|
|
}
|
|
if (url.endsWith('/api/platforms')) {
|
|
return [{ id: 'platform-1', name: 'Amazon', count: 1 }]
|
|
}
|
|
if (url.endsWith('/api/product-categories')) {
|
|
return []
|
|
}
|
|
return [{ id: 'user-1', username: 'author', displayName: '测试作者' }]
|
|
}
|
|
|
|
describe('EditMaterialPage', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
;(useSession as jest.Mock).mockReturnValue({
|
|
data: { user: { id: 'user-1', role: 'USER' } },
|
|
status: 'authenticated',
|
|
})
|
|
Element.prototype.scrollIntoView = jest.fn()
|
|
})
|
|
|
|
test('素材本体先于可选项显示,选项完成前禁止保存', async () => {
|
|
let releaseReferences: (() => void) | undefined
|
|
const referenceGate = new Promise<void>((resolve) => {
|
|
releaseReferences = resolve
|
|
})
|
|
global.fetch = jest.fn((input: RequestInfo | URL) => {
|
|
const url = String(input)
|
|
if (url.includes('/api/materials/material-1')) {
|
|
return jsonResponse(material)
|
|
}
|
|
return referenceGate.then(() => jsonResponse(referencePayload(url)))
|
|
}) as jest.Mock
|
|
|
|
render(<EditMaterialPage params={{ id: 'material-1' }} />)
|
|
|
|
expect(await screen.findByLabelText('产品型号 *')).toHaveValue('SPU-001')
|
|
expect(screen.getByText('正在加载店铺、场景和人员选项…')).toBeInTheDocument()
|
|
expect(screen.getByRole('button', { name: '保存修改' })).toBeDisabled()
|
|
|
|
await act(async () => {
|
|
releaseReferences?.()
|
|
await referenceGate
|
|
})
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByText('正在加载店铺、场景和人员选项…')).not.toBeInTheDocument()
|
|
})
|
|
expect(screen.getByRole('button', { name: '保存修改' })).toBeEnabled()
|
|
})
|
|
|
|
test('部分选项失败时保留当前值、显示重试并禁止保存', async () => {
|
|
global.fetch = jest.fn((input: RequestInfo | URL) => {
|
|
const url = String(input)
|
|
if (url.includes('/api/materials/material-1')) {
|
|
return jsonResponse(material)
|
|
}
|
|
if (url.endsWith('/api/categories')) {
|
|
return jsonResponse({ error: '店铺读取失败' }, 500)
|
|
}
|
|
return jsonResponse(referencePayload(url))
|
|
}) as jest.Mock
|
|
|
|
render(<EditMaterialPage params={{ id: 'material-1' }} />)
|
|
|
|
const categorySelect = await screen.findByLabelText('店铺 *')
|
|
await screen.findByText('店铺加载失败,当前值已保留。')
|
|
|
|
expect(categorySelect).toHaveValue('店铺A')
|
|
expect(categorySelect).toBeDisabled()
|
|
expect(screen.getByRole('button', { name: '重新加载' })).toBeInTheDocument()
|
|
expect(screen.getByRole('button', { name: '保存修改' })).toBeDisabled()
|
|
})
|
|
|
|
test('修改后取消会使用统一弹窗,刷新保护与移动端粘性操作区同时生效', async () => {
|
|
const user = userEvent.setup()
|
|
global.fetch = jest.fn((input: RequestInfo | URL) => {
|
|
const url = String(input)
|
|
if (url.includes('/api/materials/material-1')) {
|
|
return jsonResponse(material)
|
|
}
|
|
return jsonResponse(referencePayload(url))
|
|
}) as jest.Mock
|
|
|
|
render(<EditMaterialPage params={{ id: 'material-1' }} />)
|
|
|
|
const productModelInput = await screen.findByLabelText('产品型号 *')
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', { name: '保存修改' })).toBeEnabled()
|
|
})
|
|
await user.clear(productModelInput)
|
|
await user.type(productModelInput, 'SPU-002')
|
|
|
|
const beforeUnloadEvent = new Event('beforeunload', { cancelable: true })
|
|
window.dispatchEvent(beforeUnloadEvent)
|
|
expect(beforeUnloadEvent.defaultPrevented).toBe(true)
|
|
|
|
const saveButton = screen.getByRole('button', { name: '保存修改' })
|
|
expect(saveButton.closest('.sticky')).not.toBeNull()
|
|
|
|
await user.click(screen.getByRole('button', { name: '取消' }))
|
|
expect(await screen.findByText('放弃未保存的修改?')).toBeInTheDocument()
|
|
await user.click(screen.getByRole('button', { name: '继续编辑' }))
|
|
expect(mockBack).not.toHaveBeenCalled()
|
|
|
|
await user.click(screen.getByRole('button', { name: '取消' }))
|
|
await user.click(await screen.findByRole('button', { name: '放弃修改' }))
|
|
expect(mockBack).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
test.each([false, true])('检测报告编辑并保存日期,清空时恢复默认一年:%s', async (clearExpiry) => {
|
|
const user = userEvent.setup()
|
|
const report = {
|
|
...material,
|
|
materialType: '证书-检测报告',
|
|
productCategoryName: '板材',
|
|
platformName: '品牌',
|
|
reportKind: '成品',
|
|
reportTitle: '板材检测报告',
|
|
certificateExpiresOn: '2028-09-23T00:00:00.000Z',
|
|
certificateTestedOn: '2027-09-23T00:00:00.000Z',
|
|
certificateNumber: 'CMA-2026-001',
|
|
certificateUsageInstructions: '使用前核对主体',
|
|
reportUsageRules: '第一段\n第二段',
|
|
}
|
|
global.fetch = jest.fn((input: RequestInfo | URL) => {
|
|
const url = String(input)
|
|
if (url.includes('/api/materials/material-1')) return jsonResponse(report)
|
|
return jsonResponse(referencePayload(url))
|
|
}) as jest.Mock
|
|
|
|
render(<EditMaterialPage params={{ id: 'material-1' }} />)
|
|
|
|
const kindSelect = await screen.findByLabelText('报告类别 *')
|
|
expect(screen.getByLabelText('SPU')).toHaveValue('SPU-001')
|
|
expect(screen.getByLabelText('证书检测日期 *')).toHaveTextContent('2027-09-23')
|
|
expect(screen.getByLabelText('证书到期时间(选填)')).toHaveTextContent('2028-09-23')
|
|
expect(screen.queryByLabelText('证书使用说明 *')).not.toBeInTheDocument()
|
|
expect(screen.getByLabelText('检测报告使用规则说明 *')).toHaveValue('第一段\n第二段')
|
|
expect(screen.getByLabelText('平台分类')).toHaveValue('品牌')
|
|
|
|
await selectOption(kindSelect, '原材料')
|
|
expect(screen.queryByLabelText('SPU')).not.toBeInTheDocument()
|
|
expect(screen.getByLabelText('报告名称 / 检测对象 *')).toHaveValue('板材检测报告')
|
|
await user.click(screen.getByLabelText('证书到期时间(选填)'))
|
|
await user.click(screen.getByRole('button', { name: clearExpiry ? '清除' : '2028年9月24日' }))
|
|
if (clearExpiry) expect(screen.getByLabelText('证书到期时间(选填)')).toHaveTextContent('默认 2028-09-23')
|
|
fireEvent.submit(screen.getByLabelText('报告名称 / 检测对象 *').closest('form')!)
|
|
await waitFor(() => expect((global.fetch as jest.Mock).mock.calls.some(([, init]) => init?.method === 'PUT')).toBe(true))
|
|
const [, init] = (global.fetch as jest.Mock).mock.calls.find(([, init]) => init?.method === 'PUT')!
|
|
expect(init.body.get('certificateTestedOn')).toBe('2027-09-23')
|
|
expect(init.body.get('certificateExpiresOn')).toBe(clearExpiry ? '' : '2028-09-24')
|
|
expect(init.body.get('reportUsageRules')).toBe('第一段\n第二段')
|
|
expect(init.body.has('certificateUsageInstructions')).toBe(false)
|
|
})
|
|
})
|