533 lines
18 KiB
JavaScript
533 lines
18 KiB
JavaScript
/**
|
|
* Settings Modal E2E Tests
|
|
*
|
|
* End-to-end tests for the configuration/settings modal
|
|
* Tests modal open/close, configuration editing, saving, and backup/restore
|
|
*/
|
|
|
|
import { expect, test } from '@playwright/test';
|
|
|
|
test.describe('Settings Modal - Basic Functionality', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/');
|
|
// Wait for page to load
|
|
await page.waitForLoadState('networkidle');
|
|
});
|
|
|
|
test('should have settings button visible', async ({ page }) => {
|
|
const settingsBtn = page.locator('#settings-button');
|
|
await expect(settingsBtn).toBeVisible();
|
|
});
|
|
|
|
test('should open settings modal when button clicked', async ({ page }) => {
|
|
await page.click('#settings-button');
|
|
|
|
const modal = page.locator('#config-modal');
|
|
await expect(modal).not.toHaveClass(/hidden/);
|
|
});
|
|
|
|
test('should close modal when close button clicked', async ({ page }) => {
|
|
await page.click('#settings-button');
|
|
await page.waitForTimeout(500);
|
|
|
|
await page.click('#close-config');
|
|
|
|
const modal = page.locator('#config-modal');
|
|
await expect(modal).toHaveClass(/hidden/);
|
|
});
|
|
|
|
test('should close modal when overlay clicked', async ({ page }) => {
|
|
await page.click('#settings-button');
|
|
await page.waitForTimeout(500);
|
|
|
|
await page.click('#config-modal .modal-overlay');
|
|
|
|
const modal = page.locator('#config-modal');
|
|
await expect(modal).toHaveClass(/hidden/);
|
|
});
|
|
|
|
test('should close modal with Escape key', async ({ page }) => {
|
|
await page.click('#settings-button');
|
|
await page.waitForTimeout(500);
|
|
|
|
await page.keyboard.press('Escape');
|
|
|
|
const modal = page.locator('#config-modal');
|
|
await expect(modal).toHaveClass(/hidden/);
|
|
});
|
|
});
|
|
|
|
test.describe('Settings Modal - Configuration Sections', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/');
|
|
await page.waitForLoadState('networkidle');
|
|
await page.click('#settings-button');
|
|
await page.waitForTimeout(500);
|
|
});
|
|
|
|
test('should display general settings section', async ({ page }) => {
|
|
await expect(page.locator('text=General Settings')).toBeVisible();
|
|
await expect(page.locator('#app-name-input')).toBeVisible();
|
|
await expect(page.locator('#data-dir-input')).toBeVisible();
|
|
await expect(page.locator('#anime-directory-input')).toBeVisible();
|
|
});
|
|
|
|
test('should display scheduler configuration section', async ({ page }) => {
|
|
await expect(page.locator('text=Scheduled Operations')).toBeVisible();
|
|
await expect(page.locator('#scheduled-rescan-enabled')).toBeVisible();
|
|
});
|
|
|
|
test('should display NFO settings section', async ({ page }) => {
|
|
await expect(page.locator('text=NFO Metadata Settings')).toBeVisible();
|
|
});
|
|
|
|
test('should display backup settings section', async ({ page }) => {
|
|
await expect(page.locator('text=Backup Settings')).toBeVisible();
|
|
});
|
|
|
|
test('should display advanced settings section', async ({ page }) => {
|
|
await expect(page.locator('text=Advanced Settings')).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test.describe('Settings Modal - Load Configuration', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/');
|
|
await page.waitForLoadState('networkidle');
|
|
});
|
|
|
|
test('should load current configuration when opened', async ({ page }) => {
|
|
await page.click('#settings-button');
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Check that fields are populated
|
|
const animeDir = await page.locator('#anime-directory-input').inputValue();
|
|
expect(animeDir).toBeTruthy();
|
|
});
|
|
|
|
test('should load series count', async ({ page }) => {
|
|
await page.click('#settings-button');
|
|
await page.waitForTimeout(1000);
|
|
|
|
const seriesCount = page.locator('#series-count-input');
|
|
await expect(seriesCount).toBeVisible();
|
|
|
|
const value = await seriesCount.inputValue();
|
|
expect(value).toMatch(/^\d+$/); // Should be a number
|
|
});
|
|
|
|
test('should load scheduler configuration', async ({ page }) => {
|
|
await page.click('#settings-button');
|
|
await page.waitForTimeout(1000);
|
|
|
|
const enabled = page.locator('#scheduled-rescan-enabled');
|
|
await expect(enabled).toBeVisible();
|
|
|
|
// Should have a checked state (either true or false)
|
|
const isChecked = await enabled.isChecked();
|
|
expect(typeof isChecked).toBe('boolean');
|
|
});
|
|
|
|
test('should display connection status', async ({ page }) => {
|
|
await page.click('#settings-button');
|
|
await page.waitForTimeout(1000);
|
|
|
|
const statusDisplay = page.locator('#connection-status-display');
|
|
await expect(statusDisplay).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test.describe('Settings Modal - Edit Configuration', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/');
|
|
await page.waitForLoadState('networkidle');
|
|
await page.click('#settings-button');
|
|
await page.waitForTimeout(500);
|
|
});
|
|
|
|
test('should allow editing application name', async ({ page }) => {
|
|
const nameInput = page.locator('#app-name-input');
|
|
|
|
await nameInput.clear();
|
|
await nameInput.fill('Test Aniworld');
|
|
|
|
const value = await nameInput.inputValue();
|
|
expect(value).toBe('Test Aniworld');
|
|
});
|
|
|
|
test('should allow editing anime directory', async ({ page }) => {
|
|
const dirInput = page.locator('#anime-directory-input');
|
|
|
|
await dirInput.clear();
|
|
await dirInput.fill('/new/anime/directory');
|
|
|
|
const value = await dirInput.inputValue();
|
|
expect(value).toBe('/new/anime/directory');
|
|
});
|
|
|
|
test('should allow toggling scheduler enabled', async ({ page }) => {
|
|
const checkbox = page.locator('#scheduled-rescan-enabled');
|
|
|
|
const initialState = await checkbox.isChecked();
|
|
|
|
await checkbox.click();
|
|
|
|
const newState = await checkbox.isChecked();
|
|
expect(newState).not.toBe(initialState);
|
|
});
|
|
|
|
test('should series count field be readonly', async ({ page }) => {
|
|
const seriesCount = page.locator('#series-count-input');
|
|
|
|
const isReadonly = await seriesCount.evaluate(
|
|
(el: HTMLInputElement) => el.readOnly
|
|
);
|
|
expect(isReadonly).toBe(true);
|
|
});
|
|
});
|
|
|
|
test.describe('Settings Modal - Save Configuration', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/');
|
|
await page.waitForLoadState('networkidle');
|
|
await page.click('#settings-button');
|
|
await page.waitForTimeout(500);
|
|
});
|
|
|
|
test('should have save configuration button', async ({ page }) => {
|
|
const saveBtn = page.locator('#save-main-config');
|
|
await expect(saveBtn).toBeVisible();
|
|
});
|
|
|
|
test('should save main configuration when clicked', async ({ page }) => {
|
|
// Make a small change
|
|
await page.fill('#app-name-input', 'Test Config');
|
|
|
|
// Setup request interception
|
|
const responsePromise = page.waitForResponse(
|
|
response => response.url().includes('/api/config') &&
|
|
response.request().method() === 'PUT'
|
|
);
|
|
|
|
// Click save
|
|
await page.click('#save-main-config');
|
|
|
|
try {
|
|
const response = await responsePromise;
|
|
expect(response.status()).toBeLessThan(400);
|
|
} catch (error) {
|
|
// Config endpoint might not be fully functional in test
|
|
console.log('Config save endpoint interaction');
|
|
}
|
|
});
|
|
|
|
test('should save scheduler configuration', async ({ page }) => {
|
|
const saveBtn = page.locator('#save-scheduler-config');
|
|
await expect(saveBtn).toBeVisible();
|
|
|
|
await saveBtn.click();
|
|
|
|
// Should show some feedback
|
|
await page.waitForTimeout(500);
|
|
});
|
|
|
|
test('should show feedback after saving', async ({ page }) => {
|
|
await page.click('#save-main-config');
|
|
|
|
// Wait for toast or feedback message
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Check for toast notification (if exists)
|
|
const toast = page.locator('.toast, .notification, .alert');
|
|
if (await toast.count() > 0) {
|
|
await expect(toast.first()).toBeVisible();
|
|
}
|
|
});
|
|
|
|
test('should disable save button during save', async ({ page }) => {
|
|
const saveBtn = page.locator('#save-main-config');
|
|
|
|
await saveBtn.click();
|
|
|
|
// Button should be temporarily disabled
|
|
await page.waitForTimeout(100);
|
|
});
|
|
});
|
|
|
|
test.describe('Settings Modal - Reset Configuration', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/');
|
|
await page.waitForLoadState('networkidle');
|
|
await page.click('#settings-button');
|
|
await page.waitForTimeout(500);
|
|
});
|
|
|
|
test('should have reset button', async ({ page }) => {
|
|
const resetBtn = page.locator('#reset-main-config');
|
|
await expect(resetBtn).toBeVisible();
|
|
});
|
|
|
|
test('should reset fields to original values', async ({ page }) => {
|
|
const nameInput = page.locator('#app-name-input');
|
|
|
|
// Get original value
|
|
const originalValue = await nameInput.inputValue();
|
|
|
|
// Change it
|
|
await nameInput.clear();
|
|
await nameInput.fill('Changed Value');
|
|
|
|
// Reset
|
|
await page.click('#reset-main-config');
|
|
await page.waitForTimeout(300);
|
|
|
|
// Should be back to original (or reloaded)
|
|
const resetValue = await nameInput.inputValue();
|
|
// Value should have changed from "Changed Value"
|
|
expect(resetValue).not.toBe('Changed Value');
|
|
});
|
|
});
|
|
|
|
test.describe('Settings Modal - Browse Directory', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/');
|
|
await page.waitForLoadState('networkidle');
|
|
await page.click('#settings-button');
|
|
await page.waitForTimeout(500);
|
|
});
|
|
|
|
test('should have browse directory button', async ({ page }) => {
|
|
const browseBtn = page.locator('#browse-directory');
|
|
await expect(browseBtn).toBeVisible();
|
|
});
|
|
|
|
test('should trigger directory browser when clicked', async ({ page }) => {
|
|
const browseBtn = page.locator('#browse-directory');
|
|
|
|
// Click the button (may not actually open file picker in headless mode)
|
|
await browseBtn.click();
|
|
|
|
// In a real browser, this would open a file picker
|
|
// In headless, we just verify the button is functional
|
|
await page.waitForTimeout(300);
|
|
});
|
|
});
|
|
|
|
test.describe('Settings Modal - Connection Test', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/');
|
|
await page.waitForLoadState('networkidle');
|
|
await page.click('#settings-button');
|
|
await page.waitForTimeout(500);
|
|
});
|
|
|
|
test('should have test connection button', async ({ page }) => {
|
|
const testBtn = page.locator('#test-connection');
|
|
await expect(testBtn).toBeVisible();
|
|
});
|
|
|
|
test('should update connection status when clicked', async ({ page }) => {
|
|
const testBtn = page.locator('#test-connection');
|
|
const statusDisplay = page.locator('#connection-status-display');
|
|
|
|
await testBtn.click();
|
|
|
|
// Wait for connection test
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Status should have updated
|
|
await expect(statusDisplay).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test.describe('Settings Modal - Scheduler Status Display', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/');
|
|
await page.waitForLoadState('networkidle');
|
|
await page.click('#settings-button');
|
|
await page.waitForTimeout(1000);
|
|
});
|
|
|
|
test('should display next scheduled rescan time', async ({ page }) => {
|
|
const nextRescan = page.locator('#next-rescan-time');
|
|
await expect(nextRescan).toBeVisible();
|
|
|
|
const text = await nextRescan.textContent();
|
|
expect(text).toBeTruthy();
|
|
});
|
|
|
|
test('should display last rescan time', async ({ page }) => {
|
|
const lastRescan = page.locator('#last-rescan-time');
|
|
await expect(lastRescan).toBeVisible();
|
|
});
|
|
|
|
test('should display scheduler running status', async ({ page }) => {
|
|
const status = page.locator('#scheduler-running-status');
|
|
await expect(status).toBeVisible();
|
|
|
|
const text = await status.textContent();
|
|
expect(text).toMatch(/(running|stopped|active|inactive)/i);
|
|
});
|
|
});
|
|
|
|
test.describe('Settings Modal - Accessibility', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/');
|
|
await page.waitForLoadState('networkidle');
|
|
});
|
|
|
|
test('should have accessible labels for inputs', async ({ page }) => {
|
|
await page.click('#settings-button');
|
|
await page.waitForTimeout(500);
|
|
|
|
const nameLabel = page.locator('label[for="app-name-input"]');
|
|
await expect(nameLabel).toBeVisible();
|
|
});
|
|
|
|
test('should support keyboard navigation in modal', async ({ page }) => {
|
|
await page.click('#settings-button');
|
|
await page.waitForTimeout(500);
|
|
|
|
// Tab through inputs
|
|
await page.keyboard.press('Tab');
|
|
await page.keyboard.press('Tab');
|
|
|
|
// Should be able to navigate
|
|
const focusedElement = await page.evaluate(() => document.activeElement?.tagName);
|
|
expect(focusedElement).toBeTruthy();
|
|
});
|
|
|
|
test('should trap focus within modal', async ({ page }) => {
|
|
await page.click('#settings-button');
|
|
await page.waitForTimeout(500);
|
|
|
|
// Modal should be open
|
|
const modal = page.locator('#config-modal');
|
|
await expect(modal).not.toHaveClass(/hidden/);
|
|
|
|
// Focus should stay within modal when tabbing
|
|
// (implementation depends on focus trap)
|
|
});
|
|
|
|
test('should close modal with Escape', async ({ page }) => {
|
|
await page.click('#settings-button');
|
|
await page.waitForTimeout(500);
|
|
|
|
await page.keyboard.press('Escape');
|
|
|
|
const modal = page.locator('#config-modal');
|
|
await expect(modal).toHaveClass(/hidden/);
|
|
});
|
|
});
|
|
|
|
test.describe('Settings Modal - Edge Cases', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/');
|
|
await page.waitForLoadState('networkidle');
|
|
});
|
|
|
|
test('should handle opening modal multiple times', async ({ page }) => {
|
|
// Open and close multiple times
|
|
for (let i = 0; i < 3; i++) {
|
|
await page.click('#settings-button');
|
|
await page.waitForTimeout(300);
|
|
await page.click('#close-config');
|
|
await page.waitForTimeout(300);
|
|
}
|
|
|
|
// Should still work
|
|
await page.click('#settings-button');
|
|
const modal = page.locator('#config-modal');
|
|
await expect(modal).not.toHaveClass(/hidden/);
|
|
});
|
|
|
|
test('should handle rapid field changes', async ({ page }) => {
|
|
await page.click('#settings-button');
|
|
await page.waitForTimeout(500);
|
|
|
|
const nameInput = page.locator('#app-name-input');
|
|
|
|
// Rapidly change value
|
|
for (let i = 0; i < 5; i++) {
|
|
await nameInput.fill(`Value${i}`);
|
|
}
|
|
|
|
// Should remain stable
|
|
await expect(nameInput).toBeVisible();
|
|
});
|
|
|
|
test('should handle very long input values', async ({ page }) => {
|
|
await page.click('#settings-button');
|
|
await page.waitForTimeout(500);
|
|
|
|
const longValue = 'a'.repeat(1000);
|
|
await page.fill('#app-name-input', longValue);
|
|
|
|
const value = await page.locator('#app-name-input').inputValue();
|
|
expect(value.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('should handle special characters in inputs', async ({ page }) => {
|
|
await page.click('#settings-button');
|
|
await page.waitForTimeout(500);
|
|
|
|
await page.fill('#anime-directory-input', '/path/with spaces/and-special_chars!');
|
|
|
|
const value = await page.locator('#anime-directory-input').inputValue();
|
|
expect(value).toContain('spaces');
|
|
});
|
|
|
|
test('should handle clicking save without changes', async ({ page }) => {
|
|
await page.click('#settings-button');
|
|
await page.waitForTimeout(500);
|
|
|
|
// Click save without making any changes
|
|
await page.click('#save-main-config');
|
|
|
|
// Should not error
|
|
await page.waitForTimeout(500);
|
|
});
|
|
});
|
|
|
|
test.describe('Settings Modal - Theme Integration', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/');
|
|
await page.waitForLoadState('networkidle');
|
|
});
|
|
|
|
test('should respect current theme when modal opens', async ({ page }) => {
|
|
// Switch to dark theme
|
|
await page.click('#theme-toggle');
|
|
await page.waitForTimeout(300);
|
|
|
|
// Open modal
|
|
await page.click('#settings-button');
|
|
await page.waitForTimeout(300);
|
|
|
|
// Modal should have dark theme
|
|
const theme = await page.evaluate(() =>
|
|
document.documentElement.getAttribute('data-theme')
|
|
);
|
|
expect(theme).toBe('dark');
|
|
});
|
|
|
|
test('should allow theme toggle while modal is open', async ({ page }) => {
|
|
await page.click('#settings-button');
|
|
await page.waitForTimeout(300);
|
|
|
|
const initialTheme = await page.evaluate(() =>
|
|
document.documentElement.getAttribute('data-theme')
|
|
);
|
|
|
|
// Toggle theme
|
|
await page.click('#theme-toggle');
|
|
await page.waitForTimeout(300);
|
|
|
|
const newTheme = await page.evaluate(() =>
|
|
document.documentElement.getAttribute('data-theme')
|
|
);
|
|
|
|
expect(newTheme).not.toBe(initialTheme);
|
|
});
|
|
});
|