Files
Charon/docs/plans/current_spec.md
GitHub Actions fd056c05a7 feat: Enhance Notifications feature with accessibility improvements and test remediation
- Added aria-label attributes to buttons in Notifications component for better accessibility.
- Updated Notifications tests to use new button interactions and ensure proper functionality.
- Refactored notifications payload tests to mock API responses and validate payload transformations.
- Improved error handling and feedback in notification provider tests.
- Adjusted Telegram notification provider tests to streamline edit interactions.
2026-03-11 15:33:53 +00:00

22 KiB
Raw Blame History

Telegram Notification Provider — Test Failure Remediation Plan

Date: 2026-03-11 Author: Planning Agent Status: Remediation Required — All security scans pass, test failures block merge Previous Plan: Archived as docs/plans/telegram_implementation_spec.md


1. Introduction

The Telegram notification provider feature is functionally complete with passing security scans and coverage gates. However, 56 E2E test failures and 2 frontend unit test failures block the PR merge. This plan identifies root causes, categorises each failure set, and provides specific remediation steps.

Failure Summary

Spec File Failures Browsers Unique Est. Category
notifications.spec.ts 48 3 ~16 Our change
notifications-payload.spec.ts 18 3 ~6 Our change
telegram-notification-provider.spec.ts 4 13 ~2 Our change
encryption-management.spec.ts 20 3 ~7 Pre-existing
auth-middleware-cascade.spec.ts 18 3 6 Pre-existing
Notifications.test.tsx (unit) 2 2 Our change

CI retries: 2 per test (playwright.config.js L144). Failure counts above represent unique test failures × browser projects.


2. Root Cause Analysis

Root Cause A: isNew Guard on Test Button (CRITICAL — Causes ~80% of failures)

What changed: The Telegram feature added a guard in Notifications.tsx (L117-124) that blocks the "Test" button for new (unsaved) providers:

// Line 117-124: handleTest() early return guard
const handleTest = () => {
  const formData = watch();
  const currentType = normalizeProviderType(formData.type);
  if (!formData.id && currentType !== 'email') {
    toast.error(t('notificationProviders.saveBeforeTesting'));
    return;
  }
  testMutation.mutate({ ...formData, type: currentType } as Partial<NotificationProvider>);
};

And a disabled attribute on the test button at Notifications.tsx (L382):

// Line 382: Button disabled state
disabled={testMutation.isPending || (isNew && !isEmail)}

Why it was added: The backend Test handler at notification_provider_handler.go (L333-336) requires a saved provider ID for all non-email types. For Gotify/Telegram, the server needs the stored token. For Discord/Webhook, the server still fetches the provider from DB. Without a saved provider, the backend returns MISSING_PROVIDER_ID.

Why it breaks tests: Many existing E2E and unit tests click the test button from a new (unsaved) provider form using mocked endpoints. With the new guard:

  1. The <button> is disabled → browser ignores clicks → mocked routes never receive requests
  2. Even if not disabled, handleTest() returns early with a toast instead of calling testMutation.mutate()
  3. Tests that waitForRequest on /providers/test time out (60s default)
  4. Tests that assert on capturedTestPayload find null

Is the guard correct? Yes — it matches the backend's security-by-design constraint. The tests need to be adapted to the new behavior, not the guard removed.

Root Cause B: Pre-existing Infrastructure Failures (encryption-management, auth-middleware-cascade)

encryption-management.spec.ts (17 tests, ~7 unique failures) navigates to /security/encryption and tests key rotation, validation, and history display. Zero overlap with notification provider code paths. No files modified in the Telegram PR affect encryption.

auth-middleware-cascade.spec.ts (6 tests, all 6 fail) uses deprecated waitUntil: 'networkidle', creates proxy hosts via UI forms (getByLabel(/domain/i)), and tests auth flows through Caddy. Zero overlap with notification code. These tests have known fragility from UI element selectors and networkidle waits.

Verdict: Both are pre-existing failures. They should be tracked separately and not block the Telegram PR.

Root Cause C: Telegram E2E Spec Issues (4 failures)

The telegram-notification-provider.spec.ts has 8 tests, with ~2 unique failures. Most likely candidates:

  1. "should edit telegram notification provider and preserve token" (L159): Uses fragile keyboard navigation (focus Send Test → Tab → Enter) to reach the Edit button. If the title attribute on the Send Test button doesn't match the accessible name pattern /send test/i, or if the tab order is affected by any intermediate focusable element, the Enter press activates the wrong button or nothing at all.

  2. "should test telegram notification provider" (L265): Clicks the row-level "Send Test" button. The locator uses getByRole('button', { name: /send test/i }). The button has title={t('notificationProviders.sendTest')} which renders as "Send Test". This should work, but the title attribute contributing to accessible name can be browser-dependent, particularly in WebKit.


3. Affected Tests — Complete Inventory

3.1 E2E Tests: notifications.spec.ts (Test Button on New Form)

These tests open the "Add Provider" form (no id), click provider-test-btn, and expect API interactions. The disabled button now prevents all interaction.

# Test Name Line Type Used Failure Mode
1 should test notification provider L1085 discord waitForRequest times out — button disabled
2 should show test success feedback L1142 discord Success icon never appears — no click fires
3 should preserve Discord request payload contract for save, preview, and test L1236 discord capturedTestPayload is null — button disabled
4 should show error when test fails L1665 discord Error icon never appears — no click fires

Additional cascade effects: The user reports ~16 unique failures from this file. The 4 above are directly caused by the isNew guard. Remaining failures may stem from cascading timeout effects, beforeEach state leakage after long timeouts, or other pre-existing flakiness amplified by the 60s timeout waterfall.

3.2 E2E Tests: notifications-payload.spec.ts (Test Button on New Form)

# Test Name Line Type Used Failure Mode
1 provider-specific transformation strips gotify token from test and preview payloads L264 gotify provider-test-btn disabled for new gotify form; capturedTestPayload is null
2 retry split distinguishes retryable and non-retryable failures L410 webhook provider-test-btn disabled for new webhook form; waitForResponse times out

Tests that should still pass:

  • valid payload flows for discord, gotify, and webhook (L54) — uses provider-save-btn, not test button
  • malformed payload scenarios (L158) — API-level tests via page.request.post
  • missing required fields block submit (L192) — uses save button
  • auth/header behavior checks (L217) — API-level tests
  • security: SSRF (L314) — API-level tests
  • security: DNS-rebinding (L381) — API-level tests
  • security: token does not leak (L512) — API-level tests

3.3 E2E Tests: telegram-notification-provider.spec.ts

# Test Name Line Probable Failure Mode
1 should edit telegram notification provider and preserve token L159 Keyboard navigation (Tab from Send Test → Edit) fragility; may hit wrong element on some browsers
2 should test telegram notification provider L265 Row-level Send Test button; possible accessible name mismatch in WebKit with title attribute

Tests that should pass:

  • Form rendering tests (L25, L65) — UI assertions only
  • Create telegram provider (L89) — mocked POST
  • Delete telegram provider (L324) — mocked DELETE + confirm dialog
  • Security tests (L389, L436) — mock-based assertions

3.4 Frontend Unit Tests: Notifications.test.tsx

# Test Name Line Failure Mode
1 submits provider test action from form using normalized discord type L447 userEvent.click() on disabled button is no-op → testProvider never called → waitFor times out
2 shows error toast when test mutation fails L569 Same — disabled button prevents click → toast.error with saveBeforeTesting fires instead of mutation error

3.5 Pre-existing (Not Caused By Telegram PR)

Spec Tests Rationale
encryption-management.spec.ts ~7 unique Tests encryption page at /security/encryption. No code overlap.
auth-middleware-cascade.spec.ts 6 unique Tests proxy creation + auth middleware. Uses networkidle. No code overlap.

4. Remediation Plan

Priority Order

  1. P0 — Fix unit tests (fastest, unblocks local dev verification)
  2. P1 — Fix E2E test-button tests (the core regression from our change)
  3. P2 — Fix telegram spec fragility (new tests we added)
  4. P3 — Document pre-existing failures (not our change, track separately)

4.1 P0: Frontend Unit Test Fixes

File: frontend/src/pages/__tests__/Notifications.test.tsx

Fix 1: "submits provider test action from form using normalized discord type" (L447)

Problem: Test opens "Add Provider" (new form, no id), clicks test button. Button is now disabled for new providers.

Fix: Change to test from an existing provider's edit form instead of a new form. This preserves the original intent (verifying the test payload uses normalized type).

// BEFORE (L447-462):
it('submits provider test action from form using normalized discord type', async () => {
  vi.mocked(notificationsApi.testProvider).mockResolvedValue()
  const user = userEvent.setup()
  renderWithQueryClient(<Notifications />)

  await user.click(await screen.findByTestId('add-provider-btn'))
  await user.type(screen.getByTestId('provider-name'), 'Preview/Test Provider')
  await user.type(screen.getByTestId('provider-url'), 'https://example.com/webhook')
  await user.click(screen.getByTestId('provider-test-btn'))

  await waitFor(() => {
    expect(notificationsApi.testProvider).toHaveBeenCalled()
  })
  const payload = vi.mocked(notificationsApi.testProvider).mock.calls[0][0]
  expect(payload.type).toBe('discord')
})

// AFTER:
it('submits provider test action from form using normalized discord type', async () => {
  vi.mocked(notificationsApi.testProvider).mockResolvedValue()
  setupMocks([baseProvider])          // baseProvider has an id
  const user = userEvent.setup()
  renderWithQueryClient(<Notifications />)

  // Open edit form for existing provider (has id → test button enabled)
  const row = await screen.findByTestId(`provider-row-${baseProvider.id}`)
  const buttons = within(row).getAllByRole('button')
  await user.click(buttons[1]) // Edit button

  await user.click(screen.getByTestId('provider-test-btn'))

  await waitFor(() => {
    expect(notificationsApi.testProvider).toHaveBeenCalled()
  })
  const payload = vi.mocked(notificationsApi.testProvider).mock.calls[0][0]
  expect(payload.type).toBe('discord')
})

Fix 2: "shows error toast when test mutation fails" (L569)

Problem: Same — test opens new form, clicks test button, expects mutation error toast. Button is disabled.

Fix: Test from an existing provider's edit form.

// BEFORE (L569-582):
it('shows error toast when test mutation fails', async () => {
  vi.mocked(notificationsApi.testProvider).mockRejectedValue(new Error('Connection refused'))
  const user = userEvent.setup()
  renderWithQueryClient(<Notifications />)

  await user.click(await screen.findByTestId('add-provider-btn'))
  await user.type(screen.getByTestId('provider-name'), 'Failing Provider')
  await user.type(screen.getByTestId('provider-url'), 'https://example.com/webhook')
  await user.click(screen.getByTestId('provider-test-btn'))

  await waitFor(() => {
    expect(toast.error).toHaveBeenCalledWith('Connection refused')
  })
})

// AFTER:
it('shows error toast when test mutation fails', async () => {
  vi.mocked(notificationsApi.testProvider).mockRejectedValue(new Error('Connection refused'))
  setupMocks([baseProvider])
  const user = userEvent.setup()
  renderWithQueryClient(<Notifications />)

  // Open edit form for existing provider
  const row = await screen.findByTestId(`provider-row-${baseProvider.id}`)
  const buttons = within(row).getAllByRole('button')
  await user.click(buttons[1]) // Edit button

  await user.click(screen.getByTestId('provider-test-btn'))

  await waitFor(() => {
    expect(toast.error).toHaveBeenCalledWith('Connection refused')
  })
})

Bonus: Add a NEW unit test for the saveBeforeTesting guard

it('disables test button when provider is new (unsaved) and not email type', async () => {
  const user = userEvent.setup()
  renderWithQueryClient(<Notifications />)

  await user.click(await screen.findByTestId('add-provider-btn'))
  const testBtn = screen.getByTestId('provider-test-btn')
  expect(testBtn).toBeDisabled()
})

4.2 P1: E2E Test Fixes — notifications.spec.ts

File: tests/settings/notifications.spec.ts

Strategy: For tests that click the test button from a new form, restructure the flow to:

  1. First save the provider (mocked create → returns id)
  2. Then test from the saved provider row's Send Test button (row buttons are not gated by isNew)

Fix 3: "should test notification provider" (L1085)

Current flow: Add form → fill → mock test endpoint → click provider-test-btn → verify request Problem: Test button disabled for new form Fix: Save first, then click test from the provider row's Send Test button.

// In the test, after filling the form and before clicking test:

// 1. Mock the create endpoint to return a provider with an id
await page.route('**/api/v1/notifications/providers', async (route, request) => {
  if (request.method() === 'POST') {
    const payload = await request.postDataJSON();
    await route.fulfill({
      status: 201,
      contentType: 'application/json',
      body: JSON.stringify({ id: 'saved-test-id', ...payload }),
    });
  } else if (request.method() === 'GET') {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify([{
        id: 'saved-test-id',
        name: 'Test Provider',
        type: 'discord',
        url: 'https://discord.com/api/webhooks/test/token',
        enabled: true
      }]),
    });
  } else {
    await route.continue();
  }
});

// 2. Save the provider first
await page.getByTestId('provider-save-btn').click();

// 3. Wait for the provider to appear in the list
await expect(page.getByText('Test Provider')).toBeVisible({ timeout: 5000 });

// 4. Click row-level Send Test button
const providerRow = page.getByTestId('provider-row-saved-test-id');
const sendTestButton = providerRow.getByRole('button', { name: /send test/i });
await sendTestButton.click();

Fix 4: "should show test success feedback" (L1142)

Same pattern as Fix 3: save provider first, then test from row.

Fix 5: "should preserve Discord request payload contract for save, preview, and test" (L1236)

Current flow: Add form → fill → click preview → click test → save → verify all payloads Problem: Test button disabled for new form Fix: Reorder to: Add form → fill → click preview → savetest from row → verify payloads

The preview button is NOT disabled for new forms (only the test button is), so preview still works from the new form. The test step must happen after save.

Fix 6: "should show error when test fails" (L1665)

Same pattern: save first, then test from row.


4.3 P1: E2E Test Fixes — notifications-payload.spec.ts

File: tests/settings/notifications-payload.spec.ts

Fix 7: "provider-specific transformation strips gotify token from test and preview payloads" (L264)

Current flow: Add gotify form → fill with token → click preview → click test → verify token not in payloads Problem: Test button disabled for new gotify form Fix: Preview still works from new form. For test, save first, then test from the saved provider row.

Note: The row-level test call uses { ...provider, type: normalizeProviderType(provider.type) } where provider is the list item (which never contains token/gotify_token per the List handler that strips tokens). So the token-stripping assertion naturally holds for row-level tests.

Fix 8: "retry split distinguishes retryable and non-retryable failures" (L410)

Current flow: Add webhook form → fill → click test → verify retry semantics Problem: Test button disabled for new webhook form Fix: Save first (mock create), then open edit form (which has id) or test from the row.


4.4 P2: Telegram E2E Spec Hardening

File: tests/settings/telegram-notification-provider.spec.ts

Fix 9: "should edit telegram notification provider and preserve token" (L159)

Problem: Uses fragile keyboard navigation to reach the Edit button:

await sendTestButton.focus();
await page.keyboard.press('Tab');
await page.keyboard.press('Enter');

This assumes Tab from Send Test lands on Edit. Tab order can vary across browsers.

Fix: Use a direct locator for the Edit button instead of keyboard navigation:

// BEFORE:
await sendTestButton.focus();
await page.keyboard.press('Tab');
await page.keyboard.press('Enter');

// AFTER:
const editButton = providerRow.getByRole('button').nth(1); // Send Test=0, Edit=1
await editButton.click();

Or use a structural locator based on the edit icon class.

Fix 10: "should test telegram notification provider" (L265)

Probable issue: The getByRole('button', { name: /send test/i }) relies on title for accessible name. WebKit may not compute accessible name from title the same way.

Fix (source — preferred): Add explicit aria-label to the row Send Test button in Notifications.tsx (L703):

<Button
  variant="secondary"
  size="sm"
  onClick={() => testMutation.mutate({...})}
  title={t('notificationProviders.sendTest')}
  aria-label={t('notificationProviders.sendTest')}
>

Fix (test — alternative): Use structural locator:

const sendTestButton = providerRow.locator('button').first();

4.5 P3: Document Pre-existing Failures

Action: File separate issues (not part of this PR) for:

  1. encryption-management.spec.ts — ~7 unique test failures in /security/encryption. Likely UI rendering timing issues or flaky selectors. No code overlap with Telegram PR.

  2. auth-middleware-cascade.spec.ts — All 6 tests fail × 3 browsers. Uses deprecated waitUntil: 'networkidle', creates proxy hosts through fragile UI selectors (getByLabel(/domain/i)), and tests auth middleware cascade. Needs modernization pass for locators and waits.


5. Implementation Plan

Phase 1: Unit Test Fixes (Immediate)

Task File Lines Complexity
Fix "submits provider test action" test Notifications.test.tsx L447-462 Low
Fix "shows error toast" test Notifications.test.tsx L569-582 Low
Add saveBeforeTesting guard unit test Notifications.test.tsx New Low

Validation: cd frontend && npx vitest run src/pages/__tests__/Notifications.test.tsx

Phase 2: E2E Test Fixes — Core Regression

Task File Lines Complexity
Fix "should test notification provider" notifications.spec.ts L1085-1138 Medium
Fix "should show test success feedback" notifications.spec.ts L1142-1178 Medium
Fix "should preserve Discord payload contract" notifications.spec.ts L1236-1340 Medium
Fix "should show error when test fails" notifications.spec.ts L1665-1706 Medium
Fix "transformation strips gotify token" notifications-payload.spec.ts L264-312 Medium
Fix "retry split retryable/non-retryable" notifications-payload.spec.ts L410-510 High

Validation per test: npx playwright test --project=firefox <spec-file> -g "<test-name>"

Phase 3: Telegram Spec Hardening

Task File Lines Complexity
Replace keyboard nav with direct locator telegram-notification-provider.spec.ts L220-223 Low
Add aria-label to row Send Test button Notifications.tsx L703-708 Low
Verify all 8 telegram tests pass 3 browsers All Low

Validation: npx playwright test tests/settings/telegram-notification-provider.spec.ts

Phase 4: Accessibility Hardening (Optional — Low Priority)

Consider adding aria-label attributes to all icon-only buttons in the provider row for improved accessibility and test resilience:

Button Current Accessible Name Source Recommended
Send Test title attribute Add aria-label
Edit None (icon only) Add aria-label={t('common.edit')}
Delete None (icon only) Add aria-label={t('common.delete')}

6. Commit Slicing Strategy

Decision: Single PR with 2 focused commits

Rationale: All fixes are tightly coupled to the Telegram feature PR and represent test adaptations to a correct behavioral change. No cross-domain changes. Small total diff.

Commit 1: "fix(test): adapt notification tests to save-before-test guard"

  • Scope: All unit test and E2E test fixes (Phases 1-3)
  • Files: Notifications.test.tsx, notifications.spec.ts, notifications-payload.spec.ts, telegram-notification-provider.spec.ts
  • Dependencies: None
  • Validation Gate: All notification-related tests pass locally on at least one browser

Commit 2: "feat(a11y): add aria-labels to notification provider row buttons"

  • Scope: Source code accessibility improvement (Phase 4)
  • Files: Notifications.tsx
  • Dependencies: Depends on Commit 1 (tests must pass first)
  • Validation Gate: Telegram spec tests pass consistently on WebKit

Rollback

  • These are test-only changes (except the optional aria-label). Reverting either commit has zero production impact.
  • If tests still fail after fixes, the next step is to run with --debug and capture trace artifacts.

7. Acceptance Criteria

  • Notifications.test.tsx — all 2 previously failing tests pass
  • notifications.spec.ts — all 4 isNew-guard-affected tests pass on 3 browsers
  • notifications-payload.spec.ts — "transformation" and "retry split" tests pass on 3 browsers
  • telegram-notification-provider.spec.ts — all 8 tests pass on 3 browsers
  • No regressions in other notification tests
  • New unit test validates the saveBeforeTesting guard / disabled button behavior
  • encryption-management.spec.ts and auth-middleware-cascade.spec.ts failures documented as separate issues (not blocked by this PR)