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
352 lines
12 KiB
JavaScript
352 lines
12 KiB
JavaScript
/**
|
|
* Theme/Dark Mode Tests
|
|
*
|
|
* Tests for theme switching functionality in app.js
|
|
* Covers localStorage persistence, DOM attribute changes, and icon updates
|
|
*/
|
|
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
/**
|
|
* Mock implementation of the theme-related methods from app.js
|
|
* This simulates the actual app behavior for testing
|
|
*/
|
|
class ThemeManager {
|
|
initTheme() {
|
|
const savedTheme = localStorage.getItem('theme') || 'light';
|
|
this.setTheme(savedTheme);
|
|
}
|
|
|
|
setTheme(theme) {
|
|
document.documentElement.setAttribute('data-theme', theme);
|
|
localStorage.setItem('theme', theme);
|
|
|
|
const themeIcon = document.querySelector('#theme-toggle i');
|
|
if (themeIcon) {
|
|
themeIcon.className = theme === 'light' ? 'fas fa-moon' : 'fas fa-sun';
|
|
}
|
|
}
|
|
|
|
toggleTheme() {
|
|
const currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
|
|
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
|
|
this.setTheme(newTheme);
|
|
}
|
|
}
|
|
|
|
describe('Theme Management', () => {
|
|
let themeManager;
|
|
|
|
beforeEach(() => {
|
|
// Reset DOM
|
|
document.body.innerHTML = `
|
|
<button id="theme-toggle">
|
|
<i class="fas fa-moon"></i>
|
|
</button>
|
|
`;
|
|
document.documentElement.removeAttribute('data-theme');
|
|
|
|
// Clear localStorage
|
|
localStorage.clear();
|
|
|
|
// Create fresh instance
|
|
themeManager = new ThemeManager();
|
|
});
|
|
|
|
afterEach(() => {
|
|
localStorage.clear();
|
|
document.documentElement.removeAttribute('data-theme');
|
|
});
|
|
|
|
describe('Theme Initialization', () => {
|
|
it('should initialize with light theme by default when no saved preference', () => {
|
|
themeManager.initTheme();
|
|
|
|
expect(document.documentElement.getAttribute('data-theme')).toBe('light');
|
|
expect(localStorage.getItem('theme')).toBe('light');
|
|
});
|
|
|
|
it('should load saved theme from localStorage', () => {
|
|
localStorage.setItem('theme', 'dark');
|
|
|
|
themeManager.initTheme();
|
|
|
|
expect(document.documentElement.getAttribute('data-theme')).toBe('dark');
|
|
});
|
|
|
|
it('should update theme icon on initialization', () => {
|
|
localStorage.setItem('theme', 'dark');
|
|
|
|
themeManager.initTheme();
|
|
|
|
const icon = document.querySelector('#theme-toggle i');
|
|
expect(icon.className).toBe('fas fa-sun');
|
|
});
|
|
|
|
it('should handle missing localStorage gracefully', () => {
|
|
// Simulate localStorage not available
|
|
const originalGetItem = Storage.prototype.getItem;
|
|
Storage.prototype.getItem = vi.fn(() => null);
|
|
|
|
themeManager.initTheme();
|
|
|
|
expect(document.documentElement.getAttribute('data-theme')).toBe('light');
|
|
|
|
Storage.prototype.getItem = originalGetItem;
|
|
});
|
|
});
|
|
|
|
describe('Theme Setting', () => {
|
|
it('should set light theme correctly', () => {
|
|
themeManager.setTheme('light');
|
|
|
|
expect(document.documentElement.getAttribute('data-theme')).toBe('light');
|
|
expect(localStorage.getItem('theme')).toBe('light');
|
|
});
|
|
|
|
it('should set dark theme correctly', () => {
|
|
themeManager.setTheme('dark');
|
|
|
|
expect(document.documentElement.getAttribute('data-theme')).toBe('dark');
|
|
expect(localStorage.getItem('theme')).toBe('dark');
|
|
});
|
|
|
|
it('should update icon to moon for light theme', () => {
|
|
themeManager.setTheme('light');
|
|
|
|
const icon = document.querySelector('#theme-toggle i');
|
|
expect(icon.className).toBe('fas fa-moon');
|
|
});
|
|
|
|
it('should update icon to sun for dark theme', () => {
|
|
themeManager.setTheme('dark');
|
|
|
|
const icon = document.querySelector('#theme-toggle i');
|
|
expect(icon.className).toBe('fas fa-sun');
|
|
});
|
|
|
|
it('should persist theme to localStorage', () => {
|
|
themeManager.setTheme('dark');
|
|
expect(localStorage.getItem('theme')).toBe('dark');
|
|
|
|
themeManager.setTheme('light');
|
|
expect(localStorage.getItem('theme')).toBe('light');
|
|
});
|
|
|
|
it('should handle missing theme icon element gracefully', () => {
|
|
document.body.innerHTML = ''; // Remove theme toggle button
|
|
|
|
expect(() => {
|
|
themeManager.setTheme('dark');
|
|
}).not.toThrow();
|
|
|
|
expect(document.documentElement.getAttribute('data-theme')).toBe('dark');
|
|
});
|
|
});
|
|
|
|
describe('Theme Toggling', () => {
|
|
it('should toggle from light to dark', () => {
|
|
themeManager.setTheme('light');
|
|
|
|
themeManager.toggleTheme();
|
|
|
|
expect(document.documentElement.getAttribute('data-theme')).toBe('dark');
|
|
expect(localStorage.getItem('theme')).toBe('dark');
|
|
});
|
|
|
|
it('should toggle from dark to light', () => {
|
|
themeManager.setTheme('dark');
|
|
|
|
themeManager.toggleTheme();
|
|
|
|
expect(document.documentElement.getAttribute('data-theme')).toBe('light');
|
|
expect(localStorage.getItem('theme')).toBe('light');
|
|
});
|
|
|
|
it('should update icon when toggling', () => {
|
|
themeManager.setTheme('light');
|
|
let icon = document.querySelector('#theme-toggle i');
|
|
expect(icon.className).toBe('fas fa-moon');
|
|
|
|
themeManager.toggleTheme();
|
|
icon = document.querySelector('#theme-toggle i');
|
|
expect(icon.className).toBe('fas fa-sun');
|
|
});
|
|
|
|
it('should handle multiple toggles correctly', () => {
|
|
themeManager.setTheme('light');
|
|
|
|
themeManager.toggleTheme(); // light -> dark
|
|
expect(document.documentElement.getAttribute('data-theme')).toBe('dark');
|
|
|
|
themeManager.toggleTheme(); // dark -> light
|
|
expect(document.documentElement.getAttribute('data-theme')).toBe('light');
|
|
|
|
themeManager.toggleTheme(); // light -> dark
|
|
expect(document.documentElement.getAttribute('data-theme')).toBe('dark');
|
|
});
|
|
|
|
it('should default to light when no theme attribute exists', () => {
|
|
document.documentElement.removeAttribute('data-theme');
|
|
|
|
themeManager.toggleTheme();
|
|
|
|
expect(document.documentElement.getAttribute('data-theme')).toBe('dark');
|
|
});
|
|
});
|
|
|
|
describe('Theme Persistence', () => {
|
|
it('should persist theme across page reloads', () => {
|
|
themeManager.setTheme('dark');
|
|
|
|
// Simulate page reload by creating new instance
|
|
const newThemeManager = new ThemeManager();
|
|
newThemeManager.initTheme();
|
|
|
|
expect(document.documentElement.getAttribute('data-theme')).toBe('dark');
|
|
});
|
|
|
|
it('should maintain theme when toggling multiple times', () => {
|
|
themeManager.initTheme();
|
|
|
|
themeManager.toggleTheme();
|
|
themeManager.toggleTheme();
|
|
themeManager.toggleTheme();
|
|
|
|
// Should be dark after 3 toggles (light -> dark -> light -> dark)
|
|
expect(localStorage.getItem('theme')).toBe('dark');
|
|
|
|
// Reload and verify
|
|
const newThemeManager = new ThemeManager();
|
|
newThemeManager.initTheme();
|
|
expect(document.documentElement.getAttribute('data-theme')).toBe('dark');
|
|
});
|
|
});
|
|
|
|
describe('Theme Button Click Handler', () => {
|
|
it('should toggle theme when button is clicked', () => {
|
|
themeManager.initTheme();
|
|
|
|
const button = document.getElementById('theme-toggle');
|
|
button.addEventListener('click', () => {
|
|
themeManager.toggleTheme();
|
|
});
|
|
|
|
expect(document.documentElement.getAttribute('data-theme')).toBe('light');
|
|
|
|
button.click();
|
|
expect(document.documentElement.getAttribute('data-theme')).toBe('dark');
|
|
|
|
button.click();
|
|
expect(document.documentElement.getAttribute('data-theme')).toBe('light');
|
|
});
|
|
});
|
|
|
|
describe('DOM Attribute Application', () => {
|
|
it('should apply data-theme attribute to document root', () => {
|
|
themeManager.setTheme('dark');
|
|
|
|
const root = document.documentElement;
|
|
expect(root.hasAttribute('data-theme')).toBe(true);
|
|
expect(root.getAttribute('data-theme')).toBe('dark');
|
|
});
|
|
|
|
it('should update existing data-theme attribute', () => {
|
|
document.documentElement.setAttribute('data-theme', 'light');
|
|
|
|
themeManager.setTheme('dark');
|
|
|
|
expect(document.documentElement.getAttribute('data-theme')).toBe('dark');
|
|
});
|
|
|
|
it('should work with CSS attribute selectors', () => {
|
|
themeManager.setTheme('dark');
|
|
|
|
// CSS would use: [data-theme="dark"] { ... }
|
|
const root = document.documentElement;
|
|
const themValue = root.getAttribute('data-theme');
|
|
expect(themValue).toBe('dark');
|
|
});
|
|
});
|
|
|
|
describe('Edge Cases', () => {
|
|
it('should handle invalid theme values gracefully', () => {
|
|
themeManager.setTheme('invalid-theme');
|
|
|
|
// Should still set the attribute and localStorage
|
|
expect(document.documentElement.getAttribute('data-theme')).toBe('invalid-theme');
|
|
expect(localStorage.getItem('theme')).toBe('invalid-theme');
|
|
});
|
|
|
|
it('should handle empty string theme', () => {
|
|
themeManager.setTheme('');
|
|
|
|
expect(document.documentElement.getAttribute('data-theme')).toBe('');
|
|
expect(localStorage.getItem('theme')).toBe('');
|
|
});
|
|
|
|
it('should handle rapid theme changes', () => {
|
|
for (let i = 0; i < 10; i++) {
|
|
themeManager.toggleTheme();
|
|
}
|
|
|
|
// After 10 toggles (even number), should be back to light
|
|
expect(document.documentElement.getAttribute('data-theme')).toBe('light');
|
|
});
|
|
|
|
it('should work when localStorage is full', () => {
|
|
// Fill localStorage (though this is hard to truly test)
|
|
const setItemSpy = vi.spyOn(Storage.prototype, 'setItem');
|
|
setItemSpy.mockImplementation(() => {
|
|
throw new Error('QuotaExceededError');
|
|
});
|
|
|
|
expect(() => {
|
|
themeManager.setTheme('dark');
|
|
}).toThrow();
|
|
|
|
setItemSpy.mockRestore();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Theme Icon Updates', () => {
|
|
let themeManager;
|
|
|
|
beforeEach(() => {
|
|
document.body.innerHTML = `
|
|
<button id="theme-toggle">
|
|
<i class="fas fa-moon"></i>
|
|
</button>
|
|
`;
|
|
localStorage.clear();
|
|
themeManager = new ThemeManager();
|
|
});
|
|
|
|
it('should use moon icon for light theme', () => {
|
|
themeManager.setTheme('light');
|
|
|
|
const icon = document.querySelector('#theme-toggle i');
|
|
expect(icon.className).toContain('fa-moon');
|
|
expect(icon.className).not.toContain('fa-sun');
|
|
});
|
|
|
|
it('should use sun icon for dark theme', () => {
|
|
themeManager.setTheme('dark');
|
|
|
|
const icon = document.querySelector('#theme-toggle i');
|
|
expect(icon.className).toContain('fa-sun');
|
|
expect(icon.className).not.toContain('fa-moon');
|
|
});
|
|
|
|
it('should update icon class completely (no class accumulation)', () => {
|
|
themeManager.setTheme('light');
|
|
const icon = document.querySelector('#theme-toggle i');
|
|
expect(icon.className).toBe('fas fa-moon');
|
|
|
|
themeManager.setTheme('dark');
|
|
expect(icon.className).toBe('fas fa-sun');
|
|
expect(icon.className.split(' ').length).toBe(2); // Only 'fas' and 'fa-sun'
|
|
});
|
|
});
|