feat: add update instructions dialog, email notifications, and capture date sorting
- Add Update Instructions Dialog with environment-specific commands (Docker/Git/Standalone) - Add email notification settings for new version alerts - Add "Sort by Capture Date" option using EXIF metadata extraction - Fix E2E tests by loading environment variables via dotenv - Add test-images/ and backend/*.db to .gitignore Closes #181
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
const ADMIN_EMAIL = process.env.ADMIN_EMAIL || '[email protected]';
|
||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'Admin!234';
|
||||
|
||||
// Helper to login to admin
|
||||
async function loginToAdmin(page) {
|
||||
await page.goto('/admin/login');
|
||||
await page.getByLabel(/Email|E-Mail/i).fill(ADMIN_EMAIL);
|
||||
await page.getByLabel(/Password|Passwort/i).fill(ADMIN_PASSWORD);
|
||||
await page.getByRole('button', { name: /Sign In|Log in|Anmelden/i }).click();
|
||||
await expect(page.getByRole('heading', { name: /Dashboard|Übersicht/i })).toBeVisible({ timeout: 20000 });
|
||||
}
|
||||
|
||||
test.describe('Admin Dark Mode Toggle', () => {
|
||||
test('dark mode toggle button exists in admin header', async ({ page }, testInfo) => {
|
||||
if (testInfo.project.name === 'mobile-chrome') {
|
||||
test.skip('Dark mode toggle validated on desktop viewport');
|
||||
}
|
||||
|
||||
await loginToAdmin(page);
|
||||
|
||||
// Look for the dark mode toggle button
|
||||
const toggleButton = page.getByRole('button', { name: /dark mode|light mode|Dunkelmodus|Hellmodus/i });
|
||||
await expect(toggleButton).toBeVisible();
|
||||
});
|
||||
|
||||
test('clicking toggle switches to dark mode and adds dark class', async ({ page }, testInfo) => {
|
||||
if (testInfo.project.name === 'mobile-chrome') {
|
||||
test.skip('Dark mode toggle validated on desktop viewport');
|
||||
}
|
||||
|
||||
await loginToAdmin(page);
|
||||
|
||||
// Check initial state - should be light
|
||||
const html = page.locator('html');
|
||||
const initialHasDark = await html.evaluate(el => el.classList.contains('dark'));
|
||||
|
||||
// Click the toggle
|
||||
const toggleButton = page.getByRole('button', { name: /dark mode|light mode|Dunkelmodus|Hellmodus/i });
|
||||
await toggleButton.click();
|
||||
|
||||
// Wait a moment for the class to toggle
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Verify dark class toggled
|
||||
const afterHasDark = await html.evaluate(el => el.classList.contains('dark'));
|
||||
expect(afterHasDark).toBe(!initialHasDark);
|
||||
|
||||
// Click again to revert
|
||||
await toggleButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const finalHasDark = await html.evaluate(el => el.classList.contains('dark'));
|
||||
expect(finalHasDark).toBe(initialHasDark);
|
||||
});
|
||||
|
||||
test('dark mode preference persists across page reloads', async ({ page }, testInfo) => {
|
||||
if (testInfo.project.name === 'mobile-chrome') {
|
||||
test.skip('Dark mode toggle validated on desktop viewport');
|
||||
}
|
||||
|
||||
await loginToAdmin(page);
|
||||
|
||||
// Set to dark mode
|
||||
const html = page.locator('html');
|
||||
const isAlreadyDark = await html.evaluate(el => el.classList.contains('dark'));
|
||||
|
||||
if (!isAlreadyDark) {
|
||||
const toggleButton = page.getByRole('button', { name: /dark mode|Dunkelmodus/i });
|
||||
await toggleButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
// Verify dark mode is active
|
||||
await expect(html).toHaveAttribute('class', /dark/);
|
||||
|
||||
// Reload the page
|
||||
await page.reload();
|
||||
await expect(page.getByRole('heading', { name: /Dashboard|Übersicht/i })).toBeVisible({ timeout: 20000 });
|
||||
|
||||
// Verify dark mode persists
|
||||
await expect(html).toHaveAttribute('class', /dark/);
|
||||
|
||||
// Clean up - switch back to light mode
|
||||
const toggleButton = page.getByRole('button', { name: /light mode|Hellmodus/i });
|
||||
await toggleButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
});
|
||||
|
||||
test('dark mode applies correct dark background to admin layout', async ({ page }, testInfo) => {
|
||||
if (testInfo.project.name === 'mobile-chrome') {
|
||||
test.skip('Dark mode toggle validated on desktop viewport');
|
||||
}
|
||||
|
||||
await loginToAdmin(page);
|
||||
|
||||
// Enable dark mode
|
||||
const html = page.locator('html');
|
||||
const isAlreadyDark = await html.evaluate(el => el.classList.contains('dark'));
|
||||
|
||||
if (!isAlreadyDark) {
|
||||
const toggleButton = page.getByRole('button', { name: /dark mode|Dunkelmodus/i });
|
||||
await toggleButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
// Verify the main layout container has dark background
|
||||
const mainContainer = page.locator('.h-screen.bg-neutral-50, .h-screen.dark\\:bg-neutral-950').first();
|
||||
const bgColor = await mainContainer.evaluate(el => getComputedStyle(el).backgroundColor);
|
||||
|
||||
// In dark mode, background should be very dark (close to black)
|
||||
// neutral-950 is approximately rgb(10, 10, 10)
|
||||
expect(bgColor).not.toBe('rgb(255, 255, 255)'); // Not white
|
||||
expect(bgColor).not.toBe('rgba(0, 0, 0, 0)'); // Not transparent
|
||||
|
||||
// Clean up
|
||||
const toggleButton = page.getByRole('button', { name: /light mode|Hellmodus/i });
|
||||
await toggleButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
});
|
||||
|
||||
test('dark mode preference is stored in localStorage', async ({ page }, testInfo) => {
|
||||
if (testInfo.project.name === 'mobile-chrome') {
|
||||
test.skip('Dark mode toggle validated on desktop viewport');
|
||||
}
|
||||
|
||||
await loginToAdmin(page);
|
||||
|
||||
// Enable dark mode
|
||||
const html = page.locator('html');
|
||||
const isAlreadyDark = await html.evaluate(el => el.classList.contains('dark'));
|
||||
|
||||
if (!isAlreadyDark) {
|
||||
const toggleButton = page.getByRole('button', { name: /dark mode|Dunkelmodus/i });
|
||||
await toggleButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
// Verify dark mode preference is stored in localStorage
|
||||
const storedPreference = await page.evaluate(() => localStorage.getItem('admin-dark-mode'));
|
||||
expect(storedPreference).toBe('dark');
|
||||
|
||||
// Clean up - toggle back to light
|
||||
const toggleButton = page.getByRole('button', { name: /light mode|Hellmodus/i });
|
||||
await toggleButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Verify light mode preference is stored
|
||||
const lightPreference = await page.evaluate(() => localStorage.getItem('admin-dark-mode'));
|
||||
expect(lightPreference).toBe('light');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Settings Page Dark Mode', () => {
|
||||
test('settings page tabs render correctly in dark mode', async ({ page }, testInfo) => {
|
||||
if (testInfo.project.name === 'mobile-chrome') {
|
||||
test.skip('Settings dark mode validated on desktop viewport');
|
||||
}
|
||||
|
||||
await loginToAdmin(page);
|
||||
|
||||
// Enable dark mode
|
||||
const html = page.locator('html');
|
||||
const isAlreadyDark = await html.evaluate(el => el.classList.contains('dark'));
|
||||
|
||||
if (!isAlreadyDark) {
|
||||
const toggleButton = page.getByRole('button', { name: /dark mode|Dunkelmodus/i });
|
||||
await toggleButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
// Go to settings
|
||||
await page.goto('/admin/settings');
|
||||
await expect(page.getByRole('heading', { name: /Settings|Einstellungen/i })).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Verify settings heading has dark text style
|
||||
const heading = page.getByRole('heading', { name: /Settings|Einstellungen/i }).first();
|
||||
const headingColor = await heading.evaluate(el => getComputedStyle(el).color);
|
||||
|
||||
// In dark mode, text should be light (not dark)
|
||||
// neutral-100 is approximately rgb(245, 245, 245)
|
||||
const [r, g, b] = headingColor.match(/\d+/g).map(Number);
|
||||
expect(r + g + b).toBeGreaterThan(500); // Light colored text
|
||||
|
||||
// Verify tab buttons are visible (use exact: true to avoid matching save buttons)
|
||||
await expect(page.getByRole('button', { name: 'General', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: /^SEO & Robots$|^SEO$/ })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Security', exact: true })).toBeVisible();
|
||||
|
||||
// Clean up
|
||||
const toggleButton = page.getByRole('button', { name: /light mode|Hellmodus/i });
|
||||
await toggleButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Gallery Theme Color Mode', () => {
|
||||
test('branding page has color mode selector', async ({ page }, testInfo) => {
|
||||
if (testInfo.project.name === 'mobile-chrome') {
|
||||
test.skip('Theme customizer validated on desktop viewport');
|
||||
}
|
||||
|
||||
await loginToAdmin(page);
|
||||
|
||||
await page.goto('/admin/branding');
|
||||
await expect(page.getByText(/Theme|Themen/i).first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Look for the color mode selector
|
||||
await expect(page.getByText(/Color Mode|Farbmodus/i)).toBeVisible();
|
||||
|
||||
// Verify the mode buttons exist
|
||||
await expect(page.getByRole('button', { name: /^Light$|^Hell$/i })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: /^Dark$|^Dunkel$/i })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: /^Auto$/i })).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,158 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
const ADMIN_EMAIL = process.env.ADMIN_EMAIL || '[email protected]';
|
||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'Admin!234';
|
||||
|
||||
// Helper to login and navigate to settings
|
||||
async function loginAndGoToSeoSettings(page) {
|
||||
await page.goto('/admin/login');
|
||||
await page.getByLabel(/Email|E-Mail/i).fill(ADMIN_EMAIL);
|
||||
await page.getByLabel(/Password|Passwort/i).fill(ADMIN_PASSWORD);
|
||||
await page.getByRole('button', { name: /Sign In|Log in|Anmelden/i }).click();
|
||||
await expect(page.getByRole('heading', { name: /Dashboard|Übersicht/i })).toBeVisible({ timeout: 20000 });
|
||||
|
||||
await page.goto('/admin/settings');
|
||||
// Click on SEO tab
|
||||
const seoTab = page.getByRole('button', { name: /SEO|Robots/i });
|
||||
await seoTab.click();
|
||||
await expect(page.getByRole('heading', { name: /Search Engine Indexing|Suchmaschinen-Indexierung/i })).toBeVisible({ timeout: 10000 });
|
||||
}
|
||||
|
||||
test.describe('SEO Settings Tab', () => {
|
||||
test('SEO tab renders all sections correctly', async ({ page }, testInfo) => {
|
||||
if (testInfo.project.name === 'mobile-chrome') {
|
||||
test.skip('SEO settings UI validated on desktop viewport');
|
||||
}
|
||||
|
||||
await loginAndGoToSeoSettings(page);
|
||||
|
||||
// Verify all three cards are visible
|
||||
await expect(page.getByRole('heading', { name: /Search Engine Indexing|Suchmaschinen-Indexierung/i })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: /AI & Bot Blocking|KI- & Bot-Blockierung/i })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: /Meta Tags|Meta-Tags/i })).toBeVisible();
|
||||
|
||||
// Verify key form elements
|
||||
await expect(page.getByLabel(/Allow search engine indexing|Suchmaschinen-Indexierung erlauben/i)).toBeVisible();
|
||||
await expect(page.getByLabel(/Block AI\/LLM crawlers|KI-\/LLM-Crawler blockieren/i)).toBeVisible();
|
||||
await expect(page.getByLabel(/Add noindex meta tag|noindex-Meta-Tag hinzufügen/i)).toBeVisible();
|
||||
});
|
||||
|
||||
test('can toggle indexing and save settings', async ({ page }, testInfo) => {
|
||||
if (testInfo.project.name === 'mobile-chrome') {
|
||||
test.skip('SEO settings UI validated on desktop viewport');
|
||||
}
|
||||
|
||||
await loginAndGoToSeoSettings(page);
|
||||
|
||||
const indexingToggle = page.getByLabel(/Allow search engine indexing|Suchmaschinen-Indexierung erlauben/i);
|
||||
const initialState = await indexingToggle.isChecked();
|
||||
|
||||
// Toggle the setting
|
||||
await indexingToggle.click();
|
||||
|
||||
// Save
|
||||
const saveButton = page.getByRole('button', { name: /Save SEO Settings|SEO-Einstellungen speichern/i });
|
||||
await saveButton.click();
|
||||
|
||||
// Wait for success toast
|
||||
await expect(page.locator('.Toastify__toast--success')).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Verify toggle state changed
|
||||
const newState = await indexingToggle.isChecked();
|
||||
expect(newState).toBe(!initialState);
|
||||
|
||||
// Revert to original state
|
||||
await indexingToggle.click();
|
||||
await saveButton.click();
|
||||
await expect(page.locator('.Toastify__toast--success')).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test('robots.txt preview updates based on settings', async ({ page }, testInfo) => {
|
||||
if (testInfo.project.name === 'mobile-chrome') {
|
||||
test.skip('SEO settings UI validated on desktop viewport');
|
||||
}
|
||||
|
||||
await loginAndGoToSeoSettings(page);
|
||||
|
||||
// Click show preview button
|
||||
const previewButton = page.getByRole('button', { name: /Show robots\.txt preview|robots\.txt-Vorschau anzeigen/i });
|
||||
await previewButton.click();
|
||||
|
||||
// Verify preview content is visible
|
||||
const previewContent = page.locator('pre');
|
||||
await expect(previewContent).toBeVisible();
|
||||
|
||||
// Verify it contains expected content
|
||||
const previewText = await previewContent.textContent();
|
||||
expect(previewText).toContain('User-agent');
|
||||
expect(previewText).toContain('Disallow');
|
||||
});
|
||||
|
||||
test('can add blocked AI agents', async ({ page }, testInfo) => {
|
||||
if (testInfo.project.name === 'mobile-chrome') {
|
||||
test.skip('SEO settings UI validated on desktop viewport');
|
||||
}
|
||||
|
||||
await loginAndGoToSeoSettings(page);
|
||||
|
||||
// Find the blocked agents section
|
||||
const agentInput = page.getByPlaceholder(/Enter agent name|Agentenname eingeben/i);
|
||||
await expect(agentInput).toBeVisible();
|
||||
|
||||
// Add a new agent
|
||||
const testAgent = 'TestBot-' + Date.now();
|
||||
await agentInput.fill(testAgent);
|
||||
await agentInput.press('Enter');
|
||||
|
||||
// Verify the agent tag appears in the list
|
||||
await expect(page.getByText(testAgent)).toBeVisible();
|
||||
|
||||
// Save and verify it persists
|
||||
const saveButton = page.getByRole('button', { name: /Save SEO Settings|SEO-Einstellungen speichern/i });
|
||||
await saveButton.click();
|
||||
|
||||
// Wait for success toast
|
||||
await expect(page.locator('.Toastify__toast--success')).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Refresh and verify it's still there
|
||||
await page.reload();
|
||||
await page.getByRole('button', { name: /SEO|Robots/i }).click();
|
||||
await expect(page.getByText(testAgent)).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('robots.txt Endpoint', () => {
|
||||
test('returns valid robots.txt from backend', async ({ request }) => {
|
||||
const response = await request.get('/robots.txt');
|
||||
|
||||
expect(response.status()).toBe(200);
|
||||
expect(response.headers()['content-type']).toContain('text/plain');
|
||||
|
||||
const body = await response.text();
|
||||
expect(body).toContain('User-agent');
|
||||
expect(body).toContain('Disallow: /admin');
|
||||
expect(body).toContain('Disallow: /api');
|
||||
});
|
||||
|
||||
test('robots.txt blocks AI crawlers by default', async ({ request }) => {
|
||||
const response = await request.get('/robots.txt');
|
||||
const body = await response.text();
|
||||
|
||||
// Check for some of the default blocked AI agents
|
||||
expect(body).toContain('GPTBot');
|
||||
expect(body).toContain('Claude-Web');
|
||||
expect(body).toContain('Google-Extended');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('SEO Meta Tags', () => {
|
||||
test('public settings include SEO meta flags', async ({ request }) => {
|
||||
const response = await request.get('/api/public/settings');
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
const settings = await response.json();
|
||||
expect(settings).toHaveProperty('seo_meta_noindex');
|
||||
expect(settings).toHaveProperty('seo_meta_nofollow');
|
||||
expect(settings).toHaveProperty('seo_meta_noai');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user