Files
Charon/docs/plans/current_spec.md

26 KiB

Caddy Import E2E Test Plan - Gap Coverage

Caddy Import E2E Test Plan - Gap Coverage

Created: 2026-01-30 Status: Active Target File: tests/tasks/caddy-import-gaps.spec.ts Related: tests/tasks/caddy-import-debug.spec.ts, tests/tasks/import-caddyfile.spec.ts Created: 2026-01-30 Status: Active Target File: tests/tasks/caddy-import-gaps.spec.ts Related: tests/tasks/caddy-import-debug.spec.ts, tests/tasks/import-caddyfile.spec.ts


Overview

This plan addresses 5 identified gaps in Caddy Import E2E test coverage. Tests will follow established patterns from existing test files, using:

  • Stored auth state (no loginUser() calls needed)
  • Response waiters registered BEFORE click actions
  • Real API calls (no mocking) for reliable integration testing
  • TestDataManager fixture from auth-fixtures.ts for automatic resource cleanup and namespace isolation
  • Relative paths with the request fixture (baseURL pre-configured)
  • Automatic namespacing via TestDataManager to prevent parallel execution conflicts

Gap 1: Success Modal Navigation

Priority: 🔴 CRITICAL Complexity: Medium

Test Case 1.1: Success modal appears after commit

Title: should display success modal after successful import commit

Prerequisites:

  • Container running with healthy API
  • No pending import session

Setup (API):

// TestDataManager handles cleanup automatically
// No explicit setup needed - clean state guaranteed by fixture

Steps:

  1. Navigate to /tasks/import/caddyfile
  2. Paste valid Caddyfile content:
    success-modal-test.example.com {
        reverse_proxy localhost:3000
    }
    
  3. Register response waiter for /api/v1/import/upload
  4. Click "Parse and Review" button
  5. Wait for review table to appear
  6. Register response waiter for /api/v1/import/commit
  7. Click "Commit Import" button
  8. Wait for commit response

Assertions:

  • [data-testid="import-success-modal"] is visible
  • Modal contains text "Import Completed"
  • Modal shows "1 host created" or similar count

Selectors:

Element Selector
Success Modal [data-testid="import-success-modal"]
Commit Button page.getByRole('button', { name: /commit/i })
Modal Header page.getByTestId('import-success-modal').locator('h2')

Test Case 1.2: "View Proxy Hosts" button navigation

Title: should navigate to /proxy-hosts when clicking View Proxy Hosts button

Prerequisites:

  • Success modal visible (chain from 1.1 or re-setup)

Setup (API):

// TestDataManager provides automatic cleanup
// Use helper function to complete import flow

Steps:

  1. Complete import flow (reuse helper or inline steps from 1.1)
  2. Wait for success modal to appear
  3. Click "View Proxy Hosts" button

Assertions:

  • page.url() ends with /proxy-hosts
  • Success modal is no longer visible

Selectors:

Element Selector
View Proxy Hosts Button button:has-text("View Proxy Hosts")

Test Case 1.3: "Go to Dashboard" button navigation

Title: should navigate to /dashboard when clicking Go to Dashboard button

Prerequisites:

  • Success modal visible

Steps:

  1. Complete import flow
  2. Wait for success modal to appear
  3. Click "Go to Dashboard" button

Assertions:

  • page.url() matches / or /dashboard
  • Success modal is no longer visible

Selectors:

Element Selector
Dashboard Button button:has-text("Go to Dashboard")

Overview

This plan addresses 5 identified gaps in Caddy Import E2E test coverage. Tests will follow established patterns from existing test files, using:

  • Stored auth state (no loginUser() calls needed)
  • Response waiters registered BEFORE click actions
  • Real API calls (no mocking) for reliable integration testing
  • TestDataManager fixture from auth-fixtures.ts for automatic resource cleanup and namespace isolation
  • Relative paths with the request fixture (baseURL pre-configured)
  • Automatic namespacing via TestDataManager to prevent parallel execution conflicts

Gap 1: Success Modal Navigation

Priority: 🔴 CRITICAL Complexity: Medium

Test Case 1.1: Success modal appears after commit

Title: should display success modal after successful import commit

Prerequisites:

  • Container running with healthy API
  • No pending import session

Setup (API):

// TestDataManager handles cleanup automatically
// No explicit setup needed - clean state guaranteed by fixture

Steps:

  1. Navigate to /tasks/import/caddyfile
  2. Paste valid Caddyfile content:
    success-modal-test.example.com {
        reverse_proxy localhost:3000
    }
    
  3. Register response waiter for /api/v1/import/upload
  4. Click "Parse and Review" button
  5. Wait for review table to appear
  6. Register response waiter for /api/v1/import/commit
  7. Click "Commit Import" button
  8. Wait for commit response

Assertions:

  • [data-testid="import-success-modal"] is visible
  • Modal contains text "Import Completed"
  • Modal shows "1 host created" or similar count

Selectors:

Element Selector
Success Modal [data-testid="import-success-modal"]
Commit Button page.getByRole('button', { name: /commit/i })
Modal Header page.getByTestId('import-success-modal').locator('h2')

Test Case 1.2: "View Proxy Hosts" button navigation

Title: should navigate to /proxy-hosts when clicking View Proxy Hosts button

Prerequisites:

  • Success modal visible (chain from 1.1 or re-setup)

Setup (API):

// TestDataManager provides automatic cleanup
// Use helper function to complete import flow

Steps:

  1. Complete import flow (reuse helper or inline steps from 1.1)
  2. Wait for success modal to appear
  3. Click "View Proxy Hosts" button

Assertions:

  • page.url() ends with /proxy-hosts
  • Success modal is no longer visible

Selectors:

Element Selector
View Proxy Hosts Button button:has-text("View Proxy Hosts")

Test Case 1.3: "Go to Dashboard" button navigation

Title: should navigate to /dashboard when clicking Go to Dashboard button

Prerequisites:

  • Success modal visible

Steps:

  1. Complete import flow
  2. Wait for success modal to appear
  3. Click "Go to Dashboard" button

Assertions:

  • page.url() matches / or /dashboard
  • Success modal is no longer visible

Selectors:

Element Selector
Dashboard Button button:has-text("Go to Dashboard")

Test Case 1.4: "Close" button behavior

Title: should close modal and stay on import page when clicking Close

Prerequisites:

  • Success modal visible

Steps:

  1. Complete import flow
  2. Wait for success modal to appear
  3. Click "Close" button

Assertions:

  • Success modal is NOT visible
  • page.url() contains /tasks/import/caddyfile
  • Page content shows import form (textarea visible)

Selectors:

Element Selector
Close Button button:has-text("Close") inside modal

Gap 2: Conflict Details Expansion

Priority: 🟠 HIGH Complexity: Complex

Test Case 2.1: Conflict indicator and expand button display

Title: should show conflict indicator and expand button for conflicting domain

Prerequisites:

  • Create existing proxy host via API first

Setup (API):

// Create existing host to generate conflict
// TestDataManager automatically namespaces domain to prevent parallel conflicts
const domain = testData.generateDomain('conflict-test');
const hostId = await testData.createProxyHost({
  name: 'Conflict Test Host',
  domain_names: [domain],
  forward_scheme: 'http',
  forward_host: 'localhost',
  forward_port: 8080,
  enabled: false,
});
// Cleanup handled automatically by TestDataManager fixture

Steps:

  1. Navigate to /tasks/import/caddyfile
  2. Paste Caddyfile with conflicting domain (use namespaced domain):
    // Use the same namespaced domain from setup
    const caddyfile = `${domain} { reverse_proxy localhost:9000 }`;
    await page.locator('textarea').fill(caddyfile);
    
  3. Click "Parse and Review" button
  4. Wait for review table to appear

Assertions:

  • Review table shows row with the namespaced domain
  • Conflict indicator visible (yellow badge with text "Conflict")
  • Expand button (▶) is visible in the row

Selectors (row-scoped):

Element Selector
Domain Row page.locator('tr').filter({ hasText: domain })
Conflict Badge domainRow.locator('.text-yellow-400', { hasText: 'Conflict' })
Expand Button domainRow.getByRole('button', { name: /expand/i })

Test Case 2.2: Side-by-side comparison renders on expand

Title: should display side-by-side configuration comparison when expanding conflict row

Prerequisites:

  • Same as 2.1 (existing host created)

Steps: 1-4: Same as 2.1 5. Click the expand button (▶) in the conflict row

Assertions:

  • Expanded row appears below the conflict row
  • "Current Configuration" section visible
  • "Imported Configuration" section visible
  • Current config shows port 8080
  • Imported config shows port 9000

Selectors:

Element Selector
Current Config Section h4:has-text("Current Configuration")
Imported Config Section h4:has-text("Imported Configuration")
Expanded Row tr.bg-gray-900\\/30
Port Display dd.font-mono containing port number

Test Case 2.3: Recommendation text displays

Title: should show recommendation text in expanded conflict details

Prerequisites:

  • Same as 2.1

Steps: 1-5: Same as 2.2 6. Verify recommendation section

Assertions:

  • Recommendation box visible (blue left border)
  • Text contains "Recommendation:"
  • Text provides actionable guidance

Selectors:

Element Selector
Recommendation Box .border-l-4.border-blue-500 or element containing 💡 Recommendation:

Gap 3: Overwrite Resolution Flow

Priority: 🟠 HIGH Complexity: Complex

Test Case 3.1: Replace with Imported updates existing host

Title: should update existing host when selecting Replace with Imported resolution

Prerequisites:

  • Create existing host via API

Setup (API):

// Create host with initial config
// TestDataManager automatically namespaces domain
const domain = testData.generateDomain('overwrite-test');
const hostId = await testData.createProxyHost({
  name: 'Overwrite Test Host',
  domain_names: [domain],
  forward_scheme: 'http',
  forward_host: 'old-server',
  forward_port: 3000,
  enabled: false,
});
// Cleanup handled automatically

Steps:

  1. Navigate to /tasks/import/caddyfile
  2. Paste Caddyfile with same domain but different config:
    // Use the same namespaced domain from setup
    const caddyfile = `${domain} { reverse_proxy new-server:9000 }`;
    await page.locator('textarea').fill(caddyfile);
    
  3. Register response waiter for upload
  4. Click "Parse and Review" button
  5. Wait for review table
  6. Find resolution dropdown for conflicting row
  7. Select "Replace with Imported" option
  8. Register response waiter for commit
  9. Click "Commit Import" button
  10. Wait for success modal

Assertions:

  • Success modal appears
  • Fetch the host via API: GET /api/v1/proxy-hosts/{hostId}
  • Verify forward_host is "new-server"
  • Verify forward_port is 9000
  • Verify no duplicate was created (only 1 host with this domain)

Selectors (row-scoped):

Element Selector
Domain Row page.locator('tr').filter({ hasText: domain })
Resolution Dropdown domainRow.locator('select')
Overwrite Option dropdown.selectOption({ label: /replace.*imported/i })

Gap 4: Session Resume via Banner

Priority: 🟠 HIGH Complexity: Medium

Test Case 4.1: Banner appears for pending session after navigation

Title: should show pending session banner when returning to import page

Prerequisites:

  • No existing session

Steps:

  1. Navigate to /tasks/import/caddyfile
  2. Paste valid Caddyfile with namespaced domain:
    const domain = testData.generateDomain('session-resume-test');
    const caddyfile = `${domain} { reverse_proxy localhost:4000 }`;
    await page.locator('textarea').fill(caddyfile);
    
  3. Click "Parse and Review" button
  4. Wait for review table to appear (session now created)
  5. Navigate away: page.goto('/proxy-hosts')
  6. Navigate back: page.goto('/tasks/import/caddyfile')

Assertions:

  • [data-testid="import-banner"] is visible
  • Banner contains text "Pending Import Session"
  • "Review Changes" button is visible
  • Review table is NOT visible (until clicking Review Changes)

Selectors:

Element Selector
Import Banner [data-testid="import-banner"]
Banner Text Text containing "Pending Import Session"
Review Changes Button button:has-text("Review Changes")

8.3 Linter Configuration

Verify gopls/staticcheck:

  • Build tags are standard Go feature
  • No linter configuration changes needed
  • GoReleaser will compile each platform separately

Test Case 4.2: Review Changes button restores review table

Title: should restore review table with previous content when clicking Review Changes

Prerequisites:

  • Pending session exists (from 4.1)

Steps: 1-6: Same as 4.1 7. Click "Review Changes" button in banner

Assertions:

  • Review table becomes visible
  • Table contains the namespaced domain from original upload
  • Banner is no longer visible (or integrated into review)

Selectors:

Element Selector
Review Table page.getByTestId('import-review-table')
Domain in Table page.getByTestId('import-review-table').getByText(domain)

GoReleaser Integration

Gap 5: Name Editing in Review

Priority: 🟡 MEDIUM Complexity: Simple

Test Case 5.1: Custom name is saved on commit

Title: should create proxy host with custom name from review table input

Steps:

  1. Navigate to /tasks/import/caddyfile
  2. Paste valid Caddyfile with namespaced domain:
    const domain = testData.generateDomain('custom-name-test');
    const caddyfile = `${domain} { reverse_proxy localhost:5000 }`;
    await page.locator('textarea').fill(caddyfile);
    
  3. Click "Parse and Review" button
  4. Wait for review table
  5. Find the name input field in the row
  6. Clear and fill with custom name: My Custom Proxy Name
  7. Click "Commit Import" button
  8. Wait for success modal
  9. Close modal

Assertions:

  • Fetch all proxy hosts: GET /api/v1/proxy-hosts
  • Find host with the namespaced domain
  • Verify name field equals "My Custom Proxy Name"

Selectors (row-scoped):

Element Selector
Domain Row page.locator('tr').filter({ hasText: domain })
Name Input domainRow.locator('input[type="text"]')
Commit Button page.getByRole('button', { name: /commit/i })

Required Data-TestId Additions

These data-testid attributes would improve test reliability:

Component Recommended TestId Location Notes
Success Modal import-success-modal ImportSuccessModal.tsx Already exists
View Proxy Hosts Button success-modal-view-hosts ImportSuccessModal.tsx line ~85 Static testid
Go to Dashboard Button success-modal-dashboard ImportSuccessModal.tsx line ~90 Static testid
Close Button success-modal-close ImportSuccessModal.tsx line ~80 Static testid
Review Changes Button banner-review-changes ImportBanner.tsx line ~14 Static testid
Import Banner import-banner ImportBanner.tsx Static testid
Review Table import-review-table ImportReviewTable.tsx Static testid

Note: Avoid dynamic testids like name-input-{domain}. Instead, use structural locators that scope to rows first, then find elements within.


Test File Structure

// tests/tasks/caddy-import-gaps.spec.ts

import { test, expect } from '../fixtures/auth-fixtures';

test.describe('Caddy Import Gap Coverage @caddy-import-gaps', () => {

  test.describe('Success Modal Navigation', () => {
    test('should display success modal after successful import commit', async ({ page, testData }) => {
      // testData provides automatic cleanup and namespace isolation
      const domain = testData.generateDomain('success-modal-test');
      await completeImportFlow(page, testData, `${domain} { reverse_proxy localhost:3000 }`);

      await expect(page.getByTestId('import-success-modal')).toBeVisible();
      await expect(page.getByTestId('import-success-modal')).toContainText('Import Completed');
    });
    // 1.2, 1.3, 1.4
  });

  test.describe('Conflict Details Expansion', () => {
    // 2.1, 2.2, 2.3
  });

  test.describe('Overwrite Resolution Flow', () => {
    // 3.1
  });

  test.describe('Session Resume via Banner', () => {
    // 4.1, 4.2
  });

  test.describe('Name Editing in Review', () => {
    // 5.1
  });

});

Complexity Summary

Test Case Complexity API Setup Required Cleanup Required
1.1 Success modal appears Medium None (TestDataManager) Automatic
1.2 View Proxy Hosts nav Medium None (TestDataManager) Automatic
1.3 Dashboard nav Medium None (TestDataManager) Automatic
1.4 Close button Medium None (TestDataManager) Automatic
2.1 Conflict indicator Complex Create host via testData Automatic
2.2 Side-by-side expand Complex Create host via testData Automatic
2.3 Recommendation text Complex Create host via testData Automatic
3.1 Overwrite resolution Complex Create host via testData Automatic
4.1 Banner appears Medium None Automatic
4.2 Review Changes click Medium None Automatic
5.1 Custom name commit Simple None Automatic

Total: 11 test cases Estimated Implementation Time: 6-8 hours

Rationale for Time Increase:

  • TestDataManager integration requires understanding fixture patterns
  • Row-scoped locator strategies more complex than simple testids
  • Parallel execution validation with namespacing
  • Additional validation for automatic cleanup

Helper Functions to Create

import type { Page } from '@playwright/test';
import type { TestDataManager } from '../fixtures/auth-fixtures';

// Helper to complete import and return to success modal
// Uses TestDataManager for automatic cleanup
async function completeImportFlow(
  page: Page,
  testData: TestDataManager,
  caddyfile: string
): Promise<void> {
  await page.goto('/tasks/import/caddyfile');
  await page.locator('textarea').fill(caddyfile);

  const uploadPromise = page.waitForResponse(r =>
    r.url().includes('/api/v1/import/upload') && r.status() === 200
  );
  await page.getByRole('button', { name: /parse|review/i }).click();
  await uploadPromise;

  await expect(page.getByTestId('import-review-table')).toBeVisible();

  const commitPromise = page.waitForResponse(r =>
    r.url().includes('/api/v1/import/commit') && r.status() === 200
  );
  await page.getByRole('button', { name: /commit/i }).click();
  await commitPromise;

  await expect(page.getByTestId('import-success-modal')).toBeVisible();
}

// Note: TestDataManager already provides createProxyHost() method
// No need for standalone helper - use testData.createProxyHost() directly
// Example:
//   const hostId = await testData.createProxyHost({
//     name: 'Test Host',
//     domain_names: [testData.generateDomain('test')],
//     forward_scheme: 'http',
//     forward_host: 'localhost',
//     forward_port: 8080,
//     enabled: false,
//   });

// Note: TestDataManager handles cleanup automatically
// No manual cleanup helper needed

Complexity Summary

Test Case Complexity API Setup Required Cleanup Required
1.1 Success modal appears Medium None (TestDataManager) Automatic
1.2 View Proxy Hosts nav Medium None (TestDataManager) Automatic
1.3 Dashboard nav Medium None (TestDataManager) Automatic
1.4 Close button Medium None (TestDataManager) Automatic
2.1 Conflict indicator Complex Create host via testData Automatic
2.2 Side-by-side expand Complex Create host via testData Automatic
2.3 Recommendation text Complex Create host via testData Automatic
3.1 Overwrite resolution Complex Create host via testData Automatic
4.1 Banner appears Medium None Automatic
4.2 Review Changes click Medium None Automatic
5.1 Custom name commit Simple None Automatic

Total: 11 test cases Estimated Implementation Time: 6-8 hours

Rationale for Time Increase:

  • TestDataManager integration requires understanding fixture patterns
  • Row-scoped locator strategies more complex than simple testids
  • Parallel execution validation with namespacing
  • Additional validation for automatic cleanup

Helper Functions to Create

import type { Page } from '@playwright/test';
import type { TestDataManager } from '../fixtures/auth-fixtures';

// Helper to complete import and return to success modal
// Uses TestDataManager for automatic cleanup
async function completeImportFlow(
  page: Page,
  testData: TestDataManager,
  caddyfile: string
): Promise<void> {
  await page.goto('/tasks/import/caddyfile');
  await page.locator('textarea').fill(caddyfile);

  const uploadPromise = page.waitForResponse(r =>
    r.url().includes('/api/v1/import/upload') && r.status() === 200
  );
  await page.getByRole('button', { name: /parse|review/i }).click();
  await uploadPromise;

  await expect(page.getByTestId('import-review-table')).toBeVisible();

  const commitPromise = page.waitForResponse(r =>
    r.url().includes('/api/v1/import/commit') && r.status() === 200
  );
  await page.getByRole('button', { name: /commit/i }).click();
  await commitPromise;

  await expect(page.getByTestId('import-success-modal')).toBeVisible();
}

// Note: TestDataManager already provides createProxyHost() method
// No need for standalone helper - use testData.createProxyHost() directly
// Example:
//   const hostId = await testData.createProxyHost({
//     name: 'Test Host',
//     domain_names: [testData.generateDomain('test')],
//     forward_scheme: 'http',
//     forward_host: 'localhost',
//     forward_port: 8080,
//     enabled: false,
//   });

// Note: TestDataManager handles cleanup automatically
// No manual cleanup helper needed

Acceptance Criteria

  • All 11 test cases pass consistently (no flakiness)
  • Tests use stored auth state (no login calls)
  • Response waiters registered before click actions
  • All resources cleaned up automatically via TestDataManager fixtures
  • Tests use testData fixture from auth-fixtures.ts
  • No hardcoded domains (use TestDataManager's namespacing)
  • Selectors use row-scoped patterns (filter by domain, then find within row)
  • Relative paths in API calls (no ${baseURL} interpolation)
  • Tests can run in parallel within their describe blocks
  • Total test runtime < 60 seconds
  • All 11 test cases pass consistently (no flakiness)
  • Tests use stored auth state (no login calls)
  • Response waiters registered before click actions
  • All resources cleaned up automatically via TestDataManager fixtures
  • Tests use testData fixture from auth-fixtures.ts
  • No hardcoded domains (use TestDataManager's namespacing)
  • Selectors use row-scoped patterns (filter by domain, then find within row)
  • Relative paths in API calls (no ${baseURL} interpolation)
  • Tests can run in parallel within their describe blocks
  • Total test runtime < 60 seconds

Requirements (EARS Notation)

  1. WHEN the import commit succeeds, THE SYSTEM SHALL display the success modal with created/updated/skipped counts.
  2. WHEN clicking "View Proxy Hosts" in the success modal, THE SYSTEM SHALL navigate to /proxy-hosts.
  3. WHEN clicking "Go to Dashboard" in the success modal, THE SYSTEM SHALL navigate to the dashboard (/).
  4. WHEN clicking "Close" in the success modal, THE SYSTEM SHALL close the modal and remain on the import page.
  5. WHEN importing a Caddyfile with a domain that already exists, THE SYSTEM SHALL display a conflict indicator.
  6. WHEN expanding a conflict row, THE SYSTEM SHALL show side-by-side comparison of current vs imported configuration.
  7. WHEN selecting "Replace with Imported" resolution and committing, THE SYSTEM SHALL update the existing host.
  8. WHEN a pending import session exists, THE SYSTEM SHALL display a yellow banner with "Review Changes" button.
  9. WHEN clicking "Review Changes" on the session banner, THE SYSTEM SHALL restore the review table with previous content.
  10. WHEN editing the name field in the review table, THE SYSTEM SHALL use that custom name when creating the proxy host.

ARCHIVED: Previous Spec

The GoReleaser v2 Migration spec previously in this file has been archived to docs/plans/archived/goreleaser_v2_migration.md.

Requirements (EARS Notation)

  1. WHEN the import commit succeeds, THE SYSTEM SHALL display the success modal with created/updated/skipped counts.
  2. WHEN clicking "View Proxy Hosts" in the success modal, THE SYSTEM SHALL navigate to /proxy-hosts.
  3. WHEN clicking "Go to Dashboard" in the success modal, THE SYSTEM SHALL navigate to the dashboard (/).
  4. WHEN clicking "Close" in the success modal, THE SYSTEM SHALL close the modal and remain on the import page.
  5. WHEN importing a Caddyfile with a domain that already exists, THE SYSTEM SHALL display a conflict indicator.
  6. WHEN expanding a conflict row, THE SYSTEM SHALL show side-by-side comparison of current vs imported configuration.
  7. WHEN selecting "Replace with Imported" resolution and committing, THE SYSTEM SHALL update the existing host.
  8. WHEN a pending import session exists, THE SYSTEM SHALL display a yellow banner with "Review Changes" button.
  9. WHEN clicking "Review Changes" on the session banner, THE SYSTEM SHALL restore the review table with previous content.
  10. WHEN editing the name field in the review table, THE SYSTEM SHALL use that custom name when creating the proxy host.

ARCHIVED: Previous Spec

The GoReleaser v2 Migration spec previously in this file has been archived to docs/plans/archived/goreleaser_v2_migration.md.