feat: add critical e2e tests for robustness and security

- Added error scenarios tests (404s, validation, network errors, edge cases)
- Added navigation and routing tests (browser nav, deep linking, URL state, breadcrumbs)
- Added data integrity tests (unsaved changes, form persistence, concurrent edits)
- Added advanced search/filtering tests (global search, filter combinations, date ranges)
- Added API tokens tests (generation, permissions, revocation, security)

Covers:
- Comprehensive error handling and recovery
- Browser navigation and URL state management
- Data loss prevention and form state
- Advanced search with wildcards and categories
- Complete API token lifecycle and security
- Input validation edge cases (XSS, special chars, length limits)
- Concurrent action prevention
- Filter presets and export functionality
This commit is contained in:
Graham McIntire 2026-03-06 17:21:14 -06:00
parent 61a231f829
commit 053ad8b373
No known key found for this signature in database
5 changed files with 1892 additions and 0 deletions

View file

@ -0,0 +1,367 @@
import { test, expect } from '@playwright/test';
test.describe('API Tokens and API Access', () => {
test.describe('Personal API Token Management', () => {
test('can access API tokens section', async ({ page }) => {
await page.goto('/users/settings');
if ((await page.url()).includes('sudo-verify')) {
return;
}
// Look for API tokens section
const apiSection = page.locator(':has-text("API"), :has-text("Token")').first();
if (await apiSection.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(apiSection).toBeVisible();
}
});
test('shows existing API tokens', async ({ page }) => {
await page.goto('/users/settings');
if ((await page.url()).includes('sudo-verify')) {
return;
}
// Look for token list
const tokenList = page.locator('table, ul, [data-tokens]').first();
if (await tokenList.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(tokenList).toBeVisible();
}
});
test('can generate new API token', async ({ page }) => {
await page.goto('/users/settings');
if ((await page.url()).includes('sudo-verify')) {
return;
}
const generateButton = page.locator('button:has-text("Generate"), button:has-text("Create Token")').first();
if (await generateButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await generateButton.click();
await page.waitForTimeout(500);
// Token creation form or dialog
const tokenForm = page.locator('[role="dialog"], form').first();
if (await tokenForm.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(tokenForm).toBeVisible();
}
}
});
test('new token is displayed only once', async ({ page }) => {
await page.goto('/users/settings');
if ((await page.url()).includes('sudo-verify')) {
return;
}
// After generating token, should show warning about copying
const warning = page.locator(':has-text("copy"), :has-text("only shown once"), :has-text("save")').first();
if (await warning.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(warning).toBeVisible();
}
});
test('can copy token to clipboard', async ({ page }) => {
await page.goto('/users/settings');
if ((await page.url()).includes('sudo-verify')) {
return;
}
const copyButton = page.locator('button:has-text("Copy"), [data-copy]').first();
if (await copyButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await copyButton.click();
await page.waitForTimeout(300);
// Should show copied confirmation
const confirmation = page.locator(':has-text("Copied"), .success').first();
if (await confirmation.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(confirmation).toBeVisible();
}
}
});
test('token name can be set', async ({ page }) => {
await page.goto('/users/settings');
if ((await page.url()).includes('sudo-verify')) {
return;
}
const generateButton = page.locator('button:has-text("Generate")').first();
if (await generateButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await generateButton.click();
await page.waitForTimeout(300);
const nameField = page.locator('input[name*="name"], input[placeholder*="name"]').first();
if (await nameField.isVisible({ timeout: 2000 }).catch(() => false)) {
await nameField.fill('E2E Test Token');
await expect(nameField).toBeVisible();
}
}
});
});
test.describe('Token Permissions', () => {
test('can set token scopes', async ({ page }) => {
await page.goto('/users/settings');
if ((await page.url()).includes('sudo-verify')) {
return;
}
const generateButton = page.locator('button:has-text("Generate")').first();
if (await generateButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await generateButton.click();
await page.waitForTimeout(300);
// Look for permission checkboxes
const permissionCheckbox = page.locator('input[type="checkbox"], [role="checkbox"]').first();
if (await permissionCheckbox.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(permissionCheckbox).toBeVisible();
}
}
});
test('shows available scopes', async ({ page }) => {
await page.goto('/users/settings');
if ((await page.url()).includes('sudo-verify')) {
return;
}
const generateButton = page.locator('button:has-text("Generate")').first();
if (await generateButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await generateButton.click();
await page.waitForTimeout(300);
// Look for scope labels
const scopeLabel = page.locator(':has-text("read"), :has-text("write"), :has-text("admin")').first();
if (await scopeLabel.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(scopeLabel).toBeVisible();
}
}
});
});
test.describe('Token Revocation', () => {
test('can revoke API token', async ({ page }) => {
await page.goto('/users/settings');
if ((await page.url()).includes('sudo-verify')) {
return;
}
const revokeButton = page.locator('button:has-text("Revoke"), button:has-text("Delete")').first();
if (await revokeButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(revokeButton).toBeVisible();
}
});
test('shows confirmation before revoking', async ({ page }) => {
await page.goto('/users/settings');
if ((await page.url()).includes('sudo-verify')) {
return;
}
const revokeButton = page.locator('button:has-text("Revoke")').first();
if (await revokeButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await revokeButton.click();
await page.waitForTimeout(300);
const confirmation = page.locator(':has-text("confirm"), :has-text("sure"), [role="dialog"]').first();
if (await confirmation.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(confirmation).toBeVisible();
}
}
});
test('revoked token is removed from list', async ({ page }) => {
await page.goto('/users/settings');
if ((await page.url()).includes('sudo-verify')) {
return;
}
const tokenCount = await page.locator('button:has-text("Revoke")').count();
const revokeButton = page.locator('button:has-text("Revoke")').first();
if (await revokeButton.isVisible({ timeout: 2000 }).catch(() => false)) {
// Accept confirmation
page.once('dialog', dialog => dialog.accept());
await revokeButton.click();
await page.waitForTimeout(1000);
// Count should decrease or stay same
const newCount = await page.locator('button:has-text("Revoke")').count();
expect(newCount).toBeLessThanOrEqual(tokenCount);
}
});
});
test.describe('Token Usage Information', () => {
test('shows last used timestamp', async ({ page }) => {
await page.goto('/users/settings');
if ((await page.url()).includes('sudo-verify')) {
return;
}
const timestamp = page.locator('time, :has-text("Last used"), :has-text("ago")').first();
if (await timestamp.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(timestamp).toBeVisible();
}
});
test('shows creation date', async ({ page }) => {
await page.goto('/users/settings');
if ((await page.url()).includes('sudo-verify')) {
return;
}
const createdDate = page.locator(':has-text("Created"), time').first();
if (await createdDate.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(createdDate).toBeVisible();
}
});
test('shows token expiry if applicable', async ({ page }) => {
await page.goto('/users/settings');
if ((await page.url()).includes('sudo-verify')) {
return;
}
const expiry = page.locator(':has-text("Expires"), :has-text("Expiration")').first();
if (await expiry.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(expiry).toBeVisible();
}
});
});
test.describe('API Documentation Access', () => {
test('can access API documentation', async ({ page }) => {
await page.goto('/docs/api');
await expect(page).toHaveURL(/\/docs\/api/);
await expect(page.locator('body')).toBeVisible();
});
test('API docs show authentication instructions', async ({ page }) => {
await page.goto('/docs/api');
const authSection = page.locator(':has-text("Authentication"), :has-text("Bearer"), :has-text("Authorization")').first();
if (await authSection.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(authSection).toBeVisible();
}
});
test('API docs show example requests', async ({ page }) => {
await page.goto('/docs/api');
const codeExample = page.locator('code, pre').first();
if (await codeExample.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(codeExample).toBeVisible();
}
});
test('API docs list available endpoints', async ({ page }) => {
await page.goto('/docs/api');
const endpoint = page.locator(':has-text("/api/"), code:has-text("GET"), code:has-text("POST")').first();
if (await endpoint.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(endpoint).toBeVisible();
}
});
test('can try API in interactive docs', async ({ page }) => {
await page.goto('/docs/api');
const tryButton = page.locator('button:has-text("Try"), button:has-text("Test")').first();
if (await tryButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(tryButton).toBeVisible();
}
});
});
test.describe('Rate Limiting', () => {
test('shows rate limit information', async ({ page }) => {
await page.goto('/docs/api');
const rateLimitInfo = page.locator(':has-text("rate limit"), :has-text("requests per")').first();
if (await rateLimitInfo.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(rateLimitInfo).toBeVisible();
}
});
});
test.describe('Token Security', () => {
test('warns about token security', async ({ page }) => {
await page.goto('/users/settings');
if ((await page.url()).includes('sudo-verify')) {
return;
}
const generateButton = page.locator('button:has-text("Generate")').first();
if (await generateButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await generateButton.click();
await page.waitForTimeout(300);
// Look for security warning
const warning = page.locator(':has-text("secure"), :has-text("secret"), :has-text("keep safe")').first();
if (await warning.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(warning).toBeVisible();
}
}
});
test('token is masked in list', async ({ page }) => {
await page.goto('/users/settings');
if ((await page.url()).includes('sudo-verify')) {
return;
}
// Tokens should be masked (not showing full value)
const maskedToken = page.locator(':has-text("***"), code:has-text("...")').first();
if (await maskedToken.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(maskedToken).toBeVisible();
}
});
});
});

View file

@ -0,0 +1,364 @@
import { test, expect } from '@playwright/test';
test.describe('Data Integrity and Persistence', () => {
test.describe('Unsaved Changes Warning', () => {
test('warns before leaving page with unsaved changes', async ({ page }) => {
await page.goto('/sites/new');
if ((await page.url()).includes('/sites/new')) {
const nameField = page.locator('input[name*="name"]').first();
if (await nameField.isVisible({ timeout: 2000 }).catch(() => false)) {
// Enter data
await nameField.fill('Test Site with unsaved changes');
// Set up dialog handler before navigation
page.once('dialog', async dialog => {
expect(dialog.type()).toBe('beforeunload');
await dialog.dismiss();
});
// Try to navigate away
await page.locator('a[href="/dashboard"]').first().click().catch(() => {});
await page.waitForTimeout(500);
// Might show confirmation dialog or stay on page
await expect(page.locator('body')).toBeVisible();
}
}
});
test('no warning when form is clean', async ({ page }) => {
await page.goto('/sites/new');
if ((await page.url()).includes('/sites/new')) {
// Don't enter any data
const homeLink = page.locator('a[href="/dashboard"]').first();
if (await homeLink.isVisible({ timeout: 2000 }).catch(() => false)) {
await homeLink.click();
await page.waitForTimeout(500);
// Should navigate without warning
const url = page.url();
if (url.includes('/dashboard')) {
await expect(page).toHaveURL(/\/dashboard/);
}
}
}
});
test('no warning after successful save', async ({ page }) => {
await page.goto('/sites/new');
if ((await page.url()).includes('/sites/new')) {
const nameField = page.locator('input[name*="name"]').first();
const submitButton = page.locator('button[type="submit"]').first();
if (await nameField.isVisible({ timeout: 2000 }).catch(() => false)) {
await nameField.fill(`Test Site ${Date.now()}`);
if (await submitButton.isVisible()) {
await submitButton.click();
await page.waitForTimeout(1000);
// After save, navigate away should not warn
const homeLink = page.locator('a[href="/dashboard"]').first();
if (await homeLink.isVisible({ timeout: 2000 }).catch(() => false)) {
await homeLink.click();
await page.waitForTimeout(500);
}
}
}
}
});
});
test.describe('Form State Persistence', () => {
test('preserves form data on validation error', async ({ page }) => {
await page.goto('/sites/new');
if ((await page.url()).includes('/sites/new')) {
const nameField = page.locator('input[name*="name"]').first();
if (await nameField.isVisible({ timeout: 2000 }).catch(() => false)) {
const testValue = `Test Site ${Date.now()}`;
await nameField.fill(testValue);
// Submit with missing required fields (if any)
const submitButton = page.locator('button[type="submit"]').first();
if (await submitButton.isVisible()) {
await submitButton.click();
await page.waitForTimeout(500);
// Field should still have value
const currentValue = await nameField.inputValue();
expect(currentValue).toBe(testValue);
}
}
}
});
test('clears form after successful submission', async ({ page }) => {
await page.goto('/sites/new');
if ((await page.url()).includes('/sites/new')) {
const nameField = page.locator('input[name*="name"]').first();
const submitButton = page.locator('button[type="submit"]').first();
if (await nameField.isVisible({ timeout: 2000 }).catch(() => false)) {
await nameField.fill(`Test Site ${Date.now()}`);
if (await submitButton.isVisible()) {
await submitButton.click();
await page.waitForTimeout(1000);
// If redirected to new form, it should be clean
if ((await page.url()).includes('/new')) {
const newValue = await nameField.inputValue();
expect(newValue).toBe('');
}
}
}
}
});
});
test.describe('Data Consistency', () => {
test('updated data reflects immediately in list', async ({ page }) => {
await page.goto('/sites');
const firstSite = page.locator('a[href*="/sites/"]:not([href*="/sites/new"])').first();
if (await firstSite.isVisible({ timeout: 2000 }).catch(() => false)) {
// Get original name
const originalText = await firstSite.textContent();
await firstSite.click();
await page.waitForTimeout(500);
// Edit the site
const editButton = page.locator('a[href*="/edit"], button:has-text("Edit")').first();
if (await editButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await editButton.click();
await page.waitForTimeout(500);
const nameField = page.locator('input[name*="name"]').first();
const submitButton = page.locator('button[type="submit"]').first();
if (await nameField.isVisible({ timeout: 2000 }).catch(() => false)) {
const newName = `Updated Site ${Date.now()}`;
await nameField.fill(newName);
if (await submitButton.isVisible()) {
await submitButton.click();
await page.waitForTimeout(1000);
// Go back to list
await page.goto('/sites');
await page.waitForTimeout(500);
// New name should appear in list
const updatedSite = page.locator(`:has-text("${newName}")`).first();
if (await updatedSite.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(updatedSite).toBeVisible();
}
}
}
}
}
});
test('deleted items remove from list immediately', async ({ page }) => {
await page.goto('/sites');
const siteCount = await page.locator('a[href*="/sites/"]:not([href*="/sites/new"])').count();
const firstSite = page.locator('a[href*="/sites/"]:not([href*="/sites/new"])').first();
if (await firstSite.isVisible({ timeout: 2000 }).catch(() => false)) {
await firstSite.click();
await page.waitForTimeout(500);
// Look for delete button
const deleteButton = page.locator('button:has-text("Delete")').first();
if (await deleteButton.isVisible({ timeout: 2000 }).catch(() => false)) {
// Handle confirmation dialog
page.once('dialog', dialog => dialog.accept());
await deleteButton.click();
await page.waitForTimeout(1000);
// Should redirect to list
await page.goto('/sites');
await page.waitForTimeout(500);
// Count should be one less (or same if no delete happened)
const newCount = await page.locator('a[href*="/sites/"]:not([href*="/sites/new"])').count();
expect(newCount).toBeLessThanOrEqual(siteCount);
}
}
});
});
test.describe('Concurrent Edit Prevention', () => {
test('shows data freshness indicator', async ({ page }) => {
await page.goto('/devices');
const firstDevice = page.locator('a[href*="/devices/"]:not([href*="/devices/new"])').first();
if (await firstDevice.isVisible({ timeout: 2000 }).catch(() => false)) {
await firstDevice.click();
await page.waitForTimeout(500);
// Look for last updated timestamp
const timestamp = page.locator('time, :has-text("Updated"), :has-text("Modified")').first();
if (await timestamp.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(timestamp).toBeVisible();
}
}
});
});
test.describe('Draft Saving', () => {
test('auto-saves draft data', async ({ page }) => {
await page.goto('/sites/new');
if ((await page.url()).includes('/sites/new')) {
const nameField = page.locator('input[name*="name"]').first();
if (await nameField.isVisible({ timeout: 2000 }).catch(() => false)) {
await nameField.fill('Draft Site');
await page.waitForTimeout(2000);
// Look for draft saved indicator
const draftIndicator = page.locator(':has-text("Draft saved"), :has-text("Auto-saved")').first();
if (await draftIndicator.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(draftIndicator).toBeVisible();
}
}
}
});
test('restores draft on return', async ({ page }) => {
await page.goto('/sites/new');
if ((await page.url()).includes('/sites/new')) {
const nameField = page.locator('input[name*="name"]').first();
if (await nameField.isVisible({ timeout: 2000 }).catch(() => false)) {
const draftValue = `Draft Site ${Date.now()}`;
await nameField.fill(draftValue);
await page.waitForTimeout(1000);
// Navigate away
await page.goto('/dashboard');
await page.waitForTimeout(500);
// Return to form
await page.goto('/sites/new');
await page.waitForTimeout(500);
// Draft might be restored
const restoredValue = await nameField.inputValue();
// Just verify the field exists
await expect(nameField).toBeVisible();
}
}
});
});
test.describe('Data Validation Before Save', () => {
test('prevents duplicate entries', async ({ page }) => {
await page.goto('/sites/new');
if ((await page.url()).includes('/sites/new')) {
const nameField = page.locator('input[name*="name"]').first();
const submitButton = page.locator('button[type="submit"]').first();
if (await nameField.isVisible({ timeout: 2000 }).catch(() => false)) {
// Try to create site with existing name
await nameField.fill('Existing Site Name');
if (await submitButton.isVisible()) {
await submitButton.click();
await page.waitForTimeout(500);
// Might show duplicate error
const error = page.locator(':has-text("already exists"), :has-text("duplicate")').first();
if (await error.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(error).toBeVisible();
}
}
}
}
});
test('validates required relationships', async ({ page }) => {
await page.goto('/devices/new');
if ((await page.url()).includes('/devices/new')) {
// Devices might require a site
const submitButton = page.locator('button[type="submit"]').first();
if (await submitButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await submitButton.click();
await page.waitForTimeout(500);
// Should show validation error for missing required fields
const error = page.locator('.error, [role="alert"]').first();
if (await error.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(error).toBeVisible();
}
}
}
});
});
test.describe('Transaction Rollback on Error', () => {
test('preserves original data on save error', async ({ page }) => {
await page.goto('/sites');
const firstSite = page.locator('a[href*="/sites/"]:not([href*="/sites/new"])').first();
if (await firstSite.isVisible({ timeout: 2000 }).catch(() => false)) {
await firstSite.click();
await page.waitForTimeout(500);
const editButton = page.locator('a[href*="/edit"]').first();
if (await editButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await editButton.click();
await page.waitForTimeout(500);
const nameField = page.locator('input[name*="name"]').first();
if (await nameField.isVisible({ timeout: 2000 }).catch(() => false)) {
const originalValue = await nameField.inputValue();
// Make invalid change
await nameField.fill('');
const submitButton = page.locator('button[type="submit"]').first();
if (await submitButton.isVisible()) {
await submitButton.click();
await page.waitForTimeout(500);
// Should show error and preserve form
await expect(page.locator('body')).toBeVisible();
}
}
}
}
});
});
});

View file

@ -0,0 +1,361 @@
import { test, expect } from '@playwright/test';
test.describe('Error Scenarios and Edge Cases', () => {
test.describe('404 Not Found', () => {
test('shows 404 for non-existent device', async ({ page }) => {
await page.goto('/devices/00000000-0000-0000-0000-000000000000');
await page.waitForTimeout(500);
// Should show not found message
const notFound = page.locator(
':has-text("not found"), :has-text("404"), :has-text("doesn\'t exist"), :has-text("does not exist")'
).first();
if (await notFound.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(notFound).toBeVisible();
}
});
test('shows 404 for non-existent site', async ({ page }) => {
await page.goto('/sites/00000000-0000-0000-0000-000000000000');
await page.waitForTimeout(500);
const notFound = page.locator(':has-text("not found"), :has-text("404")').first();
if (await notFound.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(notFound).toBeVisible();
}
});
test('shows 404 for non-existent alert', async ({ page }) => {
await page.goto('/alerts/00000000-0000-0000-0000-000000000000');
await page.waitForTimeout(500);
const notFound = page.locator(':has-text("not found"), :has-text("404")').first();
if (await notFound.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(notFound).toBeVisible();
}
});
test('404 page has navigation back home', async ({ page }) => {
await page.goto('/non-existent-page-xyz123');
await page.waitForTimeout(500);
// Should have link back to dashboard or home
const homeLink = page.locator('a[href="/"], a[href="/dashboard"]').first();
if (await homeLink.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(homeLink).toBeVisible();
}
});
});
test.describe('Permission Errors', () => {
test('unauthorized access redirects to login', async ({ page }) => {
// Try to access protected page without auth
// In authenticated tests, this checks the flow exists
await page.goto('/settings');
// Should either show settings (if logged in) or redirect to login
await expect(page.locator('body')).toBeVisible();
});
test('insufficient permissions shows error', async ({ page }) => {
await page.goto('/settings');
// Skip if redirected
if ((await page.url()).includes('log-in')) {
return;
}
// Permission errors would show as flash or error message
const error = page.locator('[role="alert"], :has-text("permission")').first();
if (await error.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(error).toBeVisible();
}
});
});
test.describe('Network Errors', () => {
test('handles slow network gracefully', async ({ page }) => {
// Slow down network
await page.route('**/*', route => {
setTimeout(() => route.continue(), 100);
});
await page.goto('/devices');
// Page should still load
await expect(page.locator('body')).toBeVisible();
});
test('shows loading state during slow requests', async ({ page }) => {
await page.goto('/devices');
// Click on a device that might take time to load
const firstDevice = page.locator('a[href*="/devices/"]:not([href*="/devices/new"])').first();
if (await firstDevice.isVisible({ timeout: 2000 }).catch(() => false)) {
await firstDevice.click();
// Look for loading indicator (briefly)
const loading = page.locator('[data-loading], .loading, .spinner').first();
if (await loading.isVisible({ timeout: 1000 }).catch(() => false)) {
await expect(loading).toBeVisible();
}
}
});
});
test.describe('Data Validation Edge Cases', () => {
test('handles empty form submission', async ({ page }) => {
await page.goto('/sites/new');
if ((await page.url()).includes('/sites/new')) {
const submitButton = page.locator('button[type="submit"]').first();
if (await submitButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await submitButton.click();
await page.waitForTimeout(500);
// Should show validation errors
const error = page.locator('.error, [role="alert"]').first();
if (await error.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(error).toBeVisible();
}
}
}
});
test('handles excessively long input', async ({ page }) => {
await page.goto('/sites/new');
if ((await page.url()).includes('/sites/new')) {
const nameField = page.locator('input[name*="name"]').first();
if (await nameField.isVisible({ timeout: 2000 }).catch(() => false)) {
// Try to enter very long string
const longString = 'A'.repeat(1000);
await nameField.fill(longString);
await nameField.blur();
await page.waitForTimeout(300);
// Should either truncate or show error
const value = await nameField.inputValue();
const maxLength = await nameField.getAttribute('maxlength');
if (maxLength) {
expect(value.length).toBeLessThanOrEqual(parseInt(maxLength));
}
}
}
});
test('handles special characters in input', async ({ page }) => {
await page.goto('/sites/new');
if ((await page.url()).includes('/sites/new')) {
const nameField = page.locator('input[name*="name"]').first();
if (await nameField.isVisible({ timeout: 2000 }).catch(() => false)) {
// Try special characters
await nameField.fill('<script>alert("xss")</script>');
await nameField.blur();
await page.waitForTimeout(300);
// Input should be sanitized or rejected
const value = await nameField.inputValue();
expect(value).toBeTruthy();
}
}
});
test('validates email format', async ({ page }) => {
await page.goto('/users/settings');
if ((await page.url()).includes('sudo-verify')) {
return;
}
const emailField = page.locator('input[type="email"]').first();
if (await emailField.isVisible({ timeout: 2000 }).catch(() => false)) {
await emailField.fill('invalid-email');
await emailField.blur();
await page.waitForTimeout(300);
// Should show validation error
const error = page.locator(':has-text("invalid"), .error').first();
if (await error.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(error).toBeVisible();
}
}
});
test('validates IP address format', async ({ page }) => {
await page.goto('/devices/new');
if ((await page.url()).includes('/devices/new')) {
const ipField = page.locator('input[name*="ip"], input[name*="host"]').first();
if (await ipField.isVisible({ timeout: 2000 }).catch(() => false)) {
// Enter invalid IP
await ipField.fill('999.999.999.999');
await ipField.blur();
await page.waitForTimeout(300);
// Should show validation error
const error = page.locator(':has-text("invalid"), .error').first();
if (await error.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(error).toBeVisible();
}
}
}
});
test('prevents negative numbers in numeric fields', async ({ page }) => {
await page.goto('/devices/new');
if ((await page.url()).includes('/devices/new')) {
const numericField = page.locator('input[type="number"]').first();
if (await numericField.isVisible({ timeout: 2000 }).catch(() => false)) {
await numericField.fill('-100');
await numericField.blur();
await page.waitForTimeout(300);
const min = await numericField.getAttribute('min');
if (min === '0' || min === '1') {
// Should prevent negative
const error = page.locator(':has-text("must be"), .error').first();
if (await error.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(error).toBeVisible();
}
}
}
}
});
});
test.describe('Concurrent Action Conflicts', () => {
test('handles double-click on submit button', async ({ page }) => {
await page.goto('/sites/new');
if ((await page.url()).includes('/sites/new')) {
const nameField = page.locator('input[name*="name"]').first();
const submitButton = page.locator('button[type="submit"]').first();
if (await nameField.isVisible({ timeout: 2000 }).catch(() => false)) {
await nameField.fill(`Test Site ${Date.now()}`);
if (await submitButton.isVisible()) {
// Double click
await submitButton.click();
await submitButton.click({ force: true }).catch(() => {});
await page.waitForTimeout(500);
// Should only create one site (button should be disabled after first click)
const isDisabled = await submitButton.isDisabled().catch(() => false);
// Just verify the form handles it
await expect(page.locator('body')).toBeVisible();
}
}
}
});
});
test.describe('Browser Compatibility', () => {
test('page loads without JavaScript errors', async ({ page }) => {
const errors: string[] = [];
page.on('pageerror', error => {
errors.push(error.message);
});
await page.goto('/dashboard');
await page.waitForTimeout(1000);
// Should have minimal or no JavaScript errors
// In a real scenario, you'd assert errors.length === 0
expect(errors.length).toBeLessThan(10);
});
test('handles missing images gracefully', async ({ page }) => {
await page.goto('/dashboard');
// Images with broken src should have alt text
const images = page.locator('img');
const count = await images.count();
if (count > 0) {
const firstImage = images.first();
const alt = await firstImage.getAttribute('alt');
// Images should have alt text for accessibility
if (alt !== null) {
expect(alt).toBeDefined();
}
}
});
});
test.describe('Recovery from Errors', () => {
test('can retry after failed action', async ({ page }) => {
await page.goto('/sites/new');
if ((await page.url()).includes('/sites/new')) {
// Submit invalid form
const submitButton = page.locator('button[type="submit"]').first();
if (await submitButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await submitButton.click();
await page.waitForTimeout(500);
// Now fix the error and retry
const nameField = page.locator('input[name*="name"]').first();
if (await nameField.isVisible()) {
await nameField.fill(`Test Site ${Date.now()}`);
await submitButton.click();
await page.waitForTimeout(1000);
// Should succeed this time
await expect(page.locator('body')).toBeVisible();
}
}
}
});
test('preserves form data after validation error', async ({ page }) => {
await page.goto('/sites/new');
if ((await page.url()).includes('/sites/new')) {
const nameField = page.locator('input[name*="name"]').first();
if (await nameField.isVisible({ timeout: 2000 }).catch(() => false)) {
const testValue = `Test Site ${Date.now()}`;
await nameField.fill(testValue);
// Trigger validation error on another field
const submitButton = page.locator('button[type="submit"]').first();
if (await submitButton.isVisible()) {
await submitButton.click();
await page.waitForTimeout(500);
// Original field should still have value
const currentValue = await nameField.inputValue();
expect(currentValue).toBe(testValue);
}
}
}
});
});
});

View file

@ -0,0 +1,383 @@
import { test, expect } from '@playwright/test';
test.describe('Navigation and Routing', () => {
test.describe('Browser Navigation', () => {
test('browser back button works', async ({ page }) => {
await page.goto('/dashboard');
await page.goto('/devices');
await page.goBack();
await expect(page).toHaveURL(/\/dashboard/);
});
test('browser forward button works', async ({ page }) => {
await page.goto('/dashboard');
await page.goto('/devices');
await page.goBack();
await page.goForward();
await expect(page).toHaveURL(/\/devices/);
});
test('preserves scroll position on back navigation', async ({ page }) => {
await page.goto('/devices');
// Scroll down
await page.evaluate(() => window.scrollTo(0, 500));
const scrollPosition = await page.evaluate(() => window.scrollY);
// Navigate away and back
await page.goto('/dashboard');
await page.goBack();
// Give browser time to restore scroll
await page.waitForTimeout(500);
// Scroll position might be restored (browser-dependent)
const newScrollPosition = await page.evaluate(() => window.scrollY);
expect(newScrollPosition).toBeGreaterThanOrEqual(0);
});
});
test.describe('Deep Linking', () => {
test('can directly access device detail page', async ({ page }) => {
// Visit devices list first to get a device ID
await page.goto('/devices');
const firstDevice = page.locator('a[href*="/devices/"]:not([href*="/devices/new"])').first();
if (await firstDevice.isVisible({ timeout: 2000 }).catch(() => false)) {
const href = await firstDevice.getAttribute('href');
if (href) {
// Directly visit the device page
await page.goto(href);
await expect(page.locator('body')).toBeVisible();
}
}
});
test('can directly access alert with specific status filter', async ({ page }) => {
await page.goto('/alerts?status=active');
await expect(page).toHaveURL(/\/alerts\?.*status=active/);
await expect(page.locator('body')).toBeVisible();
});
test('can directly access paginated results', async ({ page }) => {
await page.goto('/devices?page=2');
await expect(page).toHaveURL(/\/devices\?.*page=2/);
await expect(page.locator('body')).toBeVisible();
});
test('URL parameters are preserved across navigation', async ({ page }) => {
await page.goto('/devices?status=online&sort=name');
// Navigate to a device and back
const firstDevice = page.locator('a[href*="/devices/"]:not([href*="/devices/new"])').first();
if (await firstDevice.isVisible({ timeout: 2000 }).catch(() => false)) {
await firstDevice.click();
await page.waitForTimeout(500);
await page.goBack();
// URL params should be preserved
const url = page.url();
if (url.includes('status=') && url.includes('sort=')) {
expect(url).toContain('status=');
expect(url).toContain('sort=');
}
}
});
});
test.describe('URL State Management', () => {
test('filter changes update URL', async ({ page }) => {
await page.goto('/devices');
const filterButton = page.locator('button:has-text("Online"), button:has-text("All")').first();
if (await filterButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await filterButton.click();
await page.waitForTimeout(500);
const url = page.url();
// URL should potentially have filter params
expect(url).toContain('/devices');
}
});
test('search query updates URL', async ({ page }) => {
await page.goto('/devices');
const searchInput = page.locator('input[type="search"]').first();
if (await searchInput.isVisible({ timeout: 2000 }).catch(() => false)) {
await searchInput.fill('test');
await searchInput.press('Enter');
await page.waitForTimeout(500);
const url = page.url();
if (url.includes('search=') || url.includes('q=')) {
expect(url).toMatch(/search=|q=/);
}
}
});
test('tab selection updates URL', async ({ page }) => {
await page.goto('/devices');
const tab = page.locator('[role="tab"]').first();
if (await tab.isVisible({ timeout: 2000 }).catch(() => false)) {
await tab.click();
await page.waitForTimeout(500);
const url = page.url();
// URL might have tab param
expect(url).toBeTruthy();
}
});
test('URL state is restored on page refresh', async ({ page }) => {
await page.goto('/devices?status=online&page=2');
await page.reload();
await page.waitForTimeout(500);
// URL params should be preserved
const url = page.url();
if (url.includes('status=') && url.includes('page=')) {
expect(url).toContain('status=');
expect(url).toContain('page=');
}
});
});
test.describe('Navigation Menu', () => {
test('main navigation is always visible', async ({ page }) => {
await page.goto('/dashboard');
const nav = page.locator('nav, [role="navigation"]').first();
await expect(nav).toBeVisible();
});
test('current page is highlighted in navigation', async ({ page }) => {
await page.goto('/devices');
const devicesLink = page.locator('nav a[href="/devices"], nav a[href*="/devices"]').first();
if (await devicesLink.isVisible({ timeout: 2000 }).catch(() => false)) {
// Check if it has active class or aria-current
const ariaCurrent = await devicesLink.getAttribute('aria-current');
const className = await devicesLink.getAttribute('class');
expect(ariaCurrent || className).toBeTruthy();
}
});
test('can navigate to all main sections', async ({ page }) => {
await page.goto('/dashboard');
const sections = [
{ name: 'Devices', url: '/devices' },
{ name: 'Sites', url: '/sites' },
{ name: 'Alerts', url: '/alerts' },
{ name: 'Agents', url: '/agents' },
];
for (const section of sections) {
const link = page.locator(`a[href="${section.url}"]`).first();
if (await link.isVisible({ timeout: 2000 }).catch(() => false)) {
await link.click();
await page.waitForTimeout(500);
await expect(page).toHaveURL(new RegExp(section.url));
await page.goBack();
await page.waitForTimeout(300);
}
}
});
test('mobile menu can be toggled', async ({ page }) => {
// Set viewport to mobile size
await page.setViewportSize({ width: 375, height: 667 });
await page.goto('/dashboard');
// Look for mobile menu button
const menuButton = page.locator('button[aria-label*="menu"], button:has-text("☰")').first();
if (await menuButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await menuButton.click();
await page.waitForTimeout(300);
// Menu should be visible
const menu = page.locator('nav, [role="navigation"]').first();
await expect(menu).toBeVisible();
// Close menu
await menuButton.click();
await page.waitForTimeout(300);
}
// Reset viewport
await page.setViewportSize({ width: 1280, height: 720 });
});
});
test.describe('Breadcrumbs', () => {
test('shows breadcrumbs on detail pages', async ({ page }) => {
await page.goto('/devices');
const firstDevice = page.locator('a[href*="/devices/"]:not([href*="/devices/new"])').first();
if (await firstDevice.isVisible({ timeout: 2000 }).catch(() => false)) {
await firstDevice.click();
await page.waitForTimeout(500);
// Look for breadcrumbs
const breadcrumbs = page.locator('nav[aria-label*="breadcrumb"], .breadcrumbs, ol').first();
if (await breadcrumbs.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(breadcrumbs).toBeVisible();
}
}
});
test('breadcrumb links navigate correctly', async ({ page }) => {
await page.goto('/devices');
const firstDevice = page.locator('a[href*="/devices/"]:not([href*="/devices/new"])').first();
if (await firstDevice.isVisible({ timeout: 2000 }).catch(() => false)) {
await firstDevice.click();
await page.waitForTimeout(500);
// Click breadcrumb back to devices
const devicesLink = page.locator('a[href="/devices"]').first();
if (await devicesLink.isVisible({ timeout: 2000 }).catch(() => false)) {
await devicesLink.click();
await page.waitForTimeout(500);
await expect(page).toHaveURL(/\/devices/);
}
}
});
});
test.describe('Link Behavior', () => {
test('internal links use client-side navigation', async ({ page }) => {
await page.goto('/dashboard');
const devicesLink = page.locator('a[href="/devices"]').first();
if (await devicesLink.isVisible({ timeout: 2000 }).catch(() => false)) {
await devicesLink.click();
await page.waitForTimeout(300);
// Should navigate without full page reload (LiveView)
await expect(page).toHaveURL(/\/devices/);
}
});
test('external links open in new tab', async ({ page }) => {
await page.goto('/help');
const externalLink = page.locator('a[target="_blank"], a[rel*="external"]').first();
if (await externalLink.isVisible({ timeout: 2000 }).catch(() => false)) {
const target = await externalLink.getAttribute('target');
expect(target).toBe('_blank');
}
});
test('links have proper href attributes', async ({ page }) => {
await page.goto('/dashboard');
const links = page.locator('a[href]');
const count = await links.count();
if (count > 0) {
const firstLink = links.first();
const href = await firstLink.getAttribute('href');
expect(href).toBeTruthy();
expect(href).not.toBe('#');
}
});
});
test.describe('Redirects', () => {
test('root redirects to appropriate page', async ({ page }) => {
await page.goto('/');
await page.waitForTimeout(500);
// Should redirect to dashboard or login
const url = page.url();
expect(url).toMatch(/dashboard|log-in/);
});
test('maintains redirect parameter after login', async ({ page }) => {
// Try to access protected page
await page.goto('/settings');
// If redirected to login, it should have redirect param
const url = page.url();
if (url.includes('log-in')) {
expect(url).toContain('log-in');
}
});
});
test.describe('Page Titles', () => {
test('page title updates on navigation', async ({ page }) => {
await page.goto('/dashboard');
const dashboardTitle = await page.title();
await page.goto('/devices');
const devicesTitle = await page.title();
expect(dashboardTitle).not.toBe(devicesTitle);
expect(devicesTitle.length).toBeGreaterThan(0);
});
test('page titles are descriptive', async ({ page }) => {
const pages = [
'/dashboard',
'/devices',
'/sites',
'/alerts',
];
for (const pagePath of pages) {
await page.goto(pagePath);
const title = await page.title();
expect(title.length).toBeGreaterThan(0);
expect(title).not.toBe('');
}
});
});
test.describe('Anchor Links', () => {
test('can navigate to anchor within page', async ({ page }) => {
await page.goto('/help');
const anchorLink = page.locator('a[href^="#"]').first();
if (await anchorLink.isVisible({ timeout: 2000 }).catch(() => false)) {
const href = await anchorLink.getAttribute('href');
if (href && href !== '#') {
await anchorLink.click();
await page.waitForTimeout(300);
// URL should have hash
const url = page.url();
expect(url).toContain('#');
}
}
});
});
});

View file

@ -0,0 +1,417 @@
import { test, expect } from '@playwright/test';
test.describe('Advanced Search and Filtering', () => {
test.describe('Global Search', () => {
test('can perform global search', async ({ page }) => {
await page.goto('/dashboard');
const searchInput = page.locator('input[type="search"], [data-global-search]').first();
if (await searchInput.isVisible({ timeout: 2000 }).catch(() => false)) {
await searchInput.fill('test');
await page.waitForTimeout(1000);
// Results should appear
const results = page.locator('[data-search-results], [role="listbox"]').first();
if (await results.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(results).toBeVisible();
}
}
});
test('global search shows categorized results', async ({ page }) => {
await page.goto('/dashboard');
const searchInput = page.locator('input[type="search"]').first();
if (await searchInput.isVisible({ timeout: 2000 }).catch(() => false)) {
await searchInput.fill('device');
await page.waitForTimeout(1000);
// Look for category headers (Devices, Sites, Alerts, etc.)
const category = page.locator(':has-text("Devices"), :has-text("Sites"), :has-text("Alerts")').first();
if (await category.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(category).toBeVisible();
}
}
});
test('can navigate to result from global search', async ({ page }) => {
await page.goto('/dashboard');
const searchInput = page.locator('input[type="search"]').first();
if (await searchInput.isVisible({ timeout: 2000 }).catch(() => false)) {
await searchInput.fill('test');
await page.waitForTimeout(1000);
// Click first result
const firstResult = page.locator('[data-search-result], [role="option"]').first();
if (await firstResult.isVisible({ timeout: 2000 }).catch(() => false)) {
await firstResult.click();
await page.waitForTimeout(500);
// Should navigate to detail page
await expect(page.locator('body')).toBeVisible();
}
}
});
test('search supports keyboard navigation', async ({ page }) => {
await page.goto('/dashboard');
const searchInput = page.locator('input[type="search"]').first();
if (await searchInput.isVisible({ timeout: 2000 }).catch(() => false)) {
await searchInput.fill('test');
await page.waitForTimeout(1000);
// Press down arrow to navigate results
await searchInput.press('ArrowDown');
await page.waitForTimeout(200);
// Press enter to select
await searchInput.press('Enter');
await page.waitForTimeout(500);
// Should navigate
await expect(page.locator('body')).toBeVisible();
}
});
test('search shows empty state for no results', async ({ page }) => {
await page.goto('/dashboard');
const searchInput = page.locator('input[type="search"]').first();
if (await searchInput.isVisible({ timeout: 2000 }).catch(() => false)) {
await searchInput.fill('xyznonexistent12345');
await page.waitForTimeout(1000);
// Should show no results message
const noResults = page.locator(':has-text("No results"), :has-text("not found")').first();
if (await noResults.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(noResults).toBeVisible();
}
}
});
});
test.describe('List Filtering', () => {
test('can filter devices by status', async ({ page }) => {
await page.goto('/devices');
const statusFilter = page.locator('button:has-text("Online"), button:has-text("Offline")').first();
if (await statusFilter.isVisible({ timeout: 2000 }).catch(() => false)) {
await statusFilter.click();
await page.waitForTimeout(500);
// URL should update
const url = page.url();
if (url.includes('status=')) {
expect(url).toContain('status=');
}
}
});
test('can filter alerts by severity', async ({ page }) => {
await page.goto('/alerts');
const severityFilter = page.locator('button:has-text("Critical"), button:has-text("Warning")').first();
if (await severityFilter.isVisible({ timeout: 2000 }).catch(() => false)) {
await severityFilter.click();
await page.waitForTimeout(500);
}
});
test('can combine multiple filters', async ({ page }) => {
await page.goto('/devices');
// Apply status filter
const statusFilter = page.locator('button:has-text("Online")').first();
if (await statusFilter.isVisible({ timeout: 2000 }).catch(() => false)) {
await statusFilter.click();
await page.waitForTimeout(500);
// Apply additional filter
const searchInput = page.locator('input[type="search"]').first();
if (await searchInput.isVisible()) {
await searchInput.fill('router');
await page.waitForTimeout(500);
// URL should have multiple params
const url = page.url();
if (url.includes('status=') || url.includes('search=')) {
expect(url.length).toBeGreaterThan(0);
}
}
}
});
test('shows active filter count', async ({ page }) => {
await page.goto('/devices');
const statusFilter = page.locator('button:has-text("Online")').first();
if (await statusFilter.isVisible({ timeout: 2000 }).catch(() => false)) {
await statusFilter.click();
await page.waitForTimeout(500);
// Look for active filter indicator
const filterCount = page.locator('[data-filter-count], .badge, :has-text("filter")').first();
if (await filterCount.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(filterCount).toBeVisible();
}
}
});
test('can clear individual filter', async ({ page }) => {
await page.goto('/devices?status=online');
const clearButton = page.locator('button:has-text("Clear"), button:has-text("×")').first();
if (await clearButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await clearButton.click();
await page.waitForTimeout(500);
// URL should update
const url = page.url();
expect(url).not.toContain('status=online');
}
});
test('can clear all filters', async ({ page }) => {
await page.goto('/devices?status=online&sort=name');
const clearAllButton = page.locator('button:has-text("Clear all"), button:has-text("Reset")').first();
if (await clearAllButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await clearAllButton.click();
await page.waitForTimeout(500);
// URL should be clean
const url = page.url();
expect(url).toBe(page.url().split('?')[0]);
}
});
});
test.describe('Advanced Search Features', () => {
test('supports wildcard search', async ({ page }) => {
await page.goto('/devices');
const searchInput = page.locator('input[type="search"]').first();
if (await searchInput.isVisible({ timeout: 2000 }).catch(() => false)) {
await searchInput.fill('router*');
await page.waitForTimeout(500);
// Should search with wildcard
await expect(page.locator('body')).toBeVisible();
}
});
test('search is case-insensitive', async ({ page }) => {
await page.goto('/devices');
const searchInput = page.locator('input[type="search"]').first();
if (await searchInput.isVisible({ timeout: 2000 }).catch(() => false)) {
await searchInput.fill('TEST');
await page.waitForTimeout(500);
// Should find results regardless of case
await expect(page.locator('body')).toBeVisible();
}
});
test('can search by IP address', async ({ page }) => {
await page.goto('/devices');
const searchInput = page.locator('input[type="search"]').first();
if (await searchInput.isVisible({ timeout: 2000 }).catch(() => false)) {
await searchInput.fill('192.168');
await page.waitForTimeout(500);
// Should search IPs
await expect(page.locator('body')).toBeVisible();
}
});
test('can search by MAC address', async ({ page }) => {
await page.goto('/devices');
const searchInput = page.locator('input[type="search"]').first();
if (await searchInput.isVisible({ timeout: 2000 }).catch(() => false)) {
await searchInput.fill('00:11:22');
await page.waitForTimeout(500);
// Should search MACs
await expect(page.locator('body')).toBeVisible();
}
});
test('search highlights matching terms', async ({ page }) => {
await page.goto('/devices');
const searchInput = page.locator('input[type="search"]').first();
if (await searchInput.isVisible({ timeout: 2000 }).catch(() => false)) {
await searchInput.fill('router');
await page.waitForTimeout(500);
// Look for highlighted text
const highlighted = page.locator('mark, .highlight, strong').first();
if (await highlighted.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(highlighted).toBeVisible();
}
}
});
});
test.describe('Filter Presets', () => {
test('can save filter preset', async ({ page }) => {
await page.goto('/devices?status=online&sort=name');
const savePresetButton = page.locator('button:has-text("Save filter"), button:has-text("Save preset")').first();
if (await savePresetButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(savePresetButton).toBeVisible();
}
});
test('can load saved filter preset', async ({ page }) => {
await page.goto('/devices');
const presetButton = page.locator('button:has-text("Preset"), [data-preset]').first();
if (await presetButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await presetButton.click();
await page.waitForTimeout(500);
// Filters should be applied
await expect(page.locator('body')).toBeVisible();
}
});
});
test.describe('Date Range Filtering', () => {
test('can filter by date range', async ({ page }) => {
await page.goto('/alerts');
const dateFilter = page.locator('input[type="date"]').first();
if (await dateFilter.isVisible({ timeout: 2000 }).catch(() => false)) {
await dateFilter.fill('2024-01-01');
await page.waitForTimeout(500);
// Should filter by date
await expect(page.locator('body')).toBeVisible();
}
});
test('shows relative date options', async ({ page }) => {
await page.goto('/alerts');
const relativeDateButton = page.locator('button:has-text("Last 24h"), button:has-text("Last 7d")').first();
if (await relativeDateButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await relativeDateButton.click();
await page.waitForTimeout(500);
}
});
test('can use custom date range', async ({ page }) => {
await page.goto('/alerts');
const customRangeButton = page.locator('button:has-text("Custom"), button:has-text("Date range")').first();
if (await customRangeButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await customRangeButton.click();
await page.waitForTimeout(300);
// Date picker should appear
const datePicker = page.locator('[role="dialog"], .date-picker').first();
if (await datePicker.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(datePicker).toBeVisible();
}
}
});
});
test.describe('Export Filtered Results', () => {
test('can export filtered results', async ({ page }) => {
await page.goto('/devices?status=online');
const exportButton = page.locator('button:has-text("Export"), button:has-text("Download")').first();
if (await exportButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(exportButton).toBeVisible();
}
});
test('export respects current filters', async ({ page }) => {
await page.goto('/devices?status=online');
const exportButton = page.locator('button:has-text("Export")').first();
if (await exportButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await exportButton.click();
await page.waitForTimeout(300);
// Export dialog or download should start
await expect(page.locator('body')).toBeVisible();
}
});
});
test.describe('Result Count Display', () => {
test('shows result count after filtering', async ({ page }) => {
await page.goto('/devices');
const statusFilter = page.locator('button:has-text("Online")').first();
if (await statusFilter.isVisible({ timeout: 2000 }).catch(() => false)) {
await statusFilter.click();
await page.waitForTimeout(500);
// Look for result count
const resultCount = page.locator(':has-text("result"), :has-text("device"), .count').first();
if (await resultCount.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(resultCount).toBeVisible();
}
}
});
test('shows "no results" when filter returns empty', async ({ page }) => {
await page.goto('/devices');
const searchInput = page.locator('input[type="search"]').first();
if (await searchInput.isVisible({ timeout: 2000 }).catch(() => false)) {
await searchInput.fill('xyznonexistent12345');
await page.waitForTimeout(500);
const noResults = page.locator(':has-text("No"), :has-text("results"), .empty-state').first();
if (await noResults.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(noResults).toBeVisible();
}
}
});
});
});