Some checks are pending
Go Benchmark / Performance Regression Check (push) Waiting to run
Cerberus Integration / Cerberus Security Stack Integration (push) Waiting to run
Upload Coverage to Codecov / Backend Codecov Upload (push) Waiting to run
Upload Coverage to Codecov / Frontend Codecov Upload (push) Waiting to run
CodeQL - Analyze / CodeQL analysis (go) (push) Waiting to run
CodeQL - Analyze / CodeQL analysis (javascript-typescript) (push) Waiting to run
CrowdSec Integration / CrowdSec Bouncer Integration (push) Waiting to run
Docker Build, Publish & Test / build-and-push (push) Waiting to run
Docker Build, Publish & Test / Security Scan PR Image (push) Blocked by required conditions
Quality Checks / Auth Route Protection Contract (push) Waiting to run
Quality Checks / Codecov Trigger/Comment Parity Guard (push) Waiting to run
Quality Checks / Backend (Go) (push) Waiting to run
Quality Checks / Frontend (React) (push) Waiting to run
Rate Limit integration / Rate Limiting Integration (push) Waiting to run
Security Scan (PR) / Trivy Binary Scan (push) Waiting to run
Supply Chain Verification (PR) / Verify Supply Chain (push) Waiting to run
WAF integration / Coraza WAF Integration (push) Waiting to run
658 lines
25 KiB
TypeScript
Executable File
658 lines
25 KiB
TypeScript
Executable File
import { test, expect } from './fixtures/test';
|
|
import { waitForAPIHealth } from './utils/api-helpers';
|
|
import { getToastLocator, refreshListAndWait } from './utils/ui-helpers';
|
|
import {
|
|
waitForAPIResponse,
|
|
waitForConfigReload,
|
|
waitForDialog,
|
|
waitForLoadingComplete,
|
|
waitForResourceInUI,
|
|
} from './utils/wait-helpers';
|
|
|
|
async function getAuthToken(page: import('@playwright/test').Page): Promise<string> {
|
|
const storageState = await page.request.storageState();
|
|
const origins = Array.isArray(storageState.origins) ? storageState.origins : [];
|
|
|
|
for (const originEntry of origins) {
|
|
const localStorageEntries = Array.isArray(originEntry?.localStorage)
|
|
? originEntry.localStorage
|
|
: [];
|
|
|
|
const authEntry = localStorageEntries.find((entry) => entry.name === 'auth');
|
|
if (authEntry?.value) {
|
|
try {
|
|
const parsed = JSON.parse(authEntry.value) as { token?: string };
|
|
if (parsed?.token) {
|
|
return parsed.token;
|
|
}
|
|
} catch {
|
|
}
|
|
}
|
|
|
|
const tokenEntry = localStorageEntries.find(
|
|
(entry) => entry.name === 'token' || entry.name === 'charon_auth_token'
|
|
);
|
|
if (tokenEntry?.value) {
|
|
return tokenEntry.value;
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
function buildAuthHeaders(token: string): Record<string, string> | undefined {
|
|
return token ? { Authorization: `Bearer ${token}` } : undefined;
|
|
}
|
|
|
|
/**
|
|
* DNS Provider CRUD Operations E2E Tests
|
|
*
|
|
* Tests Create, Read, Update, Delete operations for DNS Providers including:
|
|
* - Creating providers of different types
|
|
* - Listing providers
|
|
* - Editing provider configuration
|
|
* - Deleting providers
|
|
* - Form validation
|
|
*/
|
|
|
|
test.describe('DNS Provider CRUD Operations', () => {
|
|
test.beforeEach(async ({ request }) => {
|
|
await waitForAPIHealth(request);
|
|
});
|
|
|
|
test.describe('Create Provider', () => {
|
|
test('should create a Manual DNS provider', async ({ page }) => {
|
|
await page.goto('/dns/providers');
|
|
await waitForLoadingComplete(page);
|
|
|
|
await test.step('Wait for DNS Providers page content', async () => {
|
|
// Wait for the nested DNS page content area to be visible
|
|
// DNS route has parent DNS component + child DNSProviders component via Outlet
|
|
await page.waitForLoadState('networkidle');
|
|
await page.waitForTimeout(1000); // Allow React nested routing to complete
|
|
});
|
|
|
|
await test.step('Click Add Provider button', async () => {
|
|
// Use first() to handle both header button and empty state button
|
|
const addButton = page.getByRole('button', { name: /add.*provider/i }).first();
|
|
await expect(addButton).toBeVisible({ timeout: 10000 });
|
|
await addButton.click();
|
|
await waitForDialog(page);
|
|
});
|
|
|
|
await test.step('Fill provider name', async () => {
|
|
// The input has id="provider-name" and aria-label="Name" (from translation)
|
|
const nameInput = page.locator('#provider-name').or(page.getByRole('textbox', { name: /name/i }));
|
|
await expect(nameInput).toBeVisible();
|
|
await nameInput.fill('Test Manual Provider');
|
|
});
|
|
|
|
await test.step('Select Manual type', async () => {
|
|
// Select has id="provider-type" and aria-label from translation
|
|
const typeSelect = page.locator('#provider-type').or(page.getByRole('combobox', { name: /type|provider/i }));
|
|
await typeSelect.click();
|
|
await page.getByRole('option', { name: /manual/i }).click();
|
|
});
|
|
|
|
await test.step('Save provider', async () => {
|
|
// Click the Create button in the dialog footer
|
|
const dialog = await waitForDialog(page);
|
|
// Look for button with exact text "Create" within dialog
|
|
const saveButton = dialog.getByRole('button', { name: 'Create' });
|
|
await expect(saveButton).toBeVisible();
|
|
await expect(saveButton).toBeEnabled();
|
|
|
|
const responsePromise = waitForAPIResponse(page, '/api/v1/dns-providers');
|
|
await saveButton.click();
|
|
|
|
await responsePromise;
|
|
await waitForConfigReload(page);
|
|
|
|
// Wait for dialog to close (indicates success)
|
|
await expect(dialog).not.toBeVisible({ timeout: 10000 });
|
|
});
|
|
|
|
await test.step('Verify success', async () => {
|
|
// Wait for success toast using shared helper
|
|
const successToast = getToastLocator(page, /success|created/i, { type: 'success' });
|
|
await expect(successToast).toBeVisible({ timeout: 5000 });
|
|
|
|
// Refresh list to ensure provider appears
|
|
await refreshListAndWait(page, { timeout: 5000 });
|
|
});
|
|
});
|
|
|
|
test('should create a Webhook DNS provider', async ({ page }) => {
|
|
await page.goto('/dns/providers');
|
|
await waitForLoadingComplete(page);
|
|
|
|
await test.step('Open add provider dialog', async () => {
|
|
await page.getByRole('button', { name: /add.*provider/i }).first().click();
|
|
await waitForDialog(page);
|
|
});
|
|
|
|
await test.step('Select Webhook type first', async () => {
|
|
// Must select type first to reveal credential fields
|
|
const dialog = await waitForDialog(page);
|
|
const typeSelect = dialog
|
|
.locator('#provider-type')
|
|
.or(dialog.getByRole('combobox', { name: /type|provider/i }));
|
|
await typeSelect.click();
|
|
|
|
const listbox = page.getByRole('listbox');
|
|
await expect(listbox).toBeVisible({ timeout: 5000 });
|
|
|
|
const webhookOption = listbox.getByRole('option', { name: /webhook/i });
|
|
await expect(webhookOption).toBeVisible({ timeout: 5000 });
|
|
await webhookOption.focus();
|
|
await page.keyboard.press('Enter');
|
|
await expect(listbox).toBeHidden({ timeout: 5000 });
|
|
});
|
|
|
|
await test.step('Fill provider details', async () => {
|
|
const dialog = await waitForDialog(page);
|
|
const listbox = page.getByRole('listbox');
|
|
if (await listbox.isVisible().catch(() => false)) {
|
|
await page.keyboard.press('Escape');
|
|
await expect(listbox).toBeHidden({ timeout: 5000 });
|
|
}
|
|
|
|
const nameInput = dialog.locator('#provider-name');
|
|
await expect(nameInput).toBeVisible({ timeout: 10000 });
|
|
await nameInput.click();
|
|
await nameInput.fill('Test Webhook Provider');
|
|
|
|
const createUrlField = dialog.getByRole('textbox', { name: /create url/i });
|
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
if (await createUrlField.isVisible().catch(() => false)) {
|
|
break;
|
|
}
|
|
|
|
const typeSelect = dialog
|
|
.locator('#provider-type')
|
|
.or(dialog.getByRole('combobox', { name: /type|provider/i }));
|
|
await typeSelect.click();
|
|
const webhookOption = page.getByRole('option', { name: /webhook/i });
|
|
await expect(webhookOption).toBeVisible({ timeout: 5000 });
|
|
await webhookOption.focus();
|
|
await page.keyboard.press('Enter');
|
|
}
|
|
await expect(createUrlField).toBeVisible({ timeout: 10000 });
|
|
await createUrlField.fill('https://example.com/dns/create');
|
|
|
|
const deleteUrlField = dialog.getByRole('textbox', { name: /delete url/i });
|
|
await expect(deleteUrlField).toBeVisible({ timeout: 10000 });
|
|
await deleteUrlField.fill('https://example.com/dns/delete');
|
|
});
|
|
|
|
await test.step('Save and verify', async () => {
|
|
const dialog = await waitForDialog(page);
|
|
const responsePromise = waitForAPIResponse(page, '/api/v1/dns-providers');
|
|
const createButton = dialog.getByRole('button', { name: /create/i });
|
|
await expect(createButton).toBeEnabled();
|
|
await createButton.click();
|
|
await responsePromise;
|
|
await waitForConfigReload(page);
|
|
await expect(dialog).toBeHidden({ timeout: 10000 });
|
|
|
|
const successToast = getToastLocator(page, /success|created/i, { type: 'success' });
|
|
await expect(successToast).toBeVisible({ timeout: 5000 });
|
|
});
|
|
});
|
|
|
|
test('should show validation errors for missing required fields', async ({ page }) => {
|
|
await page.goto('/dns/providers');
|
|
await waitForLoadingComplete(page);
|
|
|
|
await test.step('Open add dialog', async () => {
|
|
await page.getByRole('button', { name: /add.*provider/i }).first().click();
|
|
await waitForDialog(page);
|
|
});
|
|
|
|
await test.step('Verify save button is disabled when required fields empty', async () => {
|
|
// The Create button is disabled when name or type is empty
|
|
const saveButton = page.getByRole('button', { name: /create/i });
|
|
await expect(saveButton).toBeDisabled();
|
|
});
|
|
|
|
await test.step('Fill name only and verify still disabled', async () => {
|
|
await page.locator('#provider-name').fill('Test Provider');
|
|
// Still disabled because type is not selected
|
|
const saveButton = page.getByRole('button', { name: /create/i });
|
|
await expect(saveButton).toBeDisabled();
|
|
});
|
|
});
|
|
|
|
test('should validate webhook URL format', async ({ page }) => {
|
|
await page.goto('/dns/providers');
|
|
await waitForLoadingComplete(page);
|
|
|
|
await page.getByRole('button', { name: /add.*provider/i }).first().click();
|
|
await waitForDialog(page);
|
|
|
|
await test.step('Select Webhook type and enter invalid URL', async () => {
|
|
const dialog = await waitForDialog(page);
|
|
await dialog.locator('#provider-name').fill('Test Webhook');
|
|
|
|
const typeSelect = dialog
|
|
.locator('#provider-type')
|
|
.or(dialog.getByRole('combobox', { name: /type|provider/i }));
|
|
const urlField = dialog.getByRole('textbox', { name: /create url/i });
|
|
const webhookOption = page.getByRole('option', { name: /webhook/i });
|
|
const listbox = page.getByRole('listbox');
|
|
|
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
await typeSelect.click();
|
|
await expect(webhookOption).toBeVisible({ timeout: 5000 });
|
|
await webhookOption.focus();
|
|
await page.keyboard.press('Enter');
|
|
await expect(listbox).toBeHidden({ timeout: 5000 });
|
|
|
|
if (await urlField.isVisible().catch(() => false)) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
await expect(urlField).toBeVisible({ timeout: 10000 });
|
|
await urlField.fill('not-a-valid-url');
|
|
await urlField.blur();
|
|
});
|
|
|
|
await test.step('Try to save and check for URL validation', async () => {
|
|
const dialog = await waitForDialog(page);
|
|
const createButton = dialog.getByRole('button', { name: /save|create/i });
|
|
await createButton.click();
|
|
|
|
const urlField = page.getByRole('textbox', { name: /create url/i });
|
|
await expect(urlField).toHaveValue('not-a-valid-url');
|
|
await expect(dialog).toBeVisible();
|
|
});
|
|
});
|
|
});
|
|
|
|
test.describe('Provider List', () => {
|
|
test('should display provider list or empty state', async ({ page }) => {
|
|
await page.goto('/dns/providers');
|
|
await waitForLoadingComplete(page);
|
|
|
|
await test.step('Verify page loads', async () => {
|
|
await expect(page).toHaveURL(/dns\/providers/);
|
|
});
|
|
|
|
await test.step('Check for providers or empty state', async () => {
|
|
// The page should always show at least one of:
|
|
// 1. Add Provider button (header or empty state)
|
|
// 2. Provider cards with Edit buttons
|
|
// 3. Empty state message
|
|
|
|
const addButton = page.getByRole('button', { name: /add.*provider/i });
|
|
await expect(addButton.first()).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test('should show Add Provider button', async ({ page }) => {
|
|
await page.goto('/dns/providers');
|
|
await waitForLoadingComplete(page);
|
|
|
|
// Use first() since there may be both header button and empty state button
|
|
const addButton = page.getByRole('button', { name: /add.*provider/i }).first();
|
|
await expect(addButton).toBeVisible();
|
|
await expect(addButton).toBeEnabled();
|
|
});
|
|
|
|
test('should show provider details in list', async ({ page }) => {
|
|
await page.goto('/dns/providers');
|
|
await waitForLoadingComplete(page);
|
|
|
|
// If providers exist, verify they show required info
|
|
// The page uses Card components in a grid with .grid class
|
|
const providerCards = page.locator('.grid > div').filter({ has: page.locator('h3, [class*="title"]') });
|
|
|
|
if ((await providerCards.count()) > 0) {
|
|
const firstProvider = providerCards.first();
|
|
|
|
await test.step('Verify provider name is displayed', async () => {
|
|
// Provider should have a name visible in the card title
|
|
const hasName = (await firstProvider.locator('h3, [class*="title"]').count()) > 0;
|
|
expect(hasName).toBeTruthy();
|
|
});
|
|
|
|
await test.step('Verify provider type is displayed', async () => {
|
|
// Provider should show its type (cloudflare, manual, etc.)
|
|
const typeText = firstProvider.getByText(/cloudflare|route53|manual|webhook|rfc2136|script/i).first();
|
|
await expect(typeText).toBeVisible();
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
test.describe('Edit Provider', () => {
|
|
// These tests require at least one provider to exist
|
|
test('should open edit dialog for existing provider', async ({ page }) => {
|
|
await page.goto('/dns/providers');
|
|
await waitForLoadingComplete(page);
|
|
|
|
// Wait for the page to load and check for provider cards
|
|
// The page uses Card components inside a grid
|
|
const providerCards = page.locator('.grid > div').filter({ has: page.getByRole('button', { name: /edit/i }) });
|
|
|
|
if ((await providerCards.count()) > 0) {
|
|
await test.step('Click edit on first provider', async () => {
|
|
const firstProvider = providerCards.first();
|
|
|
|
// Look for edit button
|
|
const editButton = firstProvider.getByRole('button', { name: /edit/i });
|
|
await editButton.click();
|
|
});
|
|
|
|
await test.step('Verify edit dialog opens', async () => {
|
|
// Edit dialog should have the provider name pre-filled
|
|
const dialog = await waitForDialog(page);
|
|
const nameInput = dialog.locator('#provider-name');
|
|
await expect(nameInput).toBeVisible();
|
|
|
|
const currentValue = await nameInput.inputValue();
|
|
expect(currentValue.length).toBeGreaterThan(0);
|
|
});
|
|
}
|
|
});
|
|
|
|
test('should update provider name', async ({ page, request }) => {
|
|
let createdProviderId: number | string | undefined;
|
|
const initialName = `Update Target ${Date.now()}`;
|
|
const updatedName = `Updated Provider ${Date.now()}`;
|
|
|
|
try {
|
|
const token = await getAuthToken(page);
|
|
expect(token).toBeTruthy();
|
|
|
|
const createResponse = await page.request.post('/api/v1/dns-providers', {
|
|
data: {
|
|
name: initialName,
|
|
provider_type: 'manual',
|
|
credentials: {},
|
|
},
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
expect(createResponse.ok()).toBeTruthy();
|
|
|
|
const createdProvider = await createResponse.json();
|
|
createdProviderId = createdProvider?.uuid ?? createdProvider?.id;
|
|
expect(createdProviderId).toBeTruthy();
|
|
|
|
await page.goto('/dns/providers');
|
|
await waitForLoadingComplete(page);
|
|
|
|
const providerCard = page.locator('.grid > div').filter({ hasText: initialName }).first();
|
|
await expect(providerCard).toBeVisible({ timeout: 10000 });
|
|
|
|
await test.step('Open edit dialog', async () => {
|
|
await providerCard.getByRole('button', { name: /edit/i }).click();
|
|
});
|
|
|
|
await test.step('Update name', async () => {
|
|
const dialog = await waitForDialog(page);
|
|
const nameInput = dialog.locator('#provider-name');
|
|
await nameInput.clear();
|
|
await nameInput.fill(updatedName);
|
|
});
|
|
|
|
await test.step('Save changes', async () => {
|
|
const token = await getAuthToken(page);
|
|
expect(token).toBeTruthy();
|
|
|
|
const response = await page.request.put(`/api/v1/dns-providers/${createdProviderId}`, {
|
|
data: {
|
|
name: updatedName,
|
|
provider_type: 'manual',
|
|
credentials: {},
|
|
},
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
|
|
if (!response.ok()) {
|
|
const errorBody = await response.text().catch(() => '');
|
|
throw new Error(`Provider update failed: ${response.status()} ${errorBody}`);
|
|
}
|
|
await waitForConfigReload(page);
|
|
});
|
|
|
|
await test.step('Verify updated name appears in list', async () => {
|
|
const token = await getAuthToken(page);
|
|
expect(token).toBeTruthy();
|
|
|
|
const verifyResponse = await page.request.get('/api/v1/dns-providers', {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
expect(verifyResponse.ok()).toBe(true);
|
|
const verifyProviders = await verifyResponse.json();
|
|
const providerItems = Array.isArray(verifyProviders)
|
|
? verifyProviders
|
|
: verifyProviders?.providers;
|
|
const updatedProvider = Array.isArray(providerItems)
|
|
? providerItems.find((provider: { name?: string }) => provider?.name === updatedName)
|
|
: null;
|
|
expect(updatedProvider).toBeTruthy();
|
|
expect(updatedProvider.name).toBe(updatedName);
|
|
|
|
const dialog = page.getByRole('dialog');
|
|
if (await dialog.isVisible().catch(() => false)) {
|
|
const closeButton = dialog.getByRole('button', { name: /close|cancel/i }).first();
|
|
if (await closeButton.isVisible().catch(() => false)) {
|
|
await closeButton.click();
|
|
}
|
|
await expect(dialog).toBeHidden({ timeout: 10000 });
|
|
}
|
|
});
|
|
} finally {
|
|
if (createdProviderId) {
|
|
await page.request.delete(`/api/v1/dns-providers/${createdProviderId}`);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
test.describe('Delete Provider', () => {
|
|
test('should show delete confirmation dialog', async ({ page }) => {
|
|
await page.goto('/dns/providers');
|
|
await waitForLoadingComplete(page);
|
|
|
|
// Find provider cards with delete buttons
|
|
const providerCards = page.locator('.grid > div').filter({ has: page.getByRole('button', { name: /delete|remove/i }) });
|
|
|
|
if ((await providerCards.count()) > 0) {
|
|
await test.step('Click delete on first provider', async () => {
|
|
const firstProvider = providerCards.first();
|
|
|
|
// The delete button might be icon-only (no text)
|
|
const deleteButton = firstProvider.locator('button').filter({ has: page.locator('svg') }).last();
|
|
|
|
await deleteButton.click();
|
|
});
|
|
|
|
await test.step('Verify confirmation dialog appears', async () => {
|
|
// Should show confirmation dialog
|
|
const confirmDialog = page.getByRole('dialog').or(page.getByRole('alertdialog'));
|
|
|
|
await expect(confirmDialog).toBeVisible({ timeout: 3000 });
|
|
});
|
|
|
|
await test.step('Cancel deletion', async () => {
|
|
// Cancel to not actually delete
|
|
const cancelButton = page.getByRole('button', { name: /cancel/i });
|
|
if (await cancelButton.isVisible()) {
|
|
await cancelButton.click();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
test.describe('API Operations', () => {
|
|
test('should list providers via API', async ({ page }) => {
|
|
const token = await getAuthToken(page);
|
|
const response = await page.request.get('/api/v1/dns-providers', {
|
|
headers: buildAuthHeaders(token),
|
|
});
|
|
expect(response.ok()).toBeTruthy();
|
|
|
|
const data = await response.json();
|
|
// Response should be an array (possibly empty)
|
|
expect(Array.isArray(data) || (data && Array.isArray(data.providers || data.items || data.data))).toBeTruthy();
|
|
});
|
|
|
|
test('should create provider via API', async ({ page }) => {
|
|
const token = await getAuthToken(page);
|
|
const response = await page.request.post('/api/v1/dns-providers', {
|
|
data: {
|
|
name: 'API Test Manual Provider',
|
|
provider_type: 'manual',
|
|
},
|
|
headers: buildAuthHeaders(token),
|
|
});
|
|
|
|
// Should succeed or return validation error (not server error)
|
|
expect(response.status()).toBeLessThan(500);
|
|
|
|
if (response.ok()) {
|
|
const provider = await response.json();
|
|
expect(provider).toHaveProperty('id');
|
|
expect(provider.name).toBe('API Test Manual Provider');
|
|
expect(provider.provider_type).toBe('manual');
|
|
|
|
// Cleanup: delete the created provider
|
|
if (provider.id) {
|
|
await page.request.delete(`/api/v1/dns-providers/${provider.id}`, {
|
|
headers: buildAuthHeaders(token),
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
test('should reject invalid provider type via API', async ({ page }) => {
|
|
const token = await getAuthToken(page);
|
|
const response = await page.request.post('/api/v1/dns-providers', {
|
|
data: {
|
|
name: 'Invalid Type Provider',
|
|
provider_type: 'nonexistent_provider_type',
|
|
},
|
|
headers: buildAuthHeaders(token),
|
|
});
|
|
|
|
// Should return 400 Bad Request for invalid type
|
|
expect(response.status()).toBe(400);
|
|
});
|
|
|
|
test('should get single provider via API', async ({ page }) => {
|
|
const token = await getAuthToken(page);
|
|
// First, create a provider to ensure we have at least one
|
|
const createResponse = await page.request.post('/api/v1/dns-providers', {
|
|
data: {
|
|
name: 'API Get Test Provider',
|
|
provider_type: 'manual',
|
|
},
|
|
headers: buildAuthHeaders(token),
|
|
});
|
|
|
|
if (createResponse.ok()) {
|
|
const created = await createResponse.json();
|
|
|
|
const getResponse = await page.request.get(`/api/v1/dns-providers/${created.id}`, {
|
|
headers: buildAuthHeaders(token),
|
|
});
|
|
expect(getResponse.ok()).toBeTruthy();
|
|
|
|
const provider = await getResponse.json();
|
|
expect(provider).toHaveProperty('id');
|
|
expect(provider).toHaveProperty('name');
|
|
expect(provider).toHaveProperty('provider_type');
|
|
|
|
// Cleanup: delete the created provider
|
|
await page.request.delete(`/api/v1/dns-providers/${created.id}`, {
|
|
headers: buildAuthHeaders(token),
|
|
});
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
test.describe('DNS Provider Form Accessibility', () => {
|
|
test('should have accessible form labels', async ({ page }) => {
|
|
await page.goto('/dns/providers');
|
|
await waitForLoadingComplete(page);
|
|
|
|
await page.getByRole('button', { name: /add.*provider/i }).first().click();
|
|
await waitForDialog(page);
|
|
|
|
await test.step('Verify name field has label', async () => {
|
|
// Input has id="provider-name" and associated label
|
|
const nameInput = page.locator('#provider-name');
|
|
await expect(nameInput).toBeVisible();
|
|
});
|
|
|
|
await test.step('Verify type selector is accessible', async () => {
|
|
// Select trigger has id="provider-type" and aria-label
|
|
const typeSelect = page.locator('#provider-type');
|
|
await expect(typeSelect).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test('should support keyboard navigation in form', async ({ page }) => {
|
|
await page.goto('/dns/providers');
|
|
await waitForLoadingComplete(page);
|
|
|
|
await page.getByRole('button', { name: /add.*provider/i }).first().click();
|
|
await waitForDialog(page);
|
|
|
|
await test.step('Tab through form fields', async () => {
|
|
// Tab should move through form fields
|
|
await page.keyboard.press('Tab');
|
|
|
|
const focusedElement = page.locator(':focus');
|
|
await expect(focusedElement).toBeVisible();
|
|
});
|
|
|
|
await test.step('Arrow keys should work in dropdown', async () => {
|
|
const typeSelect = page.locator('#provider-type');
|
|
|
|
if (await typeSelect.isVisible()) {
|
|
await typeSelect.focus();
|
|
await typeSelect.press('Enter');
|
|
|
|
// Arrow down should move through options
|
|
await page.keyboard.press('ArrowDown');
|
|
|
|
const focusedOption = page.locator('[role="option"]:focus, [aria-selected="true"]');
|
|
// An option should be focused or selected
|
|
expect((await focusedOption.count()) >= 0).toBeTruthy();
|
|
}
|
|
});
|
|
});
|
|
|
|
test('should announce errors to screen readers', async ({ page }) => {
|
|
await page.goto('/dns/providers');
|
|
await waitForLoadingComplete(page);
|
|
|
|
await page.getByRole('button', { name: /add.*provider/i }).first().click();
|
|
await waitForDialog(page);
|
|
|
|
await test.step('Fill form with valid data then test', async () => {
|
|
// Fill required fields to enable the button
|
|
await page.locator('#provider-name').fill('Test Provider');
|
|
await page.locator('#provider-type').click();
|
|
await page.getByRole('option', { name: /manual/i }).click();
|
|
});
|
|
|
|
await test.step('Verify error is in accessible element', async () => {
|
|
const errorElement = page
|
|
.locator('[role="alert"]')
|
|
.or(page.locator('[aria-live="polite"], [aria-live="assertive"]'));
|
|
|
|
// Error should be announced via ARIA
|
|
await expect(errorElement).toBeVisible({ timeout: 3000 }).catch(() => {
|
|
// Might use different error display
|
|
});
|
|
});
|
|
});
|
|
});
|