diff --git a/tests/core/caddy-import/caddy-import-cross-browser.spec.ts b/tests/core/caddy-import/caddy-import-cross-browser.spec.ts index 5fde8fa6..fbd631b7 100644 --- a/tests/core/caddy-import/caddy-import-cross-browser.spec.ts +++ b/tests/core/caddy-import/caddy-import-cross-browser.spec.ts @@ -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 { + 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 () => { diff --git a/tests/core/caddy-import/caddy-import-debug.spec.ts b/tests/core/caddy-import/caddy-import-debug.spec.ts index 43488ea9..e5e5aec3 100644 --- a/tests/core/caddy-import/caddy-import-debug.spec.ts +++ b/tests/core/caddy-import/caddy-import-debug.spec.ts @@ -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 diff --git a/tests/core/caddy-import/caddy-import-firefox.spec.ts b/tests/core/caddy-import/caddy-import-firefox.spec.ts index 7046a3fa..47ab81a2 100644 --- a/tests/core/caddy-import/caddy-import-firefox.spec.ts +++ b/tests/core/caddy-import/caddy-import-firefox.spec.ts @@ -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 = {}; @@ -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 () => { diff --git a/tests/core/caddy-import/caddy-import-gaps.spec.ts b/tests/core/caddy-import/caddy-import-gaps.spec.ts index 4987d489..bb02edb9 100644 --- a/tests/core/caddy-import/caddy-import-gaps.spec.ts +++ b/tests/core/caddy-import/caddy-import-gaps.spec.ts @@ -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 { 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'); diff --git a/tests/core/caddy-import/caddy-import-webkit.spec.ts b/tests/core/caddy-import/caddy-import-webkit.spec.ts index 69c326fe..731c2d36 100644 --- a/tests/core/caddy-import/caddy-import-webkit.spec.ts +++ b/tests/core/caddy-import/caddy-import-webkit.spec.ts @@ -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 = {}; @@ -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 () => { diff --git a/tests/core/caddy-import/import-page-helpers.ts b/tests/core/caddy-import/import-page-helpers.ts new file mode 100644 index 00000000..e24be335 --- /dev/null +++ b/tests/core/caddy-import/import-page-helpers.ts @@ -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 { + 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 { + 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 { + 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 { + const recoverIfNeeded = async (): Promise => { + 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 { + 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(); + }); +} diff --git a/tests/fixtures/auth-fixtures.ts b/tests/fixtures/auth-fixtures.ts index 2ba44be9..cf697a28 100644 --- a/tests/fixtures/auth-fixtures.ts +++ b/tests/fixtures/auth-fixtures.ts @@ -429,6 +429,12 @@ export async function loginUser( page: import('@playwright/test').Page, user: TestUser ): Promise { + const hasVisibleSignInControls = async (): Promise => { + 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);