gongxiangp2/lib/__tests__/work-review-reviewer-workspace.test.tsx

416 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/** @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(<WorkReviewReviewerWorkspace project={{
...project,
versions: [{
...project.versions[0],
imageGroups: [
{ ...group, imageId: 'main', sourceName: '主图作品', assetType: 'main' },
{ ...group, imageId: 'sku', sourceName: 'SKU作品', assetType: 'sku' },
{ ...group, imageId: 'detail', sourceName: '详情作品', assetType: 'detail' },
],
referenceImageGroups: [
{ ...group, imageId: 'ref-main', sourceName: '主图参考', assetType: 'main' },
{ ...group, imageId: 'ref-sku', sourceName: 'SKU参考', assetType: 'sku' },
],
}],
}} onSubmitReview={jest.fn()} />)
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(<WorkReviewReviewerWorkspace project={{
...project,
versions: [{
...project.versions[0],
imageGroups: [
{ ...group, imageId: 'main-1', sourceName: '第一张主图', assetType: 'main' },
{ ...group, imageId: 'main-2', sourceName: '第二张主图', assetType: 'main' },
{ ...group, imageId: 'sku-1', sourceName: '第一张SKU', assetType: 'sku' },
{ ...group, imageId: 'sku-2', sourceName: '第二张SKU', assetType: 'sku' },
],
}],
}} onSubmitReview={jest.fn()} />)
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(
<WorkReviewReviewerWorkspace
project={{
...project,
description: '用于测试新版视觉主图。\n重点关注标题层级与产品卖点。',
}}
onSubmitReview={jest.fn()}
/>,
)
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(<WorkReviewReviewerWorkspace project={project} onSubmitReview={jest.fn()} />)
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(<WorkReviewReviewerWorkspace project={project} onSubmitReview={onSubmitReview} />)
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(<WorkReviewReviewerWorkspace project={project} onSubmitReview={onSubmitReview} />)
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(<WorkReviewReviewerWorkspace project={project} onSubmitReview={onSubmitReview} />)
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(<WorkReviewReviewerWorkspace project={projectWithReference} onSubmitReview={jest.fn()} />)
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(<WorkReviewReviewerWorkspace project={project} onSubmitReview={jest.fn()} />)
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(
<WorkReviewReviewerWorkspace
project={{
...project,
permissions: { canResubmit: false, pendingReviewRoles: [] },
}}
onSubmitReview={jest.fn()}
/>,
)
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(
<WorkReviewReviewerWorkspace
project={{
...project,
status: 'revision_required',
versions: [{ ...project.versions[0], status: 'revision_required' }],
permissions: { canResubmit: true, pendingReviewRoles: [] },
}}
onSubmitReview={jest.fn()}
/>,
)
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(
<WorkReviewReviewerWorkspace
project={{
...project,
status: 'approved',
versions: [{ ...project.versions[0], status: 'approved', completedAt: '2026-08-01T08:00:00.000Z' }],
permissions: { canResubmit: true, pendingReviewRoles: [] },
}}
onSubmitReview={jest.fn()}
/>,
)
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(
<WorkReviewReviewerWorkspace
project={{
...project,
reviewMode: 'single_reviewer_test',
requiredReviewRoles: ['manager'],
versions: [{
...project.versions[0],
reviews: [{
id: 'skipped-review',
reviewerRole: 'final_reviewer',
reviewerName: '元泽',
decision: 'revision_required',
imageComments: [{
imageIndex: 0,
decision: 'revision_required',
comment: '不应显示的历史意见',
annotations: [{ id: 'skipped-point', x: 0.5, y: 0.5, comment: '跳过此标注' }],
}],
generalComment: null,
reviewedAt: '2026-08-01T08:00:00.000Z',
}],
}],
}}
onSubmitReview={jest.fn()}
/>,
)
expect(screen.getByRole('status')).toHaveTextContent('已跳过元泽')
expect(screen.queryByText('跳过此标注')).not.toBeInTheDocument()
expect(screen.queryByText('不应显示的历史意见')).not.toBeInTheDocument()
})
})