gongxiangp2/lib/__tests__/material-edit-page.test.tsx
2026-07-30 14:39:30 +08:00

180 lines
5.7 KiB
TypeScript

/** @jest-environment jsdom */
import { act, 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)
})
})