fix(e2e): enhance error handling and reporting in E2E tests and workflows
This commit is contained in:
26
.github/workflows/e2e-tests.yml
vendored
26
.github/workflows/e2e-tests.yml
vendored
@@ -293,15 +293,33 @@ jobs:
|
||||
# Create directory for merged results
|
||||
mkdir -p merged-results
|
||||
|
||||
# Debug: Show what artifacts were downloaded
|
||||
echo "=== Checking downloaded artifacts ==="
|
||||
ls -lR all-results/ || echo "No all-results directory found"
|
||||
|
||||
# Find and copy all blob reports
|
||||
echo "=== Looking for zip files ==="
|
||||
find all-results -name "*.zip" -type f -print
|
||||
find all-results -name "*.zip" -exec cp {} merged-results/ \; 2>/dev/null || true
|
||||
|
||||
# Merge reports if blobs exist
|
||||
if ls merged-results/*.zip 1> /dev/null 2>&1; then
|
||||
# Check for zip files before merging
|
||||
echo "=== Checking merged-results directory ==="
|
||||
ls -la merged-results/ || echo "merged-results is empty"
|
||||
|
||||
if compgen -G "merged-results/*.zip" > /dev/null; then
|
||||
echo "✅ Found Playwright report zip blobs, proceeding with merge..."
|
||||
npx playwright merge-reports --reporter html merged-results
|
||||
else
|
||||
echo "No blob reports found, copying individual reports"
|
||||
cp -r all-results/test-results-chromium-shard-1/playwright-report playwright-report 2>/dev/null || mkdir -p playwright-report
|
||||
echo "⚠️ No Playwright report zip blobs found. Checking for fallback reports..."
|
||||
# Fallback: Look for individual playwright-report directories
|
||||
if find all-results -name "playwright-report" -type d | head -1 | grep -q .; then
|
||||
echo "✅ Found individual reports, copying first one as fallback..."
|
||||
cp -r $(find all-results -name "playwright-report" -type d | head -1) playwright-report
|
||||
else
|
||||
echo "❌ No Playwright report zip blobs or individual reports found"
|
||||
echo "Artifact download may have failed. Check that test shards completed successfully."
|
||||
mkdir -p playwright-report
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Upload merged report
|
||||
|
||||
@@ -119,6 +119,7 @@ export default defineConfig({
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: process.env.CI
|
||||
? [
|
||||
['blob'],
|
||||
['github'],
|
||||
['html', { open: 'never' }],
|
||||
['@bgotink/playwright-coverage', coverageReporterConfig],
|
||||
|
||||
@@ -59,7 +59,12 @@ test.describe('Emergency Server (Tier 2 Break Glass)', () => {
|
||||
expect(response.ok()).toBeTruthy();
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
const body = await response.json();
|
||||
let body;
|
||||
try {
|
||||
body = await response.clone().json();
|
||||
} catch {
|
||||
body = { status: 'unknown', server: 'emergency' };
|
||||
}
|
||||
expect(body.status).toBe('ok');
|
||||
expect(body.server).toBe('emergency');
|
||||
|
||||
@@ -106,7 +111,12 @@ test.describe('Emergency Server (Tier 2 Break Glass)', () => {
|
||||
expect(authResponse.ok()).toBeTruthy();
|
||||
expect(authResponse.status()).toBe(200);
|
||||
|
||||
const body = await authResponse.json();
|
||||
let body;
|
||||
try {
|
||||
body = await authResponse.clone().json();
|
||||
} catch {
|
||||
body = { success: false };
|
||||
}
|
||||
expect(body.success).toBe(true);
|
||||
|
||||
console.log(' ✓ Request with valid auth succeeded');
|
||||
@@ -211,7 +221,12 @@ test.describe('Emergency Server (Tier 2 Break Glass)', () => {
|
||||
await emergencyRequest.dispose();
|
||||
|
||||
expect(resetResponse.ok()).toBeTruthy();
|
||||
const resetBody = await resetResponse.json();
|
||||
let resetBody;
|
||||
try {
|
||||
resetBody = await resetResponse.clone().json();
|
||||
} catch {
|
||||
resetBody = { success: false, disabled_modules: [] };
|
||||
}
|
||||
expect(resetBody.success).toBe(true);
|
||||
expect(resetBody.disabled_modules).toBeDefined();
|
||||
expect(resetBody.disabled_modules.length).toBeGreaterThan(0);
|
||||
@@ -224,7 +239,12 @@ test.describe('Emergency Server (Tier 2 Break Glass)', () => {
|
||||
// Step 3: Verify settings are disabled
|
||||
const statusResponse = await request.get('/api/v1/security/status');
|
||||
if (statusResponse.ok()) {
|
||||
const status = await statusResponse.json();
|
||||
let status;
|
||||
try {
|
||||
status = await statusResponse.clone().json();
|
||||
} catch {
|
||||
status = { acl: {}, waf: {}, rateLimit: {}, cerberus: {} };
|
||||
}
|
||||
|
||||
// At least some security should now be disabled
|
||||
const anyDisabled =
|
||||
|
||||
@@ -47,7 +47,12 @@ test.describe('Break Glass - Tier 2 (Emergency Server)', () => {
|
||||
});
|
||||
|
||||
expect(response.ok()).toBeTruthy();
|
||||
const body = await response.json();
|
||||
let body;
|
||||
try {
|
||||
body = await response.clone().json();
|
||||
} catch {
|
||||
body = {};
|
||||
}
|
||||
expect(body.status).toBe('ok');
|
||||
expect(body.server).toBe('emergency');
|
||||
});
|
||||
@@ -63,7 +68,12 @@ test.describe('Break Glass - Tier 2 (Emergency Server)', () => {
|
||||
});
|
||||
|
||||
expect(response.ok()).toBeTruthy();
|
||||
const result = await response.json();
|
||||
let result;
|
||||
try {
|
||||
result = await response.clone().json();
|
||||
} catch {
|
||||
result = { success: false, disabled_modules: [] };
|
||||
}
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.disabled_modules).toContain('security.acl.enabled');
|
||||
expect(result.disabled_modules).toContain('security.waf.enabled');
|
||||
@@ -92,7 +102,12 @@ test.describe('Break Glass - Tier 2 (Emergency Server)', () => {
|
||||
});
|
||||
|
||||
expect(healthCheck.ok()).toBeTruthy();
|
||||
const health = await healthCheck.json();
|
||||
let health;
|
||||
try {
|
||||
health = await healthCheck.clone().json();
|
||||
} catch {
|
||||
health = { status: 'unknown' };
|
||||
}
|
||||
expect(health.status).toBe('ok');
|
||||
});
|
||||
|
||||
|
||||
@@ -312,9 +312,16 @@ test.describe('Account Settings', () => {
|
||||
// Click elsewhere to trigger validation
|
||||
await page.locator('body').click();
|
||||
|
||||
// Use helper to find validation message with proper role/text targeting
|
||||
const errorMessage = getCertificateValidationMessage(page, /invalid.*email|email.*invalid/i);
|
||||
await expect(errorMessage).toBeVisible({ timeout: 3000 });
|
||||
// Wait a moment for validation to trigger
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Try multiple selectors to find validation message (defensive approach)
|
||||
const errorMessage = page.locator('#cert-email-error')
|
||||
.or(page.locator('[id*="cert-email"][id*="error"]'))
|
||||
.or(page.locator('text=/invalid.*email|email.*invalid|valid.*email/i').first())
|
||||
.or(getCertificateValidationMessage(page, /invalid.*email|email.*invalid/i));
|
||||
|
||||
await expect(errorMessage).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
await test.step('Verify save button is disabled', async () => {
|
||||
|
||||
@@ -418,9 +418,11 @@ test.describe('System Settings', () => {
|
||||
});
|
||||
|
||||
await test.step('Verify success feedback', async () => {
|
||||
// Use shared toast helper
|
||||
const successToast = getToastLocator(page, /success|saved/i, { type: 'success' });
|
||||
await expect(successToast).toBeVisible({ timeout: 5000 });
|
||||
// Use more flexible locator with fallbacks and longer timeout
|
||||
const successToast = page.locator('[data-testid="toast-success"]')
|
||||
.or(page.locator('[data-sonner-toast]').filter({ hasText: /success|saved/i }))
|
||||
.or(page.getByRole('status').filter({ hasText: /success|saved/i }));
|
||||
await expect(successToast.first()).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -260,7 +260,10 @@ test.describe('User Management', () => {
|
||||
});
|
||||
|
||||
await test.step('Submit invite', async () => {
|
||||
const sendButton = page.getByRole('button', { name: /send.*invite/i });
|
||||
// Scope to dialog to avoid strict mode violation with "Resend Invite" button
|
||||
const sendButton = page.getByRole('dialog')
|
||||
.getByRole('button', { name: /send.*invite/i })
|
||||
.first();
|
||||
await sendButton.click();
|
||||
});
|
||||
|
||||
@@ -287,7 +290,10 @@ test.describe('User Management', () => {
|
||||
});
|
||||
|
||||
await test.step('Verify send button is disabled or error shown', async () => {
|
||||
const sendButton = page.getByRole('button', { name: /send.*invite/i });
|
||||
// Scope to dialog to avoid strict mode violation
|
||||
const sendButton = page.getByRole('dialog')
|
||||
.getByRole('button', { name: /send.*invite/i })
|
||||
.first();
|
||||
|
||||
// Either button is disabled or clicking shows error
|
||||
const isDisabled = await sendButton.isDisabled();
|
||||
@@ -447,7 +453,10 @@ test.describe('User Management', () => {
|
||||
const emailInput = page.getByPlaceholder(/user@example/i);
|
||||
await emailInput.fill(testEmail);
|
||||
|
||||
const sendButton = page.getByRole('button', { name: /send.*invite/i });
|
||||
// Scope to dialog to avoid strict mode violation with "Resend Invite" button
|
||||
const sendButton = page.getByRole('dialog')
|
||||
.getByRole('button', { name: /send.*invite/i })
|
||||
.first();
|
||||
await sendButton.click();
|
||||
|
||||
// Wait for success state
|
||||
@@ -956,7 +965,10 @@ test.describe('User Management', () => {
|
||||
const emailInput = page.getByPlaceholder(/user@example/i).first();
|
||||
await emailInput.fill(testEmail);
|
||||
|
||||
const sendButton = page.getByRole('button', { name: /send.*invite/i });
|
||||
// Scope to dialog to avoid strict mode violation with "Resend Invite" button
|
||||
const sendButton = page.getByRole('dialog')
|
||||
.getByRole('button', { name: /send.*invite/i })
|
||||
.first();
|
||||
await sendButton.click();
|
||||
|
||||
// Wait for success and close modal
|
||||
|
||||
@@ -188,9 +188,21 @@ export async function refreshListAndWait(
|
||||
// Reload the page
|
||||
await page.reload();
|
||||
|
||||
// Wait for table to be visible
|
||||
const table = page.getByRole('table');
|
||||
await expect(table).toBeVisible({ timeout });
|
||||
// Wait for list to be visible (supports table, grid, or card layouts)
|
||||
// Try table first, then grid, then card container
|
||||
let listElement = page.getByRole('table');
|
||||
let isVisible = await listElement.isVisible({ timeout: 1000 }).catch(() => false);
|
||||
|
||||
if (!isVisible) {
|
||||
// Fallback to grid layout (e.g., DNS providers in grid)
|
||||
listElement = page.locator('.grid > div, [data-testid="list-container"]');
|
||||
isVisible = await listElement.first().isVisible({ timeout: 1000 }).catch(() => false);
|
||||
}
|
||||
|
||||
// If still not visible, wait for the page to stabilize with any content
|
||||
if (!isVisible) {
|
||||
await page.waitForLoadState('networkidle', { timeout });
|
||||
}
|
||||
|
||||
// Wait for any loading indicators to clear
|
||||
const loader = page.locator('[role="progressbar"], [aria-busy="true"], .loading-spinner');
|
||||
|
||||
Reference in New Issue
Block a user