fix: enhance import tests with user authentication handling and precondition checks
This commit is contained in:
@@ -17,8 +17,9 @@
|
||||
* Those are verified in backend/integration/ tests.
|
||||
*/
|
||||
|
||||
import { test, expect } from '../../fixtures/auth-fixtures';
|
||||
import { test, expect, type TestUser } from '../../fixtures/auth-fixtures';
|
||||
import { Page } from '@playwright/test';
|
||||
import { ensureImportUiPreconditions } from './import-page-helpers';
|
||||
|
||||
/**
|
||||
* Mock Caddyfile content for testing
|
||||
@@ -182,16 +183,20 @@ async function setupImportMocks(
|
||||
});
|
||||
}
|
||||
|
||||
async function gotoImportPageWithAuthRecovery(page: Page, adminUser: TestUser): Promise<void> {
|
||||
await ensureImportUiPreconditions(page, adminUser);
|
||||
}
|
||||
|
||||
test.describe('Caddy Import - Cross-Browser @cross-browser', () => {
|
||||
/**
|
||||
* TEST 1: Parse valid Caddyfile across all browsers
|
||||
* Verifies basic import flow works identically in Chromium, Firefox, and WebKit
|
||||
*/
|
||||
test('should parse valid Caddyfile in all browsers', async ({ page, browserName }) => {
|
||||
test('should parse valid Caddyfile in all browsers', async ({ page, browserName, adminUser }) => {
|
||||
await setupImportMocks(page);
|
||||
|
||||
await test.step(`[${browserName}] Navigate to import page`, async () => {
|
||||
await page.goto('/tasks/import/caddyfile');
|
||||
await gotoImportPageWithAuthRecovery(page, adminUser);
|
||||
await expect(page.locator('h1')).toContainText(/import/i);
|
||||
});
|
||||
|
||||
@@ -239,11 +244,11 @@ test.describe('Caddy Import - Cross-Browser @cross-browser', () => {
|
||||
* TEST 2: Handle syntax errors across all browsers
|
||||
* Verifies error handling works consistently
|
||||
*/
|
||||
test('should show error for invalid Caddyfile syntax in all browsers', async ({ page, browserName }) => {
|
||||
test('should show error for invalid Caddyfile syntax in all browsers', async ({ page, browserName, adminUser }) => {
|
||||
await setupImportMocks(page, { uploadSuccess: false });
|
||||
|
||||
await test.step(`[${browserName}] Navigate to import page`, async () => {
|
||||
await page.goto('/tasks/import/caddyfile');
|
||||
await gotoImportPageWithAuthRecovery(page, adminUser);
|
||||
});
|
||||
|
||||
await test.step(`[${browserName}] Paste invalid content and parse`, async () => {
|
||||
@@ -267,9 +272,9 @@ test.describe('Caddy Import - Cross-Browser @cross-browser', () => {
|
||||
* TEST 3: Multi-file import flow across all browsers
|
||||
* Tests the multi-file import modal and API interaction
|
||||
*/
|
||||
test('should handle multi-file import in all browsers', async ({ page, browserName }) => {
|
||||
test('should handle multi-file import in all browsers', async ({ page, browserName, adminUser }) => {
|
||||
await test.step(`[${browserName}] Navigate to import page`, async () => {
|
||||
await page.goto('/tasks/import/caddyfile');
|
||||
await gotoImportPageWithAuthRecovery(page, adminUser);
|
||||
});
|
||||
|
||||
await test.step(`[${browserName}] Set up multi-file API mocks`, async () => {
|
||||
@@ -314,7 +319,7 @@ test.describe('Caddy Import - Cross-Browser @cross-browser', () => {
|
||||
* TEST 4: Conflict resolution flow across all browsers
|
||||
* Creates a host, then imports a conflicting host to verify conflict handling
|
||||
*/
|
||||
test('should handle conflict resolution in all browsers', async ({ page, browserName }) => {
|
||||
test('should handle conflict resolution in all browsers', async ({ page, browserName, adminUser }) => {
|
||||
await setupImportMocks(page, {
|
||||
previewHosts: [
|
||||
{ domain_names: 'existing.example.com', forward_host: 'new-server', forward_port: 8080, forward_scheme: 'https' },
|
||||
@@ -354,7 +359,7 @@ test.describe('Caddy Import - Cross-Browser @cross-browser', () => {
|
||||
});
|
||||
|
||||
await test.step(`[${browserName}] Navigate to import page`, async () => {
|
||||
await page.goto('/tasks/import/caddyfile');
|
||||
await gotoImportPageWithAuthRecovery(page, adminUser);
|
||||
});
|
||||
|
||||
await test.step(`[${browserName}] Parse conflicting Caddyfile`, async () => {
|
||||
@@ -388,7 +393,7 @@ test.describe('Caddy Import - Cross-Browser @cross-browser', () => {
|
||||
* TEST 5: Session resume across all browsers
|
||||
* Verifies that starting an import, navigating away, and returning shows the session
|
||||
*/
|
||||
test('should resume import session in all browsers', async ({ page, browserName }) => {
|
||||
test('should resume import session in all browsers', async ({ page, browserName, adminUser }) => {
|
||||
await setupImportMocks(page, {
|
||||
previewHosts: [
|
||||
{ domain_names: 'test.example.com', forward_host: 'localhost', forward_port: 3000, forward_scheme: 'http' },
|
||||
@@ -396,7 +401,7 @@ test.describe('Caddy Import - Cross-Browser @cross-browser', () => {
|
||||
});
|
||||
|
||||
await test.step(`[${browserName}] Navigate to import page`, async () => {
|
||||
await page.goto('/tasks/import/caddyfile');
|
||||
await gotoImportPageWithAuthRecovery(page, adminUser);
|
||||
});
|
||||
|
||||
await test.step(`[${browserName}] Start import session`, async () => {
|
||||
@@ -432,7 +437,7 @@ test.describe('Caddy Import - Cross-Browser @cross-browser', () => {
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/tasks/import/caddyfile');
|
||||
await page.goto('/tasks/import/caddyfile', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Should show banner or button to resume
|
||||
const banner = page.locator('[data-testid="import-banner"]').or(page.getByText(/pending|resume|continue/i));
|
||||
@@ -444,7 +449,7 @@ test.describe('Caddy Import - Cross-Browser @cross-browser', () => {
|
||||
* TEST 6: Cancel import session across all browsers
|
||||
* Verifies session cancellation clears state correctly
|
||||
*/
|
||||
test('should cancel import session in all browsers', async ({ page, browserName }) => {
|
||||
test('should cancel import session in all browsers', async ({ page, browserName, adminUser }) => {
|
||||
await setupImportMocks(page, {
|
||||
previewHosts: [
|
||||
{ domain_names: 'test.example.com', forward_host: 'localhost', forward_port: 3000, forward_scheme: 'http' },
|
||||
@@ -452,7 +457,7 @@ test.describe('Caddy Import - Cross-Browser @cross-browser', () => {
|
||||
});
|
||||
|
||||
await test.step(`[${browserName}] Navigate to import page`, async () => {
|
||||
await page.goto('/tasks/import/caddyfile');
|
||||
await gotoImportPageWithAuthRecovery(page, adminUser);
|
||||
});
|
||||
|
||||
await test.step(`[${browserName}] Start import session`, async () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { ensureImportFormReady } from './import-page-helpers';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
@@ -89,6 +90,7 @@ test.describe('Caddy Import Debug Tests @caddy-import-debug', () => {
|
||||
// Navigate to import page
|
||||
console.log('[Navigation] Going to /tasks/import/caddyfile');
|
||||
await page.goto('/tasks/import/caddyfile');
|
||||
await ensureImportFormReady(page);
|
||||
|
||||
// Simple valid Caddyfile with single reverse proxy
|
||||
const caddyfile = `
|
||||
@@ -180,6 +182,7 @@ test-simple.example.com {
|
||||
// Auth state loaded from storage - no login needed
|
||||
console.log('[Auth] Using stored authentication state');
|
||||
await page.goto('/tasks/import/caddyfile');
|
||||
await ensureImportFormReady(page);
|
||||
console.log('[Navigation] Navigated to import page');
|
||||
|
||||
const caddyfileWithImports = `
|
||||
@@ -263,6 +266,7 @@ admin.example.com {
|
||||
// Auth state loaded from storage
|
||||
console.log('[Auth] Using stored authentication state');
|
||||
await page.goto('/tasks/import/caddyfile');
|
||||
await ensureImportFormReady(page);
|
||||
console.log('[Navigation] Navigated to import page');
|
||||
|
||||
const fileServerCaddyfile = `
|
||||
@@ -348,6 +352,7 @@ docs.example.com {
|
||||
// Auth state loaded from storage
|
||||
console.log('[Auth] Using stored authentication state');
|
||||
await page.goto('/tasks/import/caddyfile');
|
||||
await ensureImportFormReady(page);
|
||||
console.log('[Navigation] Navigated to import page');
|
||||
|
||||
const mixedCaddyfile = `
|
||||
@@ -456,6 +461,7 @@ redirect.example.com {
|
||||
// Auth state loaded from storage
|
||||
console.log('[Auth] Using stored authentication state');
|
||||
await page.goto('/tasks/import/caddyfile');
|
||||
await ensureImportFormReady(page);
|
||||
console.log('[Navigation] Navigated to import page');
|
||||
|
||||
const invalidCaddyfile = `
|
||||
@@ -549,6 +555,7 @@ broken.example.com {
|
||||
// Auth state loaded from storage
|
||||
console.log('[Auth] Using stored authentication state');
|
||||
await page.goto('/tasks/import/caddyfile');
|
||||
await ensureImportFormReady(page);
|
||||
console.log('[Navigation] Navigated to import page');
|
||||
|
||||
// Main Caddyfile
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
import { test, expect } from '../../fixtures/auth-fixtures';
|
||||
import { Page } from '@playwright/test';
|
||||
import { ensureImportUiPreconditions } from './import-page-helpers';
|
||||
|
||||
function firefoxOnly(browserName: string) {
|
||||
test.skip(browserName !== 'firefox', 'This suite only runs on Firefox');
|
||||
@@ -98,11 +99,11 @@ test.describe('Caddy Import - Firefox-Specific @firefox-only', () => {
|
||||
* TEST 1: Event listener attachment verification
|
||||
* Ensures the Parse button has proper click handlers in Firefox
|
||||
*/
|
||||
test('should have click handler attached to Parse button', async ({ page }) => {
|
||||
test('should have click handler attached to Parse button', async ({ page, adminUser }) => {
|
||||
|
||||
await test.step('Navigate to import page', async () => {
|
||||
await setupImportMocks(page);
|
||||
await page.goto('/tasks/import/caddyfile');
|
||||
await ensureImportUiPreconditions(page, adminUser);
|
||||
});
|
||||
|
||||
await test.step('Verify Parse button exists and is interactive', async () => {
|
||||
@@ -142,10 +143,11 @@ test.describe('Caddy Import - Firefox-Specific @firefox-only', () => {
|
||||
* TEST 2: Async state update race condition
|
||||
* Firefox's event loop may expose race conditions in state updates
|
||||
*/
|
||||
test('should handle rapid click and state updates', async ({ page }) => {
|
||||
test('should handle rapid click and state updates', async ({ page, adminUser }) => {
|
||||
|
||||
await test.step('Navigate to import page', async () => {
|
||||
await page.goto('/tasks/import/caddyfile');
|
||||
await setupImportMocks(page);
|
||||
await ensureImportUiPreconditions(page, adminUser);
|
||||
});
|
||||
|
||||
await test.step('Set up API mock with slight delay', async () => {
|
||||
@@ -191,10 +193,10 @@ test.describe('Caddy Import - Firefox-Specific @firefox-only', () => {
|
||||
* TEST 3: CORS preflight handling
|
||||
* Firefox has stricter CORS enforcement; verify no preflight issues
|
||||
*/
|
||||
test('should handle CORS correctly (same-origin)', async ({ page }) => {
|
||||
test('should handle CORS correctly (same-origin)', async ({ page, adminUser }) => {
|
||||
await test.step('Navigate to import page', async () => {
|
||||
await setupImportMocks(page);
|
||||
await page.goto('/tasks/import/caddyfile');
|
||||
await ensureImportUiPreconditions(page, adminUser);
|
||||
});
|
||||
|
||||
const corsIssues: string[] = [];
|
||||
@@ -231,10 +233,10 @@ test.describe('Caddy Import - Firefox-Specific @firefox-only', () => {
|
||||
* TEST 4: Cookie/auth header verification
|
||||
* Ensures Firefox sends auth cookies correctly with API requests
|
||||
*/
|
||||
test('should send authentication cookies with requests', async ({ page }) => {
|
||||
test('should send authentication cookies with requests', async ({ page, adminUser }) => {
|
||||
await test.step('Navigate to import page', async () => {
|
||||
await setupImportMocks(page);
|
||||
await page.goto('/tasks/import/caddyfile');
|
||||
await ensureImportUiPreconditions(page, adminUser);
|
||||
});
|
||||
|
||||
let requestHeaders: Record<string, string> = {};
|
||||
@@ -273,10 +275,10 @@ test.describe('Caddy Import - Firefox-Specific @firefox-only', () => {
|
||||
* TEST 5: Button double-click protection
|
||||
* Firefox must prevent duplicate API requests from rapid clicks
|
||||
*/
|
||||
test('should prevent duplicate requests on double-click', async ({ page }) => {
|
||||
test('should prevent duplicate requests on double-click', async ({ page, adminUser }) => {
|
||||
await test.step('Navigate to import page', async () => {
|
||||
await setupImportMocks(page);
|
||||
await page.goto('/tasks/import/caddyfile');
|
||||
await ensureImportUiPreconditions(page, adminUser);
|
||||
});
|
||||
|
||||
const requestCount: string[] = [];
|
||||
@@ -317,9 +319,10 @@ test.describe('Caddy Import - Firefox-Specific @firefox-only', () => {
|
||||
* TEST 6: Large file handling
|
||||
* Verifies Firefox handles large Caddyfile uploads without lag or timeout
|
||||
*/
|
||||
test('should handle large Caddyfile upload (10KB+)', async ({ page }) => {
|
||||
test('should handle large Caddyfile upload (10KB+)', async ({ page, adminUser }) => {
|
||||
await test.step('Navigate to import page', async () => {
|
||||
await page.goto('/tasks/import/caddyfile');
|
||||
await setupImportMocks(page);
|
||||
await ensureImportUiPreconditions(page, adminUser);
|
||||
});
|
||||
|
||||
await test.step('Generate large Caddyfile content', async () => {
|
||||
|
||||
@@ -16,9 +16,10 @@
|
||||
* - Row-scoped selectors (filter by domain, then find within row)
|
||||
*/
|
||||
|
||||
import { test, expect } from '../../fixtures/auth-fixtures';
|
||||
import { test, expect, type TestUser } from '../../fixtures/auth-fixtures';
|
||||
import type { TestDataManager } from '../../utils/TestDataManager';
|
||||
import type { Page } from '@playwright/test';
|
||||
import { ensureAuthenticatedImportFormReady, ensureImportFormReady, resetImportSession } from './import-page-helpers';
|
||||
|
||||
/**
|
||||
* Helper: Generate unique domain with namespace isolation
|
||||
@@ -34,10 +35,17 @@ function generateDomain(testData: TestDataManager, suffix: string): string {
|
||||
*/
|
||||
async function completeImportFlow(
|
||||
page: Page,
|
||||
caddyfile: string
|
||||
caddyfile: string,
|
||||
browserName: string,
|
||||
adminUser: TestUser
|
||||
): Promise<void> {
|
||||
await test.step('Navigate to import page', async () => {
|
||||
await page.goto('/tasks/import/caddyfile');
|
||||
if (browserName === 'webkit') {
|
||||
await ensureAuthenticatedImportFormReady(page, adminUser);
|
||||
} else {
|
||||
await ensureImportFormReady(page);
|
||||
}
|
||||
});
|
||||
|
||||
await test.step('Paste Caddyfile content', async () => {
|
||||
@@ -66,15 +74,19 @@ async function completeImportFlow(
|
||||
}
|
||||
|
||||
test.describe('Caddy Import Gap Coverage @caddy-import-gaps', () => {
|
||||
test.afterEach(async ({ page }) => {
|
||||
await resetImportSession(page);
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// Gap 1: Success Modal Navigation
|
||||
// =========================================================================
|
||||
test.describe('Success Modal Navigation', () => {
|
||||
test('1.1: should display success modal after successful import commit', async ({ page, testData }) => {
|
||||
test('1.1: should display success modal after successful import commit', async ({ page, testData, browserName, adminUser }) => {
|
||||
const domain = generateDomain(testData, 'success-modal-test');
|
||||
const caddyfile = `${domain} { reverse_proxy localhost:3000 }`;
|
||||
|
||||
await completeImportFlow(page, caddyfile);
|
||||
await completeImportFlow(page, caddyfile, browserName, adminUser);
|
||||
|
||||
// Verify success modal is visible
|
||||
await expect(page.getByTestId('import-success-modal')).toBeVisible();
|
||||
@@ -87,11 +99,11 @@ test.describe('Caddy Import Gap Coverage @caddy-import-gaps', () => {
|
||||
await expect(modal).toContainText(/1.*created/i);
|
||||
});
|
||||
|
||||
test('1.2: should navigate to /proxy-hosts when clicking View Proxy Hosts button', async ({ page, testData }) => {
|
||||
test('1.2: should navigate to /proxy-hosts when clicking View Proxy Hosts button', async ({ page, testData, browserName, adminUser }) => {
|
||||
const domain = generateDomain(testData, 'view-hosts-nav');
|
||||
const caddyfile = `${domain} { reverse_proxy localhost:3000 }`;
|
||||
|
||||
await completeImportFlow(page, caddyfile);
|
||||
await completeImportFlow(page, caddyfile, browserName, adminUser);
|
||||
|
||||
await test.step('Click View Proxy Hosts button', async () => {
|
||||
const modal = page.getByTestId('import-success-modal');
|
||||
@@ -104,11 +116,11 @@ test.describe('Caddy Import Gap Coverage @caddy-import-gaps', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('1.3: should navigate to /dashboard when clicking Go to Dashboard button', async ({ page, testData }) => {
|
||||
test('1.3: should navigate to /dashboard when clicking Go to Dashboard button', async ({ page, testData, browserName, adminUser }) => {
|
||||
const domain = generateDomain(testData, 'dashboard-nav');
|
||||
const caddyfile = `${domain} { reverse_proxy localhost:3000 }`;
|
||||
|
||||
await completeImportFlow(page, caddyfile);
|
||||
await completeImportFlow(page, caddyfile, browserName, adminUser);
|
||||
|
||||
await test.step('Click Go to Dashboard button', async () => {
|
||||
const modal = page.getByTestId('import-success-modal');
|
||||
@@ -122,11 +134,11 @@ test.describe('Caddy Import Gap Coverage @caddy-import-gaps', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('1.4: should close modal and stay on import page when clicking Close', async ({ page, testData }) => {
|
||||
test('1.4: should close modal and stay on import page when clicking Close', async ({ page, testData, browserName, adminUser }) => {
|
||||
const domain = generateDomain(testData, 'close-modal');
|
||||
const caddyfile = `${domain} { reverse_proxy localhost:3000 }`;
|
||||
|
||||
await completeImportFlow(page, caddyfile);
|
||||
await completeImportFlow(page, caddyfile, browserName, adminUser);
|
||||
|
||||
await test.step('Click Close button', async () => {
|
||||
const modal = page.getByTestId('import-success-modal');
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
import { test, expect } from '../../fixtures/auth-fixtures';
|
||||
import { Page } from '@playwright/test';
|
||||
import { ensureImportUiPreconditions, resetImportSession } from './import-page-helpers';
|
||||
|
||||
function webkitOnly(browserName: string) {
|
||||
test.skip(browserName !== 'webkit', 'This suite only runs on WebKit');
|
||||
@@ -89,17 +90,24 @@ async function setupImportMocks(page: Page, success: boolean = true) {
|
||||
}
|
||||
|
||||
test.describe('Caddy Import - WebKit-Specific @webkit-only', () => {
|
||||
test.beforeEach(async ({ browserName }) => {
|
||||
test.beforeEach(async ({ browserName, page, adminUser }) => {
|
||||
webkitOnly(browserName);
|
||||
await setupImportMocks(page);
|
||||
await resetImportSession(page);
|
||||
await ensureImportUiPreconditions(page, adminUser);
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
await resetImportSession(page);
|
||||
});
|
||||
|
||||
/**
|
||||
* TEST 1: Event listener attachment verification
|
||||
* Safari/WebKit may handle React event delegation differently
|
||||
*/
|
||||
test('should have click handler attached to Parse button', async ({ page }) => {
|
||||
test('should have click handler attached to Parse button', async ({ page, adminUser }) => {
|
||||
await test.step('Navigate to import page', async () => {
|
||||
await page.goto('/tasks/import/caddyfile');
|
||||
await ensureImportUiPreconditions(page, adminUser);
|
||||
});
|
||||
|
||||
await test.step('Verify Parse button is clickable in WebKit', async () => {
|
||||
@@ -120,8 +128,6 @@ test.describe('Caddy Import - WebKit-Specific @webkit-only', () => {
|
||||
});
|
||||
|
||||
await test.step('Verify click sends API request', async () => {
|
||||
await setupImportMocks(page);
|
||||
|
||||
const requestPromise = page.waitForRequest((req) => req.url().includes('/api/v1/import/upload'));
|
||||
|
||||
const parseButton = page.getByRole('button', { name: /parse|review/i });
|
||||
@@ -137,9 +143,9 @@ test.describe('Caddy Import - WebKit-Specific @webkit-only', () => {
|
||||
* TEST 2: Async state update race condition
|
||||
* WebKit's JavaScript engine (JavaScriptCore) may have different timing
|
||||
*/
|
||||
test('should handle async state updates correctly', async ({ page }) => {
|
||||
test('should handle async state updates correctly', async ({ page, adminUser }) => {
|
||||
await test.step('Navigate to import page', async () => {
|
||||
await page.goto('/tasks/import/caddyfile');
|
||||
await ensureImportUiPreconditions(page, adminUser);
|
||||
});
|
||||
|
||||
await test.step('Set up API mock with delay', async () => {
|
||||
@@ -183,10 +189,9 @@ test.describe('Caddy Import - WebKit-Specific @webkit-only', () => {
|
||||
* TEST 3: Form submission behavior
|
||||
* Safari may treat button clicks inside forms differently
|
||||
*/
|
||||
test('should handle button click without form submission', async ({ page }) => {
|
||||
test('should handle button click without form submission', async ({ page, adminUser }) => {
|
||||
await test.step('Navigate to import page', async () => {
|
||||
await setupImportMocks(page);
|
||||
await page.goto('/tasks/import/caddyfile');
|
||||
await ensureImportUiPreconditions(page, adminUser);
|
||||
});
|
||||
|
||||
const navigationOccurred: string[] = [];
|
||||
@@ -222,10 +227,9 @@ test.describe('Caddy Import - WebKit-Specific @webkit-only', () => {
|
||||
* TEST 4: Cookie/session storage handling
|
||||
* WebKit's cookie/storage behavior may differ from Chromium
|
||||
*/
|
||||
test('should maintain session state and send cookies', async ({ page }) => {
|
||||
test('should maintain session state and send cookies', async ({ page, adminUser }) => {
|
||||
await test.step('Navigate to import page', async () => {
|
||||
await setupImportMocks(page);
|
||||
await page.goto('/tasks/import/caddyfile');
|
||||
await ensureImportUiPreconditions(page, adminUser);
|
||||
});
|
||||
|
||||
let requestHeaders: Record<string, string> = {};
|
||||
@@ -260,10 +264,9 @@ test.describe('Caddy Import - WebKit-Specific @webkit-only', () => {
|
||||
* TEST 5: Button interaction after rapid state changes
|
||||
* Safari may handle rapid state updates differently
|
||||
*/
|
||||
test('should handle button state changes correctly', async ({ page }) => {
|
||||
test('should handle button state changes correctly', async ({ page, adminUser }) => {
|
||||
await test.step('Navigate to import page', async () => {
|
||||
await setupImportMocks(page);
|
||||
await page.goto('/tasks/import/caddyfile');
|
||||
await ensureImportUiPreconditions(page, adminUser);
|
||||
});
|
||||
|
||||
await test.step('Rapidly fill content and check button state', async () => {
|
||||
@@ -303,9 +306,9 @@ test.describe('Caddy Import - WebKit-Specific @webkit-only', () => {
|
||||
* TEST 6: Large file handling
|
||||
* WebKit memory management may differ from Chromium/Firefox
|
||||
*/
|
||||
test('should handle large Caddyfile upload without memory issues', async ({ page }) => {
|
||||
test('should handle large Caddyfile upload without memory issues', async ({ page, adminUser }) => {
|
||||
await test.step('Navigate to import page', async () => {
|
||||
await page.goto('/tasks/import/caddyfile');
|
||||
await ensureImportUiPreconditions(page, adminUser);
|
||||
});
|
||||
|
||||
await test.step('Generate and paste large Caddyfile', async () => {
|
||||
|
||||
144
tests/core/caddy-import/import-page-helpers.ts
Normal file
144
tests/core/caddy-import/import-page-helpers.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { expect, test, type Page } from '@playwright/test';
|
||||
import { loginUser, type TestUser } from '../../fixtures/auth-fixtures';
|
||||
|
||||
const IMPORT_PAGE_PATH = '/tasks/import/caddyfile';
|
||||
|
||||
export async function resetImportSession(page: Page): Promise<void> {
|
||||
try {
|
||||
if (!page.url().includes(IMPORT_PAGE_PATH)) {
|
||||
await page.goto(IMPORT_PAGE_PATH, { waitUntil: 'domcontentloaded' });
|
||||
}
|
||||
} catch {
|
||||
// Best-effort navigation only
|
||||
}
|
||||
|
||||
try {
|
||||
const statusResponse = await page.request.get('/api/v1/import/status');
|
||||
if (statusResponse.ok()) {
|
||||
const statusBody = await statusResponse.json();
|
||||
if (statusBody?.has_pending) {
|
||||
await page.request.post('/api/v1/import/cancel');
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Best-effort cleanup only
|
||||
}
|
||||
|
||||
try {
|
||||
await page.goto(IMPORT_PAGE_PATH, { waitUntil: 'domcontentloaded' });
|
||||
} catch {
|
||||
// Best-effort return to import page only
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureImportFormReady(page: Page): Promise<void> {
|
||||
const currentUrl = page.url();
|
||||
const currentPath = await page.evaluate(() => window.location.pathname).catch(() => '');
|
||||
if (currentUrl.includes('/login') || currentPath.includes('/login')) {
|
||||
throw new Error(
|
||||
`Auth state lost: import form is unavailable because the page is on login (url=${currentUrl}, path=${currentPath})`
|
||||
);
|
||||
}
|
||||
|
||||
const headingByRole = page.getByRole('heading', { name: /import|caddyfile/i }).first();
|
||||
const headingLike = page
|
||||
.locator('h1, h2, [data-testid="page-title"], [aria-label*="import" i], [aria-label*="caddyfile" i]')
|
||||
.first();
|
||||
|
||||
if (await headingByRole.count()) {
|
||||
await expect(headingByRole).toBeVisible();
|
||||
} else if (await headingLike.count()) {
|
||||
await expect(headingLike).toBeVisible();
|
||||
} else {
|
||||
await expect(page.locator('main, body').first()).toContainText(/import|caddyfile/i);
|
||||
}
|
||||
|
||||
await expect(page.locator('textarea')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: /parse|review/i }).first()).toBeVisible();
|
||||
}
|
||||
|
||||
async function hasLoginUiMarkers(page: Page): Promise<boolean> {
|
||||
const currentUrl = page.url();
|
||||
const currentPath = await page.evaluate(() => window.location.pathname).catch(() => '');
|
||||
if (currentUrl.includes('/login') || currentPath.includes('/login')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const signInHeading = page.getByRole('heading', { name: /sign in|login/i }).first();
|
||||
const signInButton = page.getByRole('button', { name: /sign in|login/i }).first();
|
||||
const emailTextbox = page.getByRole('textbox', { name: /email/i }).first();
|
||||
|
||||
const [headingVisible, buttonVisible, emailVisible] = await Promise.all([
|
||||
signInHeading.isVisible().catch(() => false),
|
||||
signInButton.isVisible().catch(() => false),
|
||||
emailTextbox.isVisible().catch(() => false),
|
||||
]);
|
||||
|
||||
return headingVisible || buttonVisible || emailVisible;
|
||||
}
|
||||
|
||||
export async function ensureAuthenticatedImportFormReady(page: Page, adminUser?: TestUser): Promise<void> {
|
||||
const recoverIfNeeded = async (): Promise<boolean> => {
|
||||
const loginDetected = await test.step('Auth precheck: detect login redirect or sign-in controls', async () => {
|
||||
return hasLoginUiMarkers(page);
|
||||
});
|
||||
if (!loginDetected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!adminUser) {
|
||||
throw new Error('Import auth recovery failed: login UI detected but no admin user fixture was provided.');
|
||||
}
|
||||
|
||||
return test.step('Auth recovery: perform one deterministic login and return to import page', async () => {
|
||||
try {
|
||||
await loginUser(page, adminUser);
|
||||
await page.goto(IMPORT_PAGE_PATH, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
if (await hasLoginUiMarkers(page) && adminUser.token) {
|
||||
await test.step('Auth recovery fallback: restore fixture token and reload import page', async () => {
|
||||
await page.goto('/', { waitUntil: 'domcontentloaded' });
|
||||
await page.evaluate((token: string) => {
|
||||
localStorage.setItem('charon_auth_token', token);
|
||||
}, adminUser.token);
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await page.waitForLoadState('networkidle').catch(() => {});
|
||||
await page.goto(IMPORT_PAGE_PATH, { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
}
|
||||
|
||||
await ensureImportFormReady(page);
|
||||
return true;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`Import auth recovery failed after one re-auth attempt: ${message}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (await recoverIfNeeded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await ensureImportFormReady(page);
|
||||
} catch (error) {
|
||||
if (await recoverIfNeeded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureImportUiPreconditions(page: Page, adminUser?: TestUser): Promise<void> {
|
||||
await test.step('Precondition: open Caddy import page', async () => {
|
||||
await page.goto(IMPORT_PAGE_PATH, { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
|
||||
await ensureAuthenticatedImportFormReady(page, adminUser);
|
||||
|
||||
await test.step('Precondition: verify import textarea is visible', async () => {
|
||||
await expect(page.locator('textarea')).toBeVisible();
|
||||
});
|
||||
}
|
||||
22
tests/fixtures/auth-fixtures.ts
vendored
22
tests/fixtures/auth-fixtures.ts
vendored
@@ -429,6 +429,12 @@ export async function loginUser(
|
||||
page: import('@playwright/test').Page,
|
||||
user: TestUser
|
||||
): Promise<void> {
|
||||
const hasVisibleSignInControls = async (): Promise<boolean> => {
|
||||
const signInButtonVisible = await page.getByRole('button', { name: /sign in|login/i }).first().isVisible().catch(() => false);
|
||||
const emailInputVisible = await page.getByRole('textbox', { name: /email/i }).first().isVisible().catch(() => false);
|
||||
return signInButtonVisible || emailInputVisible;
|
||||
};
|
||||
|
||||
const loginPayload = { email: user.email, password: TEST_PASSWORD };
|
||||
let apiLoginError: Error | null = null;
|
||||
try {
|
||||
@@ -467,11 +473,19 @@ export async function loginUser(
|
||||
}
|
||||
} catch (error) {
|
||||
apiLoginError = error instanceof Error ? error : new Error(String(error));
|
||||
console.warn(`API login bootstrap failed for ${user.email}: ${apiLoginError.message}`);
|
||||
console.error(`API login bootstrap failed for ${user.email}: ${apiLoginError.message}`);
|
||||
}
|
||||
|
||||
await page.goto('/');
|
||||
if (!page.url().includes('/login')) {
|
||||
const loginRouteDetected = page.url().includes('/login');
|
||||
const loginUiDetected = await hasVisibleSignInControls();
|
||||
let authSessionConfirmed = false;
|
||||
if (!loginRouteDetected && !loginUiDetected) {
|
||||
const authProbeResponse = await page.request.get('/api/v1/auth/me').catch(() => null);
|
||||
authSessionConfirmed = authProbeResponse?.ok() ?? false;
|
||||
}
|
||||
|
||||
if (!loginRouteDetected && !loginUiDetected && authSessionConfirmed) {
|
||||
if (apiLoginError) {
|
||||
console.warn(`Continuing with existing authenticated session after API login bootstrap failure for ${user.email}`);
|
||||
}
|
||||
@@ -479,7 +493,9 @@ export async function loginUser(
|
||||
return;
|
||||
}
|
||||
|
||||
await page.goto('/login');
|
||||
if (!loginRouteDetected) {
|
||||
await page.goto('/login');
|
||||
}
|
||||
await page.locator('input[type="email"]').fill(user.email);
|
||||
await page.locator('input[type="password"]').fill(TEST_PASSWORD);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user