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

10 KiB
Raw Blame History

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):

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.