E2E Tests (tests/frontend/e2e/setup_page.spec.js): - Initial page load and section display (4 tests) - Form validation: required fields, password rules (5 tests) - Password strength indicator with real-time updates (5 tests) - Password visibility toggle (3 tests) - Configuration sections: general, security, scheduler, etc (6 tests) - Form submission: valid/invalid data, loading states (4 tests) - Theme integration during setup (3 tests) - Accessibility: labels, keyboard nav, ARIA (3 tests) - Edge cases: long inputs, special chars, rapid clicks (4 tests) Total: 37 E2E tests API Tests (tests/api/test_setup_endpoints.py): - Endpoint existence and valid data submission (2 tests) - Required field validation (2 tests) - Password strength validation (1 test) - Already configured rejection (1 test) - Setting validation: scheduler, logging, backup, NFO (7 tests) - Configuration persistence to config.json (3 tests) - Setup redirect behavior (3 tests) - Password hashing security (1 test) - Edge cases: Unicode, special chars, null values (4 tests) Total: 24 API tests Updated instructions.md marking setup tests complete
363 lines
11 KiB
JavaScript
363 lines
11 KiB
JavaScript
/**
|
|
* Theme E2E Tests
|
|
*
|
|
* End-to-end tests for dark mode/theme switching functionality
|
|
* Tests the actual UI interaction and CSS application
|
|
*/
|
|
|
|
import { expect, test } from '@playwright/test';
|
|
|
|
test.describe('Theme Switching E2E', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
// Clear localStorage before each test
|
|
await page.goto('/');
|
|
await page.evaluate(() => localStorage.clear());
|
|
await page.reload();
|
|
});
|
|
|
|
test('should display theme toggle button', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
const themeToggle = page.locator('#theme-toggle');
|
|
await expect(themeToggle).toBeVisible();
|
|
});
|
|
|
|
test('should start with light theme by default', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
const theme = await page.evaluate(() =>
|
|
document.documentElement.getAttribute('data-theme')
|
|
);
|
|
|
|
expect(theme).toBe('light');
|
|
});
|
|
|
|
test('should toggle to dark theme when button clicked', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// Click theme toggle button
|
|
await page.click('#theme-toggle');
|
|
|
|
// Verify theme changed to dark
|
|
const theme = await page.evaluate(() =>
|
|
document.documentElement.getAttribute('data-theme')
|
|
);
|
|
|
|
expect(theme).toBe('dark');
|
|
});
|
|
|
|
test('should toggle back to light theme on second click', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// Click twice
|
|
await page.click('#theme-toggle');
|
|
await page.click('#theme-toggle');
|
|
|
|
// Should be back to light
|
|
const theme = await page.evaluate(() =>
|
|
document.documentElement.getAttribute('data-theme')
|
|
);
|
|
|
|
expect(theme).toBe('light');
|
|
});
|
|
|
|
test('should update icon when toggling theme', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// Light theme should show moon icon
|
|
let iconClass = await page.locator('#theme-toggle i').getAttribute('class');
|
|
expect(iconClass).toContain('fa-moon');
|
|
|
|
// Click to dark theme
|
|
await page.click('#theme-toggle');
|
|
|
|
// Dark theme should show sun icon
|
|
iconClass = await page.locator('#theme-toggle i').getAttribute('class');
|
|
expect(iconClass).toContain('fa-sun');
|
|
});
|
|
|
|
test('should persist theme in localStorage', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// Toggle to dark
|
|
await page.click('#theme-toggle');
|
|
|
|
// Check localStorage
|
|
const savedTheme = await page.evaluate(() => localStorage.getItem('theme'));
|
|
expect(savedTheme).toBe('dark');
|
|
});
|
|
|
|
test('should load saved theme on page reload', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// Toggle to dark
|
|
await page.click('#theme-toggle');
|
|
|
|
// Reload page
|
|
await page.reload();
|
|
|
|
// Should still be dark
|
|
const theme = await page.evaluate(() =>
|
|
document.documentElement.getAttribute('data-theme')
|
|
);
|
|
|
|
expect(theme).toBe('dark');
|
|
});
|
|
|
|
test('should maintain theme across navigation', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// Toggle to dark
|
|
await page.click('#theme-toggle');
|
|
|
|
// Navigate away and back (if there are other pages)
|
|
// For now, just reload as a proxy
|
|
await page.reload();
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Should still be dark
|
|
const theme = await page.evaluate(() =>
|
|
document.documentElement.getAttribute('data-theme')
|
|
);
|
|
|
|
expect(theme).toBe('dark');
|
|
});
|
|
});
|
|
|
|
test.describe('Theme CSS Application', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/');
|
|
await page.evaluate(() => localStorage.clear());
|
|
await page.reload();
|
|
});
|
|
|
|
test('should apply dark theme styles to body', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// Toggle to dark theme
|
|
await page.click('#theme-toggle');
|
|
|
|
// Wait for theme to apply
|
|
await page.waitForTimeout(100);
|
|
|
|
// Verify data-theme attribute is set (which CSS uses)
|
|
const theme = await page.evaluate(() =>
|
|
document.documentElement.getAttribute('data-theme')
|
|
);
|
|
|
|
expect(theme).toBe('dark');
|
|
});
|
|
|
|
test('should affect all page elements when theme changes', async ({ page }) => {
|
|
await page.goto('/');
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Get background color in light theme
|
|
const lightBg = await page.evaluate(() =>
|
|
window.getComputedStyle(document.body).backgroundColor
|
|
);
|
|
|
|
// Toggle to dark theme
|
|
await page.click('#theme-toggle');
|
|
await page.waitForTimeout(100);
|
|
|
|
// Get background color in dark theme
|
|
const darkBg = await page.evaluate(() =>
|
|
window.getComputedStyle(document.body).backgroundColor
|
|
);
|
|
|
|
// Colors should be different
|
|
expect(lightBg).not.toBe(darkBg);
|
|
});
|
|
});
|
|
|
|
test.describe('Theme Accessibility', () => {
|
|
test('should have accessible theme toggle button', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
const button = page.locator('#theme-toggle');
|
|
|
|
// Button should be keyboard accessible
|
|
await button.focus();
|
|
const isFocused = await button.evaluate((el) => el === document.activeElement);
|
|
expect(isFocused).toBe(true);
|
|
});
|
|
|
|
test('should toggle theme with Enter key', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// Focus the button
|
|
await page.locator('#theme-toggle').focus();
|
|
|
|
// Press Enter
|
|
await page.keyboard.press('Enter');
|
|
|
|
// Theme should change
|
|
const theme = await page.evaluate(() =>
|
|
document.documentElement.getAttribute('data-theme')
|
|
);
|
|
|
|
expect(theme).toBe('dark');
|
|
});
|
|
|
|
test('should have proper contrast in both themes', async ({ page }) => {
|
|
await page.goto('/');
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// This is a basic test - proper contrast testing would require
|
|
// more sophisticated color analysis tools
|
|
|
|
// Light theme
|
|
let bodyBg = await page.evaluate(() =>
|
|
window.getComputedStyle(document.body).backgroundColor
|
|
);
|
|
expect(bodyBg).toBeTruthy();
|
|
|
|
// Dark theme
|
|
await page.click('#theme-toggle');
|
|
await page.waitForTimeout(100);
|
|
|
|
bodyBg = await page.evaluate(() =>
|
|
window.getComputedStyle(document.body).backgroundColor
|
|
);
|
|
expect(bodyBg).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
test.describe('Theme Performance', () => {
|
|
test('should toggle theme quickly without lag', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
const startTime = Date.now();
|
|
|
|
// Toggle 10 times
|
|
for (let i = 0; i < 10; i++) {
|
|
await page.click('#theme-toggle');
|
|
}
|
|
|
|
const endTime = Date.now();
|
|
const duration = endTime - startTime;
|
|
|
|
// Should complete in reasonable time (< 2 seconds for 10 toggles)
|
|
expect(duration).toBeLessThan(2000);
|
|
});
|
|
|
|
test('should not cause memory leaks with repeated toggles', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// Get initial memory (if available)
|
|
const initialMemory = await page.evaluate(() => {
|
|
if (performance.memory) {
|
|
return performance.memory.usedJSHeapSize;
|
|
}
|
|
return null;
|
|
});
|
|
|
|
// Toggle many times
|
|
for (let i = 0; i < 50; i++) {
|
|
await page.click('#theme-toggle');
|
|
}
|
|
|
|
// Get final memory
|
|
const finalMemory = await page.evaluate(() => {
|
|
if (performance.memory) {
|
|
return performance.memory.usedJSHeapSize;
|
|
}
|
|
return null;
|
|
});
|
|
|
|
if (initialMemory && finalMemory) {
|
|
// Memory shouldn't grow excessively (allow 5MB growth)
|
|
const memoryGrowth = finalMemory - initialMemory;
|
|
expect(memoryGrowth).toBeLessThan(5 * 1024 * 1024);
|
|
}
|
|
});
|
|
});
|
|
|
|
test.describe('Theme Edge Cases', () => {
|
|
test('should handle rapid clicks gracefully', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// Click very rapidly
|
|
const button = page.locator('#theme-toggle');
|
|
await button.click({ clickCount: 5, delay: 10 });
|
|
|
|
// Should still have a valid theme
|
|
const theme = await page.evaluate(() =>
|
|
document.documentElement.getAttribute('data-theme')
|
|
);
|
|
|
|
expect(['light', 'dark']).toContain(theme);
|
|
});
|
|
|
|
test('should work when localStorage is disabled', async ({ page, context }) => {
|
|
// Some browsers/modes disable localStorage
|
|
// This test verifies graceful degradation
|
|
|
|
await page.goto('/');
|
|
|
|
// Attempt to toggle (might fail silently)
|
|
await page.click('#theme-toggle');
|
|
|
|
// Page should still function
|
|
const isVisible = await page.locator('body').isVisible();
|
|
expect(isVisible).toBe(true);
|
|
});
|
|
|
|
test('should handle missing theme icon element', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// Remove the icon element
|
|
await page.evaluate(() => {
|
|
const icon = document.querySelector('#theme-toggle i');
|
|
if (icon) icon.remove();
|
|
});
|
|
|
|
// Toggle should still work (just without icon update)
|
|
await page.click('#theme-toggle');
|
|
|
|
const theme = await page.evaluate(() =>
|
|
document.documentElement.getAttribute('data-theme')
|
|
);
|
|
|
|
expect(theme).toBe('dark');
|
|
});
|
|
});
|
|
|
|
test.describe('Theme Integration with Other Features', () => {
|
|
test('should maintain theme when opening modals', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// Set dark theme
|
|
await page.click('#theme-toggle');
|
|
|
|
// Try to open a modal (if exists)
|
|
const settingsButton = page.locator('#settings-button');
|
|
if (await settingsButton.isVisible()) {
|
|
await settingsButton.click();
|
|
await page.waitForTimeout(500);
|
|
|
|
// Theme should still be dark
|
|
const theme = await page.evaluate(() =>
|
|
document.documentElement.getAttribute('data-theme')
|
|
);
|
|
expect(theme).toBe('dark');
|
|
}
|
|
});
|
|
|
|
test('should apply theme to dynamically loaded content', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// Set dark theme
|
|
await page.click('#theme-toggle');
|
|
|
|
// Verify the data-theme attribute is on root
|
|
// Any dynamically loaded content should inherit this
|
|
const hasTheme = await page.evaluate(() =>
|
|
document.documentElement.hasAttribute('data-theme')
|
|
);
|
|
|
|
expect(hasTheme).toBe(true);
|
|
});
|
|
});
|