{t('skillAvatar.failed')}
} {settings.data && <> {(['machineEnabled', 'manualEnabled'] as const).map((field) => )} + {!draft.machineEnabled && !draft.manualEnabled &&{t('skillAvatar.noReviewRisk')}
} >} {saved &&{t('skillAvatar.saved')}
} diff --git a/web/src/features/skill/skill-avatar-manager.test.tsx b/web/src/features/skill/skill-avatar-manager.test.tsx new file mode 100644 index 00000000..edbf10cc --- /dev/null +++ b/web/src/features/skill/skill-avatar-manager.test.tsx @@ -0,0 +1,39 @@ +/** @vitest-environment jsdom */ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { cleanup, render, screen } from '@testing-library/react' +import { afterEach, describe, expect, it, vi } from 'vitest' + +const api = vi.hoisted(() => ({ status: vi.fn() })) +vi.mock('@/api/client', () => ({ skillAvatarApi: api, buildApiUrl: (url: string) => url })) +vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string) => key }) })) +import { SkillAvatarManager } from './skill-avatar-manager' + +describe('SkillAvatarManager appeal', () => { + afterEach(() => { cleanup(); api.status.mockReset() }) + + it.each([ + ['PENDING', 'BLOCKED', 'NOT_REQUIRED', true], + ['PENDING', 'BLOCKED', 'APPEAL_PENDING', false], + ['PENDING', 'PROCESSING', 'NOT_REQUIRED', false], + ['REJECTED', 'BLOCKED', 'REJECTED', false], + ['PENDING', 'ERROR', 'NOT_REQUIRED', false], + ] as const)('shows appeal only for machine-only blocked candidate: %s/%s/%s', async (candidateStatus, machineStatus, reviewStatus, expected) => { + api.status.mockResolvedValue({ avatarUrl: null, candidateId: 7, candidateStatus, machineStatus, reviewStatus, candidateUrl: null, reviewReason: null }) + const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }) + render({t('skillAvatar.ownerHelp')}
{candidate?.candidateStatus &&{t('skillAvatar.status')}: {t(`skillAvatar.states.${candidate.candidateStatus}`)} · {t(`skillAvatar.machine.${candidate.machineStatus ?? 'SKIPPED'}`)}
} {candidate?.reviewReason &&{candidate.reviewReason}
} + {candidate?.machineStatus === 'ERROR' && retryCoolingDown &&{t('skillAvatar.retryAfter', { time: new Date(candidate.machineNextRetryAt!).toLocaleString() })}
} + {candidate?.machineStatus === 'ERROR' && candidate.automaticRetryExhausted &&{t('skillAvatar.automaticRetryStopped')}
} {candidate?.candidateUrl &&{error}
}