/** @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: () => 返回素材库, })) 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((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() 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() 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() 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) }) })