/** * SSL Certificates E2E Tests * * Tests the SSL Certificates management functionality including: * - List view with table, columns, and empty states * - Upload custom certificate with form validation * - Certificate details (domain, expiry, issuer, status) * - Delete certificate with confirmation and backup * - Certificate status indicators and sorting * * @see /projects/Charon/docs/plans/current_spec.md */ import { test, expect, loginUser, TEST_PASSWORD } from '../fixtures/auth-fixtures'; import { waitForLoadingComplete, waitForToast, waitForModal, waitForDialog, waitForFormFields, waitForDebounce, waitForConfigReload, waitForNavigation, } from '../utils/wait-helpers'; import { letsEncryptCertificate, customCertificateMock, expiredCertificate, expiringCertificate, invalidCertificates, generateCertificate, type CertificateConfig, } from '../fixtures/certificates'; import { generateUniqueId } from '../fixtures/test-data'; test.describe('SSL Certificates - CRUD Operations', () => { test.beforeEach(async ({ page, adminUser }) => { await loginUser(page, adminUser); await waitForLoadingComplete(page); // Navigate to certificates page (retry once on transient failures) for (let i = 0; i < 2; i++) { try { await page.goto('/certificates'); await waitForLoadingComplete(page); break; } catch (err) { if (i === 1) throw err; // short backoff and retry await new Promise((r) => setTimeout(r, 500)); } } }); // Helper to get the Add Certificate button const getAddCertButton = (page: import('@playwright/test').Page) => page.getByRole('button', { name: /add.*certificate/i }).first(); // Helper to get Upload button in form const getUploadButton = (page: import('@playwright/test').Page) => page.getByRole('button', { name: /upload/i }).first(); // Helper to get Cancel button in form const getCancelButton = (page: import('@playwright/test').Page) => page.getByRole('button', { name: /cancel/i }).first(); test.describe('List View', () => { test('should display certificates page with title', async ({ page }) => { await test.step('Verify page title is visible', async () => { const heading = page.getByRole('heading', { name: /certificates/i }); await expect(heading).toBeVisible(); }); await test.step('Verify Add Certificate button is present', async () => { const addButton = getAddCertButton(page); await expect(addButton).toBeVisible(); }); }); test('should show correct table columns', async ({ page }) => { await test.step('Verify table headers exist', async () => { // The table should have columns: Name, Domain, Issuer, Expires, Status, Actions const expectedColumns = [ /name/i, /domain/i, /issuer/i, /expires/i, /status/i, /actions/i, ]; for (const pattern of expectedColumns) { const header = page.locator('th').filter({ hasText: pattern }); const headerExists = await header.count() > 0; if (headerExists) { await expect(header.first()).toBeVisible(); } } }); }); test('should display empty state when no certificates exist', async ({ page }) => { await test.step('Check for empty state or existing certificates', async () => { // Wait for page to fully load await waitForLoadingComplete(page); const table = page.getByRole('table'); const emptyState = page.getByText(/no.*certificates.*found/i); await expect(async () => { const hasTable = await table.count() > 0 && await table.first().isVisible(); const hasEmpty = await emptyState.count() > 0 && await emptyState.first().isVisible(); expect(hasTable || hasEmpty).toBeTruthy(); }).toPass({ timeout: 10000 }); }); }); test('should show loading spinner while fetching data', async ({ page }) => { await test.step('Navigate and observe loading state', async () => { await page.reload(); // Wait for page to fully load after reload await waitForLoadingComplete(page); const table = page.getByRole('table'); const emptyState = page.getByText(/no.*certificates.*found/i); await expect(async () => { const hasTable = await table.count() > 0 && await table.first().isVisible(); const hasEmpty = await emptyState.count() > 0 && await emptyState.first().isVisible(); expect(hasTable || hasEmpty).toBeTruthy(); }).toPass({ timeout: 10000 }); }); }); test('should navigate to certificates from sidebar', async ({ page }) => { await test.step('Navigate via sidebar', async () => { // Go to a different page first await page.goto('/'); await waitForLoadingComplete(page); // Look for SSL/Certificates menu in sidebar const sslMenu = page.getByRole('button', { name: /ssl/i }); const certificatesLink = page.getByRole('link', { name: /certificates/i }); if (await sslMenu.isVisible().catch(() => false)) { await sslMenu.click(); await waitForDebounce(page); // Wait for menu expansion animation } if (await certificatesLink.isVisible().catch(() => false)) { await certificatesLink.click(); await waitForLoadingComplete(page); // Verify we're on certificates page const heading = page.getByRole('heading', { name: /certificates/i }); await expect(heading).toBeVisible({ timeout: 5000 }); } }); }); test('should display certificate details (name, domain, issuer, expiry)', async ({ page }) => { await test.step('Check table displays certificate information', async () => { const table = page.getByRole('table'); const hasTable = await table.isVisible().catch(() => false); if (hasTable) { const rows = page.locator('tbody tr'); const rowCount = await rows.count(); if (rowCount > 0) { const firstRow = rows.first(); await expect(firstRow).toBeVisible(); // Check row has expected content patterns const rowText = await firstRow.textContent(); expect(rowText).toBeTruthy(); } } }); }); test('should show certificate status indicators', async ({ page }) => { await test.step('Check for status badges in table', async () => { // Status badges: Valid, Expiring Soon, Expired, Untrusted (Staging) const statusBadges = page.locator('span').filter({ hasText: /valid|expiring|expired|untrusted/i }); const badgeCount = await statusBadges.count(); // May or may not have certificates depending on test data expect(badgeCount >= 0).toBeTruthy(); }); }); test('should show staging badge for Let\'s Encrypt staging certificates', { retries: 1 }, async ({ page }) => { await test.step('Check for staging badges', async () => { const stagingBadge = page.locator('span').filter({ hasText: /staging/i }); const badgeCount = await stagingBadge.count(); // Verify styling if staging badge exists if (badgeCount > 0) { const firstBadge = stagingBadge.first(); await expect(firstBadge).toBeVisible(); } }); }); test('should support sorting by name', async ({ page }) => { await test.step('Click name column header to sort', async () => { const nameHeader = page.locator('th').filter({ hasText: /name/i }).first(); if (await nameHeader.isVisible().catch(() => false)) { // Check if header is clickable (has cursor-pointer class) const isClickable = await nameHeader.evaluate((el) => el.classList.contains('cursor-pointer') || window.getComputedStyle(el).cursor === 'pointer' ); if (isClickable) { await nameHeader.click(); await waitForDebounce(page); // Wait for sort animation // Sort icon should appear const sortIcon = nameHeader.locator('svg'); const hasSortIcon = await sortIcon.count() > 0; expect(hasSortIcon || true).toBeTruthy(); } } }); }); test('should support sorting by expiry date', async ({ page }) => { await test.step('Click expires column header to sort', async () => { const expiresHeader = page.locator('th').filter({ hasText: /expires/i }).first(); if (await expiresHeader.isVisible().catch(() => false)) { await expiresHeader.click(); await waitForDebounce(page); // Wait for sort animation // Verify click toggles sort direction await expiresHeader.click(); await waitForDebounce(page); // Wait for sort animation } }); }); test('should show SSL info alert', async ({ page }) => { await test.step('Verify SSL info alert is displayed', async () => { const alert = page.locator('[role="alert"], .alert').filter({ hasText: /note|ssl|certificate/i }); const hasAlert = await alert.isVisible().catch(() => false); expect(hasAlert || true).toBeTruthy(); }); }); }); test.describe('Upload Custom Certificate', () => { test('should open upload modal when Add Certificate clicked', async ({ page }) => { await test.step('Click Add Certificate button', async () => { const addButton = getAddCertButton(page); await addButton.click(); }); await test.step('Verify upload dialog opens', async () => { // Wait for dialog to be fully interactive const dialog = await waitForDialog(page); // The dialog should be visible await expect(dialog).toBeVisible({ timeout: 5000 }); // Verify dialog title const dialogTitle = dialog.getByRole('heading', { name: /upload.*certificate/i }); await expect(dialogTitle).toBeVisible(); // Verify essential form fields are present const nameInput = dialog.locator('input').first(); await expect(nameInput).toBeVisible(); // Close dialog (guard visibility/enabled to avoid transient flakiness) await expect(getCancelButton(page)).toBeVisible({ timeout: 3000 }); await expect(getCancelButton(page)).toBeEnabled({ timeout: 3000 }); await getCancelButton(page).click(); }); }); test('should have friendly name input field', async ({ page }) => { await test.step('Open upload dialog', async () => { await getAddCertButton(page).click(); await waitForDialog(page); // Wait for dialog to be fully interactive }); await test.step('Verify name input exists', async () => { const dialog = page.getByRole('dialog'); const nameInput = dialog.locator('input').first(); await expect(nameInput).toBeVisible(); // Check for label const nameLabel = dialog.getByText(/friendly.*name|name/i); await expect(nameLabel).toBeVisible(); }); await test.step('Close dialog', async () => { await getCancelButton(page).click(); }); }); test('should have certificate file input (.pem, .crt, .cer)', async ({ page }) => { await test.step('Open upload dialog', async () => { await getAddCertButton(page).click(); await waitForDialog(page); // Wait for dialog to be fully interactive }); await test.step('Verify certificate file input exists', async () => { const dialog = page.getByRole('dialog'); const certFileInput = dialog.locator('#cert-file'); await expect(certFileInput).toBeVisible(); // Check accept attribute const acceptAttr = await certFileInput.getAttribute('accept'); expect(acceptAttr).toContain('.pem'); }); await test.step('Close dialog', async () => { await expect(getCancelButton(page)).toBeVisible({ timeout: 3000 }); await expect(getCancelButton(page)).toBeEnabled({ timeout: 3000 }); await getCancelButton(page).click(); }); }); test('should have private key file input (.pem, .key)', async ({ page }) => { await test.step('Open upload dialog', async () => { await getAddCertButton(page).click(); await waitForDialog(page); // Wait for dialog to be fully interactive }); await test.step('Verify private key file input exists', async () => { const dialog = page.getByRole('dialog'); const keyFileInput = dialog.locator('#key-file'); await expect(keyFileInput).toBeVisible(); // Check accept attribute const acceptAttr = await keyFileInput.getAttribute('accept'); expect(acceptAttr).toContain('.pem'); }); await test.step('Close dialog', async () => { await expect(getCancelButton(page)).toBeVisible({ timeout: 3000 }); await expect(getCancelButton(page)).toBeEnabled({ timeout: 3000 }); await getCancelButton(page).click(); }); }); test('should validate required name field', async ({ page }) => { await test.step('Open upload dialog', async () => { await getAddCertButton(page).click(); await waitForDialog(page); // Wait for dialog to be fully interactive }); await test.step('Try to submit with empty name', async () => { const dialog = page.getByRole('dialog'); const uploadButton = dialog.getByRole('button', { name: /upload/i }); await uploadButton.click(); // Form should show validation error or prevent submission const nameInput = dialog.locator('input').first(); const isInvalid = await nameInput.evaluate((el: HTMLInputElement) => el.validity.valid === false || el.getAttribute('aria-invalid') === 'true' ).catch(() => false); // HTML5 validation should prevent submission expect(isInvalid || true).toBeTruthy(); }); await test.step('Close dialog', async () => { await getCancelButton(page).click(); }); }); test('should require certificate file', async ({ page }) => { await test.step('Open upload dialog', async () => { await getAddCertButton(page).click(); await waitForDialog(page); // Wait for dialog to be fully interactive }); await test.step('Fill name but no certificate file', async () => { const dialog = page.getByRole('dialog'); const nameInput = dialog.locator('input').first(); await nameInput.fill('Test Certificate'); const uploadButton = dialog.getByRole('button', { name: /upload/i }); await uploadButton.click(); // Should show validation error for missing file const certFileInput = dialog.locator('#cert-file'); const isRequired = await certFileInput.getAttribute('required'); expect(isRequired !== null).toBeTruthy(); }); await test.step('Close dialog', async () => { await getCancelButton(page).click(); }); }); test('should require private key file', async ({ page }) => { await test.step('Open upload dialog', async () => { await getAddCertButton(page).click(); await waitForDialog(page); // Wait for dialog to be fully interactive }); await test.step('Verify private key is required', async () => { const dialog = page.getByRole('dialog'); const keyFileInput = dialog.locator('#key-file'); const isRequired = await keyFileInput.getAttribute('required'); expect(isRequired !== null).toBeTruthy(); }); await test.step('Close dialog', async () => { await getCancelButton(page).click(); }); }); test('should show upload button with loading state', async ({ page }) => { await test.step('Open upload dialog', async () => { await getAddCertButton(page).click(); await waitForDialog(page); // Wait for dialog to be fully interactive }); await test.step('Verify upload button exists', async () => { const dialog = page.getByRole('dialog'); const uploadButton = dialog.getByRole('button', { name: /upload/i }); await expect(uploadButton).toBeVisible(); }); await test.step('Close dialog', async () => { await getCancelButton(page).click(); }); }); test('should close dialog when Cancel clicked', async ({ page }) => { await test.step('Open and close dialog', async () => { await getAddCertButton(page).click(); await waitForDialog(page); // Wait for dialog to be fully interactive const dialog = page.getByRole('dialog'); await expect(dialog).toBeVisible(); await getCancelButton(page).click(); await expect(dialog).not.toBeVisible({ timeout: 3000 }); }); }); test('should show proper file input styling', async ({ page }) => { await test.step('Open upload dialog', async () => { await getAddCertButton(page).click(); await waitForDialog(page); // Wait for dialog to be fully interactive }); await test.step('Verify file inputs have styled buttons', async () => { const dialog = page.getByRole('dialog'); // File inputs should have styled file buttons const certFileInput = dialog.locator('#cert-file'); const keyFileInput = dialog.locator('#key-file'); await expect(certFileInput).toBeVisible(); await expect(keyFileInput).toBeVisible(); }); await test.step('Close dialog', async () => { await getCancelButton(page).click(); }); }); }); test.describe('Certificate Details', () => { const getEmptyState = (page: import('@playwright/test').Page) => page.locator('tbody tr td[colspan], tbody tr td').filter({ hasText: /no.*certificates.*found/i }).first(); const findDataRow = async (page: import('@playwright/test').Page) => { const rows = page.locator('tbody tr'); const rowCount = await rows.count(); for (let i = 0; i < rowCount; i += 1) { const row = rows.nth(i); const cellCount = await row.locator('td').count(); if (cellCount >= 4) { return row; } } return null; }; const getDataRowOrEmpty = async (page: import('@playwright/test').Page) => { await waitForLoadingComplete(page); const emptyState = getEmptyState(page); await expect .poll(async () => { const dataRow = await findDataRow(page); if (dataRow) return 'data'; if (await emptyState.isVisible().catch(() => false)) return 'empty'; return 'pending'; }, { timeout: 15000 }) .not.toBe('pending'); if (await emptyState.isVisible().catch(() => false)) { return null; } return findDataRow(page); }; test('should display certificate domain in table', async ({ page }) => { await test.step('Check for domain column', async () => { const firstRow = await getDataRowOrEmpty(page); if (!firstRow) { const emptyState = getEmptyState(page); await expect(emptyState).toBeVisible(); return; } // Domain should be visible in the row const domainCell = firstRow.locator('td').nth(1); // Domain is second column await expect(domainCell).toBeVisible(); }); }); test('should display certificate issuer', async ({ page }) => { await test.step('Check for issuer column', async () => { const firstRow = await getDataRowOrEmpty(page); if (!firstRow) { const emptyState = getEmptyState(page); await expect(emptyState).toBeVisible(); return; } const issuerCell = firstRow.locator('td').nth(2); // Issuer is third column await expect(issuerCell).toBeVisible(); }); }); test('should display expiry date', async ({ page }) => { await test.step('Check for expiry column', async () => { const firstRow = await getDataRowOrEmpty(page); if (!firstRow) { const emptyState = getEmptyState(page); await expect(emptyState).toBeVisible(); return; } const expiryCell = firstRow.locator('td').nth(3); // Expires is fourth column await expect(expiryCell).toBeVisible(); // Should contain a date format const expiryText = await expiryCell.textContent(); expect(expiryText).toBeTruthy(); }); }); test('should show valid status for non-expired certificates', async ({ page }) => { await test.step('Check for valid status badge', async () => { const validBadge = page.locator('span').filter({ hasText: /^valid$/i }); const badgeCount = await validBadge.count(); if (badgeCount > 0) { const firstBadge = validBadge.first(); // Should have green styling const classes = await firstBadge.getAttribute('class'); expect(classes).toMatch(/green|success/); } }); }); test('should show expiring status for certificates near expiry', async ({ page }) => { await test.step('Check for expiring status badge', async () => { const expiringBadge = page.locator('span').filter({ hasText: /expiring.*soon/i }); const badgeCount = await expiringBadge.count(); if (badgeCount > 0) { const firstBadge = expiringBadge.first(); // Should have yellow/warning styling const classes = await firstBadge.getAttribute('class'); expect(classes).toMatch(/yellow|warning/); } }); }); test('should show expired status for expired certificates', async ({ page }) => { await test.step('Check for expired status badge', async () => { const expiredBadge = page.locator('span').filter({ hasText: /^expired$/i }); const badgeCount = await expiredBadge.count(); if (badgeCount > 0) { const firstBadge = expiredBadge.first(); // Should have red/error styling const classes = await firstBadge.getAttribute('class'); expect(classes).toMatch(/red|error|danger/); } }); }); test('should show untrusted status for staging certificates', async ({ page }) => { await test.step('Check for untrusted status badge', async () => { const untrustedBadge = page.locator('span').filter({ hasText: /untrusted|staging/i }); const badgeCount = await untrustedBadge.count(); if (badgeCount > 0) { const firstBadge = untrustedBadge.first(); // Should have orange/warning styling const classes = await firstBadge.getAttribute('class'); expect(classes).toMatch(/orange|warning/); } }); }); }); test.describe('Delete Certificate', () => { test('should show delete button for custom certificates', async ({ page }) => { await test.step('Check for delete buttons', async () => { const deleteButtons = page.locator('button[title*="Delete"], button').filter({ has: page.locator('svg.lucide-trash-2') }); const deleteCount = await deleteButtons.count(); // Custom certificates should have delete buttons expect(deleteCount >= 0).toBeTruthy(); }); }); test('should show delete button for staging certificates', async ({ page }) => { await test.step('Check for staging certificate delete buttons', async () => { // Staging certificates should have delete buttons const stagingRows = page.locator('tbody tr').filter({ hasText: /staging/i }); const stagingCount = await stagingRows.count(); if (stagingCount > 0) { const firstStagingRow = stagingRows.first(); const deleteButton = firstStagingRow.locator('button').filter({ has: page.locator('svg.lucide-trash-2') }); const hasDelete = await deleteButton.isVisible().catch(() => false); expect(hasDelete || true).toBeTruthy(); } }); }); test('should show delete confirmation dialog', async ({ page }) => { await test.step('Click delete and verify confirmation', async () => { const deleteButtons = page.locator('tbody button').filter({ has: page.locator('svg.lucide-trash-2') }); const deleteCount = await deleteButtons.count(); if (deleteCount > 0) { // Mock the confirm dialog since browser's native confirm is used page.once('dialog', dialog => { expect(dialog.type()).toBe('confirm'); expect(dialog.message()).toContain('delete'); dialog.dismiss(); }); await deleteButtons.first().click(); } }); }); test('should warn if certificate is in use by proxy host', async ({ page }) => { await test.step('Try to delete certificate in use', async () => { const deleteButtons = page.locator('tbody button').filter({ has: page.locator('svg.lucide-trash-2') }); const deleteCount = await deleteButtons.count(); if (deleteCount > 0) { // If certificate is in use, should show error toast page.once('dialog', dialog => { dialog.accept(); }); await deleteButtons.first().click(); // Wait for toast notification after deletion attempt await waitForDebounce(page); // Either toast error or successful deletion const toast = page.locator('[role="alert"], [role="status"], .toast, .Toastify__toast'); const hasToast = await toast.isVisible({ timeout: 3000 }).catch(() => false); expect(hasToast || true).toBeTruthy(); } }); }); test('should cancel delete when confirmation dismissed', async ({ page }) => { await test.step('Dismiss delete confirmation', async () => { const deleteButtons = page.locator('tbody button').filter({ has: page.locator('svg.lucide-trash-2') }); const deleteCount = await deleteButtons.count(); if (deleteCount > 0) { // Count rows before const rowsBefore = await page.locator('tbody tr').count(); // Dismiss the confirm dialog page.once('dialog', dialog => { dialog.dismiss(); }); await deleteButtons.first().click(); await waitForDebounce(page); // Wait for confirmation dialog animation // Rows should remain unchanged const rowsAfter = await page.locator('tbody tr').count(); expect(rowsAfter).toBe(rowsBefore); } }); }); test('should create backup before deletion', async ({ page }) => { await test.step('Verify backup message in confirmation', async () => { const deleteButtons = page.locator('tbody button').filter({ has: page.locator('svg.lucide-trash-2') }); const deleteCount = await deleteButtons.count(); if (deleteCount > 0) { page.once('dialog', dialog => { const message = dialog.message(); // Confirmation should mention backup expect(message.toLowerCase()).toContain('backup'); dialog.dismiss(); }); await deleteButtons.first().click(); } }); }); test('should show config reload overlay during deletion', async ({ page }) => { await test.step('Verify loading overlay appears', async () => { const deleteButtons = page.locator('tbody button').filter({ has: page.locator('svg.lucide-trash-2') }); const deleteCount = await deleteButtons.count(); if (deleteCount > 0) { page.once('dialog', dialog => { dialog.accept(); }); // Start deletion await deleteButtons.first().click(); // Loading overlay may appear briefly await waitForDebounce(page); // Wait for overlay animation } }); }); }); test.describe('Certificate Renewal', () => { test('should show renewal warning for expiring certificates', async ({ page }) => { await test.step('Check for expiring certificate indicators', async () => { const expiringBadges = page.locator('span').filter({ hasText: /expiring/i }); const count = await expiringBadges.count(); // If expiring certificates exist, they should be highlighted if (count > 0) { const firstBadge = expiringBadges.first(); await expect(firstBadge).toBeVisible(); } }); }); test('should show Let\'s Encrypt auto-renewal info', async ({ page }) => { await test.step('Check for Let\'s Encrypt info', async () => { // The info alert should mention certificate management const alert = page.locator('[role="alert"], .alert'); const hasAlert = await alert.isVisible().catch(() => false); if (hasAlert) { const alertText = await alert.textContent(); expect(alertText).toBeTruthy(); } }); }); }); test.describe('Form Validation', () => { test('should reject empty friendly name', async ({ page }) => { await test.step('Try to upload with empty name', async () => { await getAddCertButton(page).click(); await waitForDialog(page); // Wait for dialog to be fully interactive const dialog = page.getByRole('dialog'); const uploadButton = dialog.getByRole('button', { name: /upload/i }); await uploadButton.click(); // Should not close dialog (validation error) await expect(dialog).toBeVisible(); await getCancelButton(page).click(); }); }); test('should handle special characters in name', async ({ page }) => { await test.step('Test special characters', async () => { await getAddCertButton(page).click(); await waitForDialog(page); // Wait for dialog to be fully interactive const dialog = page.getByRole('dialog'); const nameInput = dialog.locator('input').first(); // Test with safe special characters await nameInput.fill('Test Cert - Special (chars) #1'); // Should accept the input const value = await nameInput.inputValue(); expect(value).toContain('Special'); await getCancelButton(page).click(); }); }); test('should show placeholder text in name input', async ({ page }) => { await test.step('Verify placeholder text', async () => { await getAddCertButton(page).click(); await waitForDialog(page); const dialog = page.getByRole('dialog'); const nameInput = dialog.locator('input').first(); const placeholder = await nameInput.getAttribute('placeholder'); expect(placeholder).toBeTruthy(); await getCancelButton(page).click(); }); }); }); test.describe('Form Accessibility', () => { test('should have accessible form labels', async ({ page }) => { await test.step('Open form and verify labels', async () => { await getAddCertButton(page).click(); await waitForDialog(page); // Wait for dialog to be fully interactive const dialog = page.getByRole('dialog'); // Check for labels const certLabel = dialog.locator('label[for="cert-file"]'); const keyLabel = dialog.locator('label[for="key-file"]'); await expect(certLabel).toBeVisible(); await expect(keyLabel).toBeVisible(); await getCancelButton(page).click(); }); }); test('should be keyboard navigable', async ({ page }) => { await test.step('Open upload dialog and wait for interactivity', async () => { await getAddCertButton(page).click(); const dialog = await waitForDialog(page); await expect(dialog).toBeVisible(); }); await test.step('Navigate through form fields with Tab key', async () => { // Tab to first input (name field) await page.keyboard.press('Tab'); const firstFocusable = page.locator(':focus'); await expect(firstFocusable).toBeVisible(); // Tab to next field await page.keyboard.press('Tab'); const secondFocusable = page.locator(':focus'); await expect(secondFocusable).toBeVisible(); // Tab to third field await page.keyboard.press('Tab'); const thirdFocusable = page.locator(':focus'); await expect(thirdFocusable).toBeVisible(); // Verify at least one element has focus const focusedElement = page.locator(':focus'); await expect(focusedElement).toBeFocused(); }); await test.step('Close dialog and verify cleanup', async () => { const dialog = page.getByRole('dialog'); await getCancelButton(page).click(); // Verify dialog is properly closed await expect(dialog).not.toBeVisible({ timeout: 3000 }); // Verify page is still interactive await expect(page.getByRole('heading', { name: /certificates/i })).toBeVisible(); }); }); test('should close dialog on Escape key', async ({ page }) => { await test.step('Open upload dialog', async () => { await getAddCertButton(page).click(); const dialog = await waitForDialog(page); await expect(dialog).toBeVisible(); }); await test.step('Press Escape and verify dialog closes', async () => { const dialog = page.getByRole('dialog'); await page.keyboard.press('Escape'); // Explicit verification with timeout await expect(dialog).not.toBeVisible({ timeout: 3000 }); }); await test.step('Verify page state after dialog close', async () => { // Ensure page is still interactive const heading = page.getByRole('heading', { name: /certificates/i }); await expect(heading).toBeVisible(); // Verify no orphaned elements const orphanedDialog = page.getByRole('dialog'); await expect(orphanedDialog).toHaveCount(0); }); }); test('should have proper dialog role', async ({ page }) => { await test.step('Verify dialog ARIA role', async () => { await getAddCertButton(page).click(); await waitForDialog(page); // Wait for dialog to be fully interactive const dialog = page.getByRole('dialog'); await expect(dialog).toBeVisible(); await getCancelButton(page).click(); }); }); test('should have dialog title in heading', async ({ page }) => { await test.step('Verify dialog has heading', async () => { await getAddCertButton(page).click(); await waitForDialog(page); // Wait for dialog to be fully interactive const dialog = page.getByRole('dialog'); const heading = dialog.getByRole('heading'); await expect(heading).toBeVisible(); await getCancelButton(page).click(); }); }); }); test.describe('Integration with Proxy Hosts', () => { test('should show certificate usage in proxy hosts', async ({ page }) => { await test.step('Check if certificates are referenced', async () => { // Navigate to proxy hosts to see certificate usage await page.goto('/proxy-hosts'); await waitForLoadingComplete(page); const table = page.getByRole('table'); const hasTable = await table.isVisible().catch(() => false); if (hasTable) { // SSL column may show certificate info const sslColumn = page.locator('th').filter({ hasText: /ssl/i }); const hasSslColumn = await sslColumn.isVisible().catch(() => false); expect(hasSslColumn || true).toBeTruthy(); } // Navigate back to certificates await page.goto('/certificates'); await waitForLoadingComplete(page); }); }); test('should navigate between Certificates and Proxy Hosts', async ({ page }) => { await test.step('Navigate to Proxy Hosts', async () => { await page.goto('/proxy-hosts'); const heading = page.getByRole('heading', { name: /^proxy hosts$/i }); await expect(heading).toBeVisible({ timeout: 10000 }); const hasHeading = await heading.isVisible({ timeout: 5000 }).catch(() => false); expect(hasHeading || true).toBeTruthy(); }); await test.step('Navigate back to Certificates', async () => { await page.goto('/certificates'); const heading = page.getByRole('heading', { name: /certificates/i }); await expect(heading).toBeVisible({ timeout: 10000 }); }); }); }); test.describe('Table Interactions', () => { test('should highlight row on hover', async ({ page }) => { await test.step('Verify hover styling on table rows', async () => { const rows = page.locator('tbody tr'); const rowCount = await rows.count(); if (rowCount > 0) { const firstRow = rows.first(); await firstRow.hover(); // Row should have hover state await waitForDebounce(page, { timeout: 1000 }); // Wait for hover animation } }); }); test('should display full table on wide screens', async ({ page }) => { await test.step('Verify table layout', async () => { await page.setViewportSize({ width: 1280, height: 720 }); const table = page.getByRole('table'); const hasTable = await table.isVisible().catch(() => false); if (hasTable) { // All columns should be visible on wide screens const headers = page.locator('thead th'); const headerCount = await headers.count(); expect(headerCount).toBeGreaterThanOrEqual(5); } }); }); test('should handle responsive layout', async ({ page }) => { await test.step('Test mobile viewport', async () => { await page.setViewportSize({ width: 375, height: 667 }); await waitForDebounce(page); // Wait for responsive layout adjustment // Page should still function const heading = page.getByRole('heading', { name: /certificates/i }); await expect(heading).toBeVisible(); // Reset viewport await page.setViewportSize({ width: 1280, height: 720 }); }); }); }); test.describe('Error Handling', () => { test('should show error message on API failure', async ({ page }) => { await test.step('Verify error handling exists', async () => { // The component should handle loading and error states const errorMessage = page.getByText(/failed.*load|error/i); const hasError = await errorMessage.isVisible().catch(() => false); // Normally there shouldn't be an error, but the component should handle it expect(hasError || true).toBeTruthy(); }); }); test('should show upload error on invalid certificate', async ({ page }) => { await test.step('Verify upload error handling', async () => { await getAddCertButton(page).click(); await waitForDialog(page); // Wait for dialog to be fully interactive // Fill in name but with invalid files would trigger error // This tests the error handling path const dialog = page.getByRole('dialog'); await expect(dialog).toBeVisible(); await getCancelButton(page).click(); }); }); }); test.describe('Page Layout', () => { test('should have PageShell with title and description', async ({ page }) => { await test.step('Verify page layout structure', async () => { const heading = page.getByRole('heading', { name: /certificates/i }); await expect(heading).toBeVisible(); // Should have description text const description = page.getByText(/manage.*ssl|certificate/i); const hasDescription = await description.isVisible().catch(() => false); expect(hasDescription || true).toBeTruthy(); }); }); test('should have action button in header', async ({ page }) => { await test.step('Verify Add Certificate button is in header area', async () => { const addButton = getAddCertButton(page); await expect(addButton).toBeVisible(); // Button should have Plus icon const plusIcon = addButton.locator('svg'); const hasIcon = await plusIcon.isVisible().catch(() => false); expect(hasIcon || true).toBeTruthy(); }); }); test('should have card container for table', async ({ page }) => { await test.step('Verify table is in styled container', async () => { const table = page.getByRole('table'); const hasTable = await table.isVisible().catch(() => false); if (hasTable) { // Table should be in a styled card const container = table.locator('..'); const classes = await container.getAttribute('class'); expect(classes).toBeTruthy(); } }); }); }); });