fix: enhance import tests with user authentication handling and precondition checks

This commit is contained in:
GitHub Actions
2026-02-26 20:32:31 +00:00
parent 4081003051
commit 68e3bee684
7 changed files with 247 additions and 57 deletions

View File

@@ -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 () => {

View File

@@ -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

View File

@@ -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 () => {

View File

@@ -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');

View File

@@ -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 () => {

View 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();
});
}

View File

@@ -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);