/** @jest-environment jsdom */ import { fireEvent, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { WorkReviewReviewerWorkspace } from '@/components/work-review/WorkReviewReviewerWorkspace' import type { WorkReviewProject } from '@/types/work-review' const project: WorkReviewProject = { id: 'project-1', title: '测试作品', description: null, workDate: '2026-08-01', status: 'pending_review', latestVersion: 1, submitter: { id: 'submitter-1', name: '提交人' }, manager: { id: 'manager-1', name: '评审人' }, finalReviewer: { id: 'final-1', name: '元泽' }, createdAt: '2026-08-01T00:00:00.000Z', updatedAt: '2026-08-01T00:00:00.000Z', versions: [{ id: 'version-1', version: 1, submissionNote: null, status: 'pending_review', submittedAt: '2026-08-01T00:00:00.000Z', completedAt: null, imageUrls: [], imageGroups: [{ imageId: 'image-1', sourceName: '作品长图', previewUrl: '/preview.webp', previewWidth: 1000, previewHeight: 2000, width: 1000, height: 2000, slices: [{ url: '/slice.webp', width: 1000, height: 2000 }], }], referenceImageUrls: [], referenceImageGroups: [], reviews: [], }], permissions: { canResubmit: false, pendingReviewRoles: ['manager'], }, } const projectWithReference: WorkReviewProject = { ...project, versions: [{ ...project.versions[0], referenceImageGroups: [{ imageId: 'reference-1', sourceName: '参考长图', previewUrl: '/reference-preview.webp', previewWidth: 1000, previewHeight: 2000, width: 1000, height: 2000, slices: [{ url: '/reference-slice.webp', width: 1000, height: 2000 }], }], }], } describe('WorkReviewReviewerWorkspace', () => { test('switching image type shows only the matching optional reference images', async () => { const user = userEvent.setup() const group = project.versions[0].imageGroups[0] render() expect(screen.getAllByRole('img', { name: '主图参考' })).toHaveLength(2) expect(screen.queryByRole('img', { name: 'SKU参考' })).not.toBeInTheDocument() expect(screen.queryByRole('button', { name: /(?:主图|详情|SKU)图片 \d+/ })).not.toBeInTheDocument() await user.click(screen.getAllByRole('button', { name: 'SKU图片' })[0]) expect(screen.getAllByRole('img', { name: 'SKU参考' })).toHaveLength(2) expect(screen.queryByRole('img', { name: '主图参考' })).not.toBeInTheDocument() await user.click(screen.getAllByRole('button', { name: '详情图片' })[0]) expect(screen.getAllByText('当前类型未上传参考图片')).toHaveLength(2) expect(screen.queryByRole('img', { name: 'SKU参考' })).not.toBeInTheDocument() }) test('same-type images remain individually selectable without numbered labels', async () => { const user = userEvent.setup() const group = project.versions[0].imageGroups[0] render() expect(screen.getAllByRole('button', { name: '主图图片' })).toHaveLength(4) expect(screen.getAllByRole('button', { name: 'SKU图片' })).toHaveLength(4) expect(screen.queryByRole('button', { name: /(?:主图|SKU)图片 \d+/ })).not.toBeInTheDocument() for (const sourceName of ['第二张主图', '第二张SKU', '第一张SKU', '第一张主图']) { await user.click(screen.getAllByTitle(sourceName)[0]) expect(screen.getAllByRole('img', { name: sourceName })).toHaveLength(2) expect(screen.getAllByTitle(sourceName)[0]).toHaveAttribute('aria-pressed', 'true') } }) test('shows the submitted work description above the image workspace', () => { render( , ) const description = screen.getByText((_, element) => ( element?.tagName === 'P' && element.textContent === '用于测试新版视觉主图。\n重点关注标题层级与产品卖点。' )) expect(screen.getByRole('heading', { name: '作品说明' })).toBeInTheDocument() expect(description).toHaveClass('whitespace-pre-wrap', 'break-words') expect(description.compareDocumentPosition(screen.getAllByText('作品图片')[0])) .toBe(Node.DOCUMENT_POSITION_FOLLOWING) }) test('keeps mobile box selection under app control and recovers after a cancelled gesture', async () => { window.PointerEvent = MouseEvent as typeof PointerEvent const user = userEvent.setup() render() await user.click(screen.getAllByRole('button', { name: '+ 添加标注' })[0]) const canvas = screen.getAllByRole('group', { name: /作品长图,可添加位置批注/ })[0] expect(canvas).toHaveClass('touch-none', 'select-none', '[-webkit-touch-callout:none]') expect(canvas).not.toHaveClass('touch-pan-y') expect(screen.getAllByRole('status')[0]).toHaveTextContent('手机端如需滚动,请先关闭标注工具') jest.spyOn(canvas, 'getBoundingClientRect').mockReturnValue({ x: 0, y: 0, top: 0, left: 0, right: 1000, bottom: 2000, width: 1000, height: 2000, toJSON: () => ({}), }) fireEvent.pointerDown(canvas, { pointerId: 1, button: 0, clientX: 200, clientY: 400 }) fireEvent.pointerMove(canvas, { pointerId: 1, button: 0, clientX: 450, clientY: 800 }) fireEvent.pointerCancel(canvas, { pointerId: 1 }) expect(screen.queryByRole('textbox', { name: /修改意见/ })).not.toBeInTheDocument() fireEvent.pointerDown(canvas, { pointerId: 2, button: 0, clientX: 300, clientY: 500 }) fireEvent.pointerMove(canvas, { pointerId: 2, button: 0, clientX: 600, clientY: 950 }) fireEvent.pointerUp(canvas, { pointerId: 2, button: 0, clientX: 600, clientY: 950 }) expect(screen.getByRole('textbox', { name: /图 1 · 标注 1 修改意见/ })).toBeInTheDocument() }) test('moves an unfinished annotation, locks it on completion, and submits the moved position', async () => { window.PointerEvent = MouseEvent as typeof PointerEvent const user = userEvent.setup() const onSubmitReview = jest.fn().mockResolvedValue(undefined) render() expect(screen.getAllByText('已标注 0 个修改点')).toHaveLength(2) expect(screen.queryByText(/已标注 0 个修改点;点击/)).not.toBeInTheDocument() expect(screen.queryByText('两位评审均提交后,系统会通知发起人')).not.toBeInTheDocument() expect(screen.queryByText('左右滑动查看作品与参考')).not.toBeInTheDocument() await user.click(screen.getAllByRole('button', { name: '+ 添加标注' })[0]) expect(screen.getAllByRole('toolbar', { name: '标注工具' })).toHaveLength(2) expect(screen.getAllByRole('button', { name: '矩形框选' })[0]).toHaveAttribute('aria-pressed', 'true') await user.click(screen.getAllByRole('radio', { name: '红色' })[0]) const canvas = screen.getAllByRole('group', { name: /作品长图,可添加位置批注/ })[0] jest.spyOn(canvas, 'getBoundingClientRect').mockReturnValue({ x: 0, y: 0, top: 0, left: 0, right: 1000, bottom: 2000, width: 1000, height: 2000, toJSON: () => ({}), }) fireEvent.pointerDown(canvas, { button: 0, clientX: 250, clientY: 500 }) fireEvent.pointerMove(canvas, { button: 0, clientX: 500, clientY: 900 }) fireEvent.pointerUp(canvas, { button: 0, clientX: 500, clientY: 900 }) const comment = screen.getByRole('textbox', { name: /图 1 · 标注 1 修改意见/ }) await user.type(comment, '人物位置向左移动') fireEvent.pointerDown(canvas, { button: 0, clientX: 750, clientY: 1200 }) fireEvent.pointerMove(canvas, { button: 0, clientX: 850, clientY: 1400 }) fireEvent.pointerUp(canvas, { button: 0, clientX: 850, clientY: 1400 }) await user.click(screen.getByRole('button', { name: '完成' })) fireEvent.pointerDown(canvas, { button: 0, clientX: 100, clientY: 200 }) fireEvent.pointerUp(canvas, { button: 0, clientX: 100, clientY: 200 }) expect(screen.getAllByText('已标注 1 个修改点')).toHaveLength(2) const commentLabels = screen.getAllByText('人物位置向左移动') expect(commentLabels).toHaveLength(2) await user.click(screen.getAllByRole('button', { name: '隐藏标注点备注文字' })[0]) expect(screen.getAllByRole('button', { name: '显示标注点备注文字' })).toHaveLength(2) commentLabels.forEach((label) => expect(label).toHaveClass('hidden')) await user.click(screen.getAllByRole('button', { name: '显示标注点备注文字' })[0]) expect(screen.getAllByRole('button', { name: '隐藏标注点备注文字' })).toHaveLength(2) commentLabels.forEach((label) => expect(label).not.toHaveClass('hidden')) await user.click(screen.getByRole('button', { name: '提交给修改人' })) await waitFor(() => expect(onSubmitReview).toHaveBeenCalledTimes(1)) expect(onSubmitReview.mock.calls[0][1]).toMatchObject({ reviewerRole: 'manager', imageComments: [{ imageIndex: 0, decision: 'revision_required', annotations: [{ x: 0.75, y: 0.6, x2: 0.85, y2: 0.7, shape: 'rectangle', color: '#d92d20', comment: '人物位置向左移动', }], }], }) }) test('requires the reviewer to complete the current annotation before submitting', async () => { window.PointerEvent = MouseEvent as typeof PointerEvent const user = userEvent.setup() const onSubmitReview = jest.fn().mockResolvedValue(undefined) render() await user.click(screen.getAllByRole('button', { name: '+ 添加标注' })[0]) const canvas = screen.getAllByRole('group', { name: /作品长图,可添加位置批注/ })[0] jest.spyOn(canvas, 'getBoundingClientRect').mockReturnValue({ x: 0, y: 0, top: 0, left: 0, right: 1000, bottom: 2000, width: 1000, height: 2000, toJSON: () => ({}), }) fireEvent.pointerDown(canvas, { button: 0, clientX: 250, clientY: 500 }) fireEvent.pointerMove(canvas, { button: 0, clientX: 500, clientY: 900 }) fireEvent.pointerUp(canvas, { button: 0, clientX: 500, clientY: 900 }) await user.type(screen.getByRole('textbox', { name: /图 1 · 标注 1 修改意见/ }), '需要调整') await user.click(screen.getByRole('button', { name: '提交给修改人' })) expect(onSubmitReview).not.toHaveBeenCalled() expect(screen.getByRole('alert')).toHaveTextContent('请先点击“完成”锁定当前标注位置') }) test('approves directly without requiring a modification annotation', async () => { const user = userEvent.setup() const onSubmitReview = jest.fn().mockResolvedValue(undefined) render() await user.click(screen.getByRole('button', { name: '通过' })) await waitFor(() => expect(onSubmitReview).toHaveBeenCalledTimes(1)) expect(onSubmitReview.mock.calls[0][1]).toMatchObject({ reviewerRole: 'manager', imageComments: [{ imageIndex: 0, decision: 'approved', comment: '通过', annotations: [], }], }) }) test('keeps mobile work and reference controls synchronized with the comparison scroller', async () => { const user = userEvent.setup() render() const scroller = screen.getByLabelText('作品与参考图片对照区,左右滑动切换') const scrollTo = jest.fn() Object.defineProperty(scroller, 'clientWidth', { configurable: true, value: 360 }) Object.defineProperty(scroller, 'scrollTo', { configurable: true, value: scrollTo }) const workButton = screen.getByRole('button', { name: '作品图' }) const referenceButton = screen.getByRole('button', { name: '参考图' }) expect(workButton).toHaveAttribute('aria-pressed', 'true') expect(referenceButton).toHaveAttribute('aria-pressed', 'false') await user.click(referenceButton) expect(scrollTo).toHaveBeenCalledWith({ left: 360, behavior: 'smooth' }) expect(referenceButton).toHaveAttribute('aria-pressed', 'true') Object.defineProperty(scroller, 'scrollLeft', { configurable: true, value: 0 }) fireEvent.scroll(scroller) expect(workButton).toHaveAttribute('aria-pressed', 'true') }) test('does not show comparison controls when the version has no reference image', () => { render() expect(screen.queryByRole('button', { name: '作品图' })).not.toBeInTheDocument() expect(screen.queryByRole('button', { name: '参考图' })).not.toBeInTheDocument() }) test('shows the shared detail workspace in read-only mode for a submitter awaiting review', () => { render( , ) expect(screen.getByRole('tab', { name: 'V1' })).toBeInTheDocument() expect(screen.getByText('等待评审人完成本轮评审;完成后系统会通知发起人')).toBeInTheDocument() expect(screen.queryByRole('button', { name: '通过' })).not.toBeInTheDocument() expect(screen.queryByRole('button', { name: '提交给修改人' })).not.toBeInTheDocument() }) test('guides the submitter to upload a revision only when changes are required', () => { render( , ) expect(screen.getByText('当前版本需要修改,请通过顶部“提交修改”上传新版本')).toBeInTheDocument() expect(screen.queryByRole('button', { name: '通过' })).not.toBeInTheDocument() expect(screen.queryByRole('button', { name: '提交给修改人' })).not.toBeInTheDocument() }) test('treats an approved result as the completed end state', () => { render( , ) expect(screen.getByText('作品评审已通过,本作品已完结')).toBeInTheDocument() expect(screen.getByRole('tab', { name: 'V1已过审' })).toBeInTheDocument() expect(screen.queryByRole('button', { name: '提交给修改人' })).not.toBeInTheDocument() }) test('shows the single-review test mode and hides skipped final-reviewer annotations', () => { render( , ) expect(screen.getByRole('status')).toHaveTextContent('已跳过元泽') expect(screen.queryByText('跳过此标注')).not.toBeInTheDocument() expect(screen.queryByText('不应显示的历史意见')).not.toBeInTheDocument() }) })