Removed local i18n mock to allow global mock to function correctly, updated assertions to use resolved English translations for better consistency in test outcomes.
10 KiB
Fix: Frontend Unit Test i18n Failures in BulkDeleteCertificateDialog
Status: Ready for implementation Severity: CI-blocking (2 test failures) Scope: Single test file change
1. Introduction
Two frontend unit tests fail in CI because BulkDeleteCertificateDialog.test.tsx contains a local vi.mock('react-i18next') that overrides the global mock in the test setup. The local mock returns raw translation keys and JSON-serialized options instead of resolved English strings, causing assertion mismatches.
Objectives
- Fix the 2 failing tests in CI
- Align
BulkDeleteCertificateDialog.test.tsxwith the project's established i18n test pattern - No behavioral or component changes required
2. Research Findings
2.1 Failing Tests (from CI log)
| # | Test Name | Expected | Actual (DOM) |
|---|---|---|---|
| 1 | lists each certificate name in the scrollable list |
"Custom", "Staging", "Expired LE" |
certificates.providerCustom, certificates.providerStaging, certificates.providerExpiredLE |
| 2 | renders "Expiring LE" label for a letsencrypt cert with status expiring |
"Expiring LE" |
certificates.providerExpiringLE |
Additional rendering artifacts visible in the DOM dump:
- Dialog title:
{"count":3}instead of"Delete 3 Certificate(s)" - Button text:
{"count":3}instead of"Delete 3 Certificate(s)" - Cancel button:
common.cancelinstead of"Cancel" - Warning text:
certificates.bulkDeleteConfirminstead of translated string - Aria label:
certificates.bulkDeleteListAriaLabelinstead of translated string
2.2 Relevant File Paths
| File | Role |
|---|---|
frontend/src/components/dialogs/__tests__/BulkDeleteCertificateDialog.test.tsx |
Failing test file — contains the problematic local mock |
frontend/src/components/dialogs/BulkDeleteCertificateDialog.tsx |
Component under test |
frontend/src/test/setup.ts |
Global test setup with proper i18n mock (lines 20–60) |
frontend/vitest.config.ts |
Vitest config — confirms setupFiles: './src/test/setup.ts' (line 24) |
frontend/src/locales/en/translation.json |
English translations source |
2.3 i18n Mock Architecture
Global mock (frontend/src/test/setup.ts, lines 20–60):
- Dynamically imports
../locales/en/translation.json - Implements
getTranslation(key)that resolves dot-notation keys (e.g.,certificates.providerCustom→"Custom") - Handles
{{variable}}interpolation via regex replacement - Applied automatically to all test files via
setupFilesin vitest config
Local mock (BulkDeleteCertificateDialog.test.tsx, lines 9–14):
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, opts?: Record<string, unknown>) => (opts ? JSON.stringify(opts) : key),
i18n: { language: 'en', changeLanguage: vi.fn() },
}),
}))
This local mock overrides the global mock because Vitest's vi.mock() at the file level takes precedence over the setup file's vi.mock(). It returns:
- Raw key when no options:
t('certificates.providerCustom')→"certificates.providerCustom" - JSON string when options present:
t('key', { count: 3 })→'{"count":3}'
2.4 Translation Keys Required
From frontend/src/locales/en/translation.json:
| Key | English Value |
|---|---|
certificates.bulkDeleteTitle |
"Delete {{count}} Certificate(s)" |
certificates.bulkDeleteDescription |
"Delete {{count}} certificate(s)" |
certificates.bulkDeleteConfirm |
"The following certificates will be permanently deleted. The server creates a backup before each removal." |
certificates.bulkDeleteListAriaLabel |
"Certificates to be deleted" |
certificates.bulkDeleteButton |
"Delete {{count}} Certificate(s)" |
certificates.providerStaging |
"Staging" |
certificates.providerCustom |
"Custom" |
certificates.providerExpiredLE |
"Expired LE" |
certificates.providerExpiringLE |
"Expiring LE" |
common.cancel |
"Cancel" |
All keys exist in the translation file. No missing translations.
2.5 Pattern Analysis — Other Test Files
20+ test files have local vi.mock('react-i18next') overrides. Most use t: (key) => key and assert against raw keys — this is internally consistent and not failing. The BulkDeleteCertificateDialog.test.tsx file is unique because its assertions expect translated values while its mock returns raw keys.
| File | Local Mock | Assertions | Status |
|---|---|---|---|
CertificateList.test.tsx |
t: (key) => key |
Raw keys (certificates.deleteTitle) |
Passing |
Certificates.test.tsx |
Custom translations map | Translated values | Passing |
AccessLists.test.tsx |
Custom translations map | Translated values | Passing |
| BulkDeleteCertificateDialog.test.tsx | t: (key, opts) => opts ? JSON.stringify(opts) : key |
Mix of translated values AND raw keys | Failing |
3. Root Cause Analysis
The local vi.mock('react-i18next') in BulkDeleteCertificateDialog.test.tsx returns raw translation keys, but the test assertions expect resolved English strings.
This is a mock/assertion mismatch introduced when the test was authored. The test expectations ('Custom', 'Expiring LE') are correct for what the component should render, but the mock prevents translation resolution.
4. Technical Specification
4.1 Fix: Remove Local Mock, Update Assertions
File: frontend/src/components/dialogs/__tests__/BulkDeleteCertificateDialog.test.tsx
Change 1 — Delete the local vi.mock('react-i18next', ...) block (lines 9–14)
Removing this allows the global mock from setup.ts to take effect, which properly resolves translation keys to English values with interpolation.
Change 2 — Update assertions that relied on the local mock's behavior
With the global mock active, translation calls resolve differently:
| Call in component | Local mock output | Global mock output |
|---|---|---|
t('certificates.bulkDeleteTitle', { count: 3 }) |
'{"count":3}' |
'Delete 3 Certificate(s)' |
t('certificates.bulkDeleteButton', { count: 3 }) |
'{"count":3}' |
'Delete 3 Certificate(s)' |
t('certificates.bulkDeleteButton', { count: 1 }) |
'{"count":1}' |
'Delete 1 Certificate(s)' |
t('common.cancel') |
'common.cancel' |
'Cancel' |
t('certificates.providerCustom') |
'certificates.providerCustom' |
'Custom' |
t('certificates.providerExpiringLE') |
'certificates.providerExpiringLE' |
'Expiring LE' |
Assertions to update:
| Line | Old Assertion | New Assertion |
|---|---|---|
| ~48 | getByRole('heading', { name: '{"count":3}' }) |
getByRole('heading', { name: 'Delete 3 Certificate(s)' }) |
| ~82 | getByRole('button', { name: '{"count":3}' }) |
getByRole('button', { name: 'Delete 3 Certificate(s)' }) |
| ~95 | getByRole('button', { name: 'common.cancel' }) |
getByRole('button', { name: 'Cancel' }) |
| ~109 | getByRole('button', { name: '{"count":3}' }) |
getByRole('button', { name: 'Delete 3 Certificate(s)' }) |
| ~111 | getByRole('button', { name: 'common.cancel' }) |
getByRole('button', { name: 'Cancel' }) |
The currently-failing assertions (getByText('Custom'), getByText('Expiring LE'), etc.) will pass without changes once the global mock is active.
4.2 Config File Review
| File | Finding |
|---|---|
.gitignore |
No changes needed. Test artifacts, coverage outputs, and CI logs are properly excluded. |
codecov.yml |
No changes needed. Test files (**/__tests__/**, **/*.test.tsx) and test setup (**/vitest.config.ts, **/vitest.setup.ts) are already excluded from coverage. |
.dockerignore |
No changes needed. Test artifacts and coverage files are excluded from Docker builds. |
Dockerfile |
No changes needed. No test files are copied into the production image. |
5. Implementation Plan
Phase 1: Fix the Test File
Single file edit: frontend/src/components/dialogs/__tests__/BulkDeleteCertificateDialog.test.tsx
- Remove the local
vi.mock('react-i18next', ...)block (lines 9–14) - Update 5 assertion strings to use resolved English translations (see table in §4.1)
- No other files need changes
Phase 2: Validation
- Run the specific test file:
cd /projects/Charon/frontend && npx vitest run src/components/dialogs/__tests__/BulkDeleteCertificateDialog.test.tsx - Run the full frontend test suite:
cd /projects/Charon/frontend && npx vitest run - Verify no regressions in other test files
6. Acceptance Criteria
- Both failing tests pass:
lists each certificate name in the scrollable listandrenders "Expiring LE" label for a letsencrypt cert with status expiring - All 7 tests in
BulkDeleteCertificateDialog.test.tsxpass - Full frontend test suite passes with no new failures
- No local
vi.mock('react-i18next')remains inBulkDeleteCertificateDialog.test.tsx
7. Commit Slicing Strategy
Decision: Single PR
Rationale: This is a single-file fix with no cross-domain changes, no schema changes, no API changes, and no risk of affecting other components. The change is purely correcting assertion/mock alignment in one test file.
PR-1: Fix BulkDeleteCertificateDialog i18n test mock
| Attribute | Value |
|---|---|
| Scope | Remove local i18n mock override, update 5 assertions |
| Files | frontend/src/components/dialogs/__tests__/BulkDeleteCertificateDialog.test.tsx |
| Dependencies | None |
| Validation Gate | All 7 tests in the file pass; full frontend suite green |
| Rollback | Revert single commit |
Contingency
If the global mock from setup.ts does not resolve all keys correctly (unlikely given the translation JSON analysis), the fallback is to replace the local mock with a custom translations map pattern (as used in AccessLists.test.tsx and Certificates.test.tsx) containing the exact keys needed by this component.