Files
Charon/docs/plans/current_spec.md
GitHub Actions 49b3e4e537 fix(tests): resolve i18n mock issues in BulkDeleteCertificateDialog tests
Removed local i18n mock to allow global mock to function correctly, updated assertions to use resolved English translations for better consistency in test outcomes.
2026-03-24 01:47:43 +00:00

205 lines
10 KiB
Markdown
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.
# 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.tsx` with 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.cancel` instead of `"Cancel"`
- Warning text: `certificates.bulkDeleteConfirm` instead of translated string
- Aria label: `certificates.bulkDeleteListAriaLabel` instead 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 2060) |
| `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 2060):
- 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 `setupFiles` in vitest config
**Local mock** (`BulkDeleteCertificateDialog.test.tsx`, lines 914):
```typescript
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 914)**
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`
1. Remove the local `vi.mock('react-i18next', ...)` block (lines 914)
2. Update 5 assertion strings to use resolved English translations (see table in §4.1)
3. No other files need changes
### Phase 2: Validation
1. Run the specific test file: `cd /projects/Charon/frontend && npx vitest run src/components/dialogs/__tests__/BulkDeleteCertificateDialog.test.tsx`
2. Run the full frontend test suite: `cd /projects/Charon/frontend && npx vitest run`
3. Verify no regressions in other test files
---
## 6. Acceptance Criteria
- [ ] Both failing tests pass: `lists each certificate name in the scrollable list` and `renders "Expiring LE" label for a letsencrypt cert with status expiring`
- [ ] All 7 tests in `BulkDeleteCertificateDialog.test.tsx` pass
- [ ] Full frontend test suite passes with no new failures
- [ ] No local `vi.mock('react-i18next')` remains in `BulkDeleteCertificateDialog.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.