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.tsfor automatic resource cleanup and namespace isolation - Relative paths with the
requestfixture (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:
- Navigate to
/tasks/import/caddyfile - Paste valid Caddyfile content:
success-modal-test.example.com { reverse_proxy localhost:3000 } - Register response waiter for
/api/v1/import/upload - Click "Parse and Review" button
- Wait for review table to appear
- Register response waiter for
/api/v1/import/commit - Click "Commit Import" button
- 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:
- Complete import flow (reuse helper or inline steps from 1.1)
- Wait for success modal to appear
- 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:
- Complete import flow
- Wait for success modal to appear
- 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.tsfor automatic resource cleanup and namespace isolation - Relative paths with the
requestfixture (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:
- Navigate to
/tasks/import/caddyfile - Paste valid Caddyfile content:
success-modal-test.example.com { reverse_proxy localhost:3000 } - Register response waiter for
/api/v1/import/upload - Click "Parse and Review" button
- Wait for review table to appear
- Register response waiter for
/api/v1/import/commit - Click "Commit Import" button
- 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:
- Complete import flow (reuse helper or inline steps from 1.1)
- Wait for success modal to appear
- 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:
- Complete import flow
- Wait for success modal to appear
- 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:
- Complete import flow
- Wait for success modal to appear
- 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:
- Navigate to
/tasks/import/caddyfile - 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); - Click "Parse and Review" button
- 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:
- Navigate to
/tasks/import/caddyfile - 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); - Register response waiter for upload
- Click "Parse and Review" button
- Wait for review table
- Find resolution dropdown for conflicting row
- Select "Replace with Imported" option
- Register response waiter for commit
- Click "Commit Import" button
- Wait for success modal
Assertions:
- Success modal appears
- Fetch the host via API:
GET /api/v1/proxy-hosts/{hostId} - Verify
forward_hostis"new-server" - Verify
forward_portis9000 - 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:
- Navigate to
/tasks/import/caddyfile - 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); - Click "Parse and Review" button
- Wait for review table to appear (session now created)
- Navigate away:
page.goto('/proxy-hosts') - 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:
- Navigate to
/tasks/import/caddyfile - 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); - Click "Parse and Review" button
- Wait for review table
- Find the name input field in the row
- Clear and fill with custom name:
My Custom Proxy Name - Click "Commit Import" button
- Wait for success modal
- Close modal
Assertions:
- Fetch all proxy hosts:
GET /api/v1/proxy-hosts - Find host with the namespaced domain
- Verify
namefield 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
testDatafixture fromauth-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
testDatafixture fromauth-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)
- WHEN the import commit succeeds, THE SYSTEM SHALL display the success modal with created/updated/skipped counts.
- WHEN clicking "View Proxy Hosts" in the success modal, THE SYSTEM SHALL navigate to
/proxy-hosts. - WHEN clicking "Go to Dashboard" in the success modal, THE SYSTEM SHALL navigate to the dashboard (
/). - WHEN clicking "Close" in the success modal, THE SYSTEM SHALL close the modal and remain on the import page.
- WHEN importing a Caddyfile with a domain that already exists, THE SYSTEM SHALL display a conflict indicator.
- WHEN expanding a conflict row, THE SYSTEM SHALL show side-by-side comparison of current vs imported configuration.
- WHEN selecting "Replace with Imported" resolution and committing, THE SYSTEM SHALL update the existing host.
- WHEN a pending import session exists, THE SYSTEM SHALL display a yellow banner with "Review Changes" button.
- WHEN clicking "Review Changes" on the session banner, THE SYSTEM SHALL restore the review table with previous content.
- 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)
- WHEN the import commit succeeds, THE SYSTEM SHALL display the success modal with created/updated/skipped counts.
- WHEN clicking "View Proxy Hosts" in the success modal, THE SYSTEM SHALL navigate to
/proxy-hosts. - WHEN clicking "Go to Dashboard" in the success modal, THE SYSTEM SHALL navigate to the dashboard (
/). - WHEN clicking "Close" in the success modal, THE SYSTEM SHALL close the modal and remain on the import page.
- WHEN importing a Caddyfile with a domain that already exists, THE SYSTEM SHALL display a conflict indicator.
- WHEN expanding a conflict row, THE SYSTEM SHALL show side-by-side comparison of current vs imported configuration.
- WHEN selecting "Replace with Imported" resolution and committing, THE SYSTEM SHALL update the existing host.
- WHEN a pending import session exists, THE SYSTEM SHALL display a yellow banner with "Review Changes" button.
- WHEN clicking "Review Changes" on the session banner, THE SYSTEM SHALL restore the review table with previous content.
- 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.