191 lines
7.5 KiB
TypeScript
191 lines
7.5 KiB
TypeScript
import {
|
|
canDeleteWorkReviewProject,
|
|
canResubmitWorkReviewProject,
|
|
canSubmitWorkReview,
|
|
deriveCompletedWorkReviewStatus,
|
|
deriveWorkReviewDecision,
|
|
getWorkReviewEmployeeAliasKeys,
|
|
getWorkReviewEmployeeIdentityKeys,
|
|
getWorkReviewEmployeeMaterialUserIds,
|
|
getWorkReviewReviewerSelectionError,
|
|
isSameWorkReviewEmployeeAlias,
|
|
isSameWorkReviewEmployeeIdentity,
|
|
validateWorkReviewAnnotations,
|
|
} from '@/lib/work-review-policy'
|
|
|
|
describe('work review policy', () => {
|
|
test('allows any matched employee level to submit work', () => {
|
|
expect(canSubmitWorkReview({ levelType: 'P' })).toBe(true)
|
|
expect(canSubmitWorkReview({ levelType: 'M' })).toBe(true)
|
|
expect(canSubmitWorkReview({ levelType: null })).toBe(true)
|
|
expect(canSubmitWorkReview(null)).toBe(false)
|
|
})
|
|
|
|
test('only grants project deletion to the wuxie account', () => {
|
|
expect(canDeleteWorkReviewProject({ username: 'wuxie' })).toBe(true)
|
|
expect(canDeleteWorkReviewProject({ username: ' WUXIE ' })).toBe(true)
|
|
expect(canDeleteWorkReviewProject({ username: '无邪' })).toBe(true)
|
|
expect(canDeleteWorkReviewProject({ username: '无邪管理员' })).toBe(false)
|
|
expect(canDeleteWorkReviewProject({ username: 'admin' })).toBe(false)
|
|
expect(canDeleteWorkReviewProject(null)).toBe(false)
|
|
})
|
|
|
|
test('requires a distinct manual reviewer in addition to the fixed reviewer', () => {
|
|
expect(getWorkReviewReviewerSelectionError({
|
|
reviewerEmployeeId: null,
|
|
submitterEmployeeId: 'submitter',
|
|
finalReviewerEmployeeId: 'yuanze',
|
|
})).toBe('请选择评审人')
|
|
expect(getWorkReviewReviewerSelectionError({
|
|
reviewerEmployeeId: 'submitter',
|
|
submitterEmployeeId: 'submitter',
|
|
finalReviewerEmployeeId: 'yuanze',
|
|
})).toContain('不能选择自己')
|
|
expect(getWorkReviewReviewerSelectionError({
|
|
reviewerEmployeeId: 'yuanze',
|
|
submitterEmployeeId: 'submitter',
|
|
finalReviewerEmployeeId: 'yuanze',
|
|
})).toContain('元泽已是固定评审人')
|
|
expect(getWorkReviewReviewerSelectionError({
|
|
reviewerEmployeeId: 'reviewer',
|
|
submitterEmployeeId: 'submitter',
|
|
finalReviewerEmployeeId: 'yuanze',
|
|
})).toBeNull()
|
|
})
|
|
|
|
test('recognizes historical profiles that belong to the same material account', () => {
|
|
const current = {
|
|
id: 'profile-current',
|
|
dingtalkUserId: 'ding-current',
|
|
dingEmail: 'wuxie@example.com',
|
|
jobNumber: 'JS001',
|
|
sourcePayload: { materialUserId: 'USER-WUXIE' },
|
|
}
|
|
const historical = {
|
|
id: 'profile-old',
|
|
sourcePayload: { userId: 'user-wuxie', materialUserEmail: 'old@example.com' },
|
|
}
|
|
|
|
expect(getWorkReviewEmployeeMaterialUserIds(historical)).toEqual(['user-wuxie'])
|
|
expect(getWorkReviewEmployeeIdentityKeys(current)).toContain('user:user-wuxie')
|
|
expect(isSameWorkReviewEmployeeIdentity(current, historical)).toBe(true)
|
|
expect(isSameWorkReviewEmployeeIdentity(current, {
|
|
id: 'profile-other',
|
|
jobNumber: 'JS002',
|
|
})).toBe(false)
|
|
})
|
|
|
|
test('recognizes simplified and traditional variants of the same employee alias', () => {
|
|
const current = { flowerName: '无邪', realName: '江林铖', displayName: '无邪' }
|
|
const historical = { flowerName: '無邪', realName: '無邪', displayName: '無邪' }
|
|
|
|
expect(getWorkReviewEmployeeAliasKeys(historical)).toContain('无邪')
|
|
expect(isSameWorkReviewEmployeeAlias(current, historical)).toBe(true)
|
|
expect(isSameWorkReviewEmployeeAlias(current, { flowerName: '若兰' })).toBe(false)
|
|
})
|
|
|
|
test('marks one reviewer region as revision required when any image needs changes', () => {
|
|
expect(deriveWorkReviewDecision([
|
|
{ decision: 'approved' },
|
|
{ decision: 'revision_required' },
|
|
])).toBe('revision_required')
|
|
})
|
|
|
|
test('does not complete the version until both independent reviewer regions exist', () => {
|
|
expect(deriveCompletedWorkReviewStatus([
|
|
{ reviewerRole: 'manager', decision: 'approved' },
|
|
])).toBeNull()
|
|
expect(deriveCompletedWorkReviewStatus([
|
|
{ reviewerRole: 'final_reviewer', decision: 'approved' },
|
|
])).toBeNull()
|
|
})
|
|
|
|
test('requires both reviewer regions to approve the final version', () => {
|
|
expect(deriveCompletedWorkReviewStatus([
|
|
{ reviewerRole: 'manager', decision: 'approved' },
|
|
{ reviewerRole: 'final_reviewer', decision: 'approved' },
|
|
])).toBe('approved')
|
|
expect(deriveCompletedWorkReviewStatus([
|
|
{ reviewerRole: 'manager', decision: 'revision_required' },
|
|
{ reviewerRole: 'final_reviewer', decision: 'approved' },
|
|
])).toBe('revision_required')
|
|
})
|
|
|
|
test('completes a manager-only test round immediately after the manager review', () => {
|
|
expect(deriveCompletedWorkReviewStatus([
|
|
{ reviewerRole: 'manager', decision: 'approved' },
|
|
], ['manager'])).toBe('approved')
|
|
expect(deriveCompletedWorkReviewStatus([
|
|
{ reviewerRole: 'manager', decision: 'revision_required' },
|
|
], ['manager'])).toBe('revision_required')
|
|
})
|
|
|
|
test('only allows a new version when both project and latest version require changes', () => {
|
|
const baseProject = {
|
|
status: 'revision_required',
|
|
versions: [{ status: 'revision_required' }],
|
|
permissions: { canResubmit: true },
|
|
}
|
|
|
|
expect(canResubmitWorkReviewProject(baseProject)).toBe(true)
|
|
expect(canResubmitWorkReviewProject({
|
|
...baseProject,
|
|
status: 'approved',
|
|
versions: [{ status: 'approved' }],
|
|
})).toBe(false)
|
|
expect(canResubmitWorkReviewProject({
|
|
...baseProject,
|
|
permissions: { canResubmit: false },
|
|
})).toBe(false)
|
|
})
|
|
|
|
test('normalizes image annotations without storing viewport pixels', () => {
|
|
expect(validateWorkReviewAnnotations([
|
|
{ id: 'point-1', x: 0.123456789, y: 0.75, comment: ' 调整人物位置 ' },
|
|
{
|
|
id: 'box-1',
|
|
x: 0.11111119,
|
|
y: 0.22222229,
|
|
x2: 0.55555559,
|
|
y2: 0.66666669,
|
|
shape: 'rectangle',
|
|
color: '#D92D20',
|
|
comment: ' 框选人物 ',
|
|
},
|
|
])).toEqual({
|
|
ok: true,
|
|
annotations: [
|
|
{ id: 'point-1', x: 0.123457, y: 0.75, comment: '调整人物位置' },
|
|
{
|
|
id: 'box-1',
|
|
x: 0.111111,
|
|
y: 0.222222,
|
|
x2: 0.555556,
|
|
y2: 0.666667,
|
|
shape: 'rectangle',
|
|
color: '#d92d20',
|
|
comment: '框选人物',
|
|
},
|
|
],
|
|
})
|
|
expect(validateWorkReviewAnnotations(undefined)).toEqual({ ok: true, annotations: [] })
|
|
})
|
|
|
|
test.each([
|
|
[[{ id: 'point-1', x: -0.1, y: 0.5, comment: '越界' }], '图片批注位置无效'],
|
|
[[{ id: 'point-1', x: 0.5, y: '0.5', comment: '类型错误' }], '图片批注位置无效'],
|
|
[[{ id: 'box-1', x: 0.2, y: 0.2, shape: 'rectangle', color: '#0050cb', comment: '缺少终点' }], '图片框选范围无效'],
|
|
[[{ id: 'box-1', x: 0.2, y: 0.2, x2: 0.5, y2: 0.5, shape: 'triangle', color: '#0050cb', comment: '形状错误' }], '图片批注形状无效'],
|
|
[[{ id: 'box-1', x: 0.2, y: 0.2, x2: 0.5, y2: 0.5, shape: 'rectangle', color: '#ff00ff', comment: '颜色错误' }], '图片批注颜色无效'],
|
|
[[{ id: 'point-1', x: 0.5, y: 0.5, comment: ' ' }], '请填写每个标注点的修改意见'],
|
|
[[
|
|
{ id: 'point-1', x: 0.2, y: 0.2, comment: 'A' },
|
|
{ id: 'point-1', x: 0.4, y: 0.4, comment: 'B' },
|
|
], '图片批注编号无效或重复'],
|
|
])('rejects invalid image annotations', (annotations, message) => {
|
|
const result = validateWorkReviewAnnotations(annotations)
|
|
expect(result.ok).toBe(false)
|
|
if (!result.ok) expect(result.error).toBe(message)
|
|
})
|
|
})
|