Files
Charon/tests/core/certificates.spec.ts
2026-03-04 18:34:49 +00:00

1092 lines
40 KiB
TypeScript

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