diff --git a/tests/e2e/gui-keyboard.spec.ts b/tests/e2e/gui-keyboard.spec.ts index b4d57c26..666b18d8 100644 --- a/tests/e2e/gui-keyboard.spec.ts +++ b/tests/e2e/gui-keyboard.spec.ts @@ -131,4 +131,69 @@ test.describe("Keyboard Shortcuts - Tool Navigation", () => { await expect(page).toHaveURL("/convert"); }); + + test("Cmd/Ctrl+Alt+5 navigates to Remove Background", async ({ loggedInPage: page }) => { + await page.keyboard.press(`${MOD}+Alt+5`); + + await expect(page).toHaveURL("/remove-background"); + }); + + test("Cmd/Ctrl+Alt+6 navigates to Watermark Text", async ({ loggedInPage: page }) => { + await page.keyboard.press(`${MOD}+Alt+6`); + + await expect(page).toHaveURL("/watermark-text"); + }); + + test("Cmd/Ctrl+Alt+7 navigates to Strip Metadata", async ({ loggedInPage: page }) => { + await page.keyboard.press(`${MOD}+Alt+7`); + + await expect(page).toHaveURL("/strip-metadata"); + }); + + test("Cmd/Ctrl+Alt+8 navigates to Image Info", async ({ loggedInPage: page }) => { + await page.keyboard.press(`${MOD}+Alt+8`); + + await expect(page).toHaveURL("/info"); + }); +}); + +// --------------------------------------------------------------------------- +// Keyboard shortcut suppression in different input types +// --------------------------------------------------------------------------- +test.describe("Keyboard Shortcuts - Input Suppression", () => { + test("Cmd/Ctrl+Shift+D does not toggle theme when focused on search input", async ({ + loggedInPage: page, + }) => { + // Navigate to fullscreen which reliably shows the search input + await page.goto("/fullscreen"); + const searchInput = page.getByPlaceholder(/search/i); + await expect(searchInput).toBeVisible(); + await searchInput.click(); + await searchInput.fill(""); + + const hadDark = await page.evaluate(() => document.documentElement.classList.contains("dark")); + + await page.keyboard.press(`${MOD}+Shift+d`); + await page.waitForTimeout(300); + + const hasDark = await page.evaluate(() => document.documentElement.classList.contains("dark")); + + // Theme should NOT have changed since we were in an input + expect(hasDark).toBe(hadDark); + }); + + test("Cmd/Ctrl+Alt+1 does not navigate when focused on search input", async ({ + loggedInPage: page, + }) => { + await page.goto("/fullscreen"); + const searchInput = page.getByPlaceholder(/search/i); + await expect(searchInput).toBeVisible(); + await searchInput.click(); + + await page.keyboard.press(`${MOD}+Alt+1`); + await page.waitForTimeout(300); + + // Should still be on fullscreen since shortcut was suppressed + await expect(page).toHaveURL("/fullscreen"); + }); }); diff --git a/tests/e2e/gui-navigation.spec.ts b/tests/e2e/gui-navigation.spec.ts index c95b0b24..4ea7eb28 100644 --- a/tests/e2e/gui-navigation.spec.ts +++ b/tests/e2e/gui-navigation.spec.ts @@ -352,6 +352,98 @@ test.describe("Sidebar Navigation", () => { }); }); +// --------------------------------------------------------------------------- +// Footer (desktop only) +// --------------------------------------------------------------------------- +test.describe("Footer", () => { + test("theme toggle button is visible with sun or moon icon", async ({ loggedInPage: page }) => { + const themeBtn = page.locator("button[title='Toggle Theme']"); + await expect(themeBtn).toBeVisible(); + + // Should contain an SVG icon (Sun or Moon) + await expect(themeBtn.locator("svg")).toBeVisible(); + }); + + test("theme toggle switches between sun and moon icons", async ({ loggedInPage: page }) => { + const themeBtn = page.locator("button[title='Toggle Theme']"); + await expect(themeBtn).toBeVisible(); + + const hadDark = await page.evaluate(() => document.documentElement.classList.contains("dark")); + + await themeBtn.click(); + await page.waitForTimeout(300); + + const hasDark = await page.evaluate(() => document.documentElement.classList.contains("dark")); + expect(hasDark).not.toBe(hadDark); + }); + + test("language button is visible and shows English", async ({ loggedInPage: page }) => { + const langBtn = page.locator("button[title='Language']"); + await expect(langBtn).toBeVisible(); + await expect(langBtn).toContainText("English"); + await expect(langBtn.locator("svg")).toBeVisible(); + }); +}); + +// --------------------------------------------------------------------------- +// Drag-and-Drop Upload +// --------------------------------------------------------------------------- +test.describe("Drag-and-Drop Upload", () => { + test("dropzone accepts dropped files via DataTransfer", async ({ loggedInPage: page }) => { + const dropzone = page.locator("section[aria-label='File drop zone']"); + await expect(dropzone).toBeVisible(); + + // Verify the dropzone aria-label and interactive elements + await expect(page.getByText("Drop files here or click the upload button")).toBeVisible(); + + // Upload via the file chooser flow (same onFiles handler as drag-and-drop) + await uploadTestImage(page); + + // After upload, the file info should appear + await expect(page.getByText(/test-image/i).first()).toBeVisible(); + }); +}); + +// --------------------------------------------------------------------------- +// Files Page Layout +// --------------------------------------------------------------------------- +test.describe("Files Page Layout", () => { + test("desktop shows three-column layout with nav, list, and details", async ({ + loggedInPage: page, + }) => { + await page.goto("/files"); + + // Left nav column with "My Files" + await expect(page.getByText("My Files")).toBeVisible(); + // Nav items: Recent and Upload Files + await expect(page.getByRole("button", { name: /recent/i }).first()).toBeVisible(); + await expect(page.getByRole("button", { name: /upload files/i }).first()).toBeVisible(); + }); + + test("mobile shows tabbed layout with Recent and Upload tabs", async ({ browser }) => { + const context = await browser.newContext({ + viewport: { width: 375, height: 667 }, + }); + const page = await context.newPage(); + await page.goto("/login"); + await page.getByLabel("Username").fill("admin"); + await page.getByLabel("Password").fill("admin"); + await page.getByRole("button", { name: /login/i }).click(); + await page.waitForURL("/", { timeout: 15_000 }); + + await page.goto("/files"); + + // Mobile tabs should be visible + await expect(page.getByRole("button", { name: "Recent" })).toBeVisible(); + await expect(page.getByRole("button", { name: "Upload" })).toBeVisible(); + + // Desktop nav "My Files" heading should not be visible (hidden md:block) + await expect(page.getByText("My Files")).not.toBeVisible(); + + await context.close(); + }); +}); + // --------------------------------------------------------------------------- // Routing Edge Cases // --------------------------------------------------------------------------- @@ -388,4 +480,78 @@ test.describe("Routing Edge Cases", () => { await expect(page.getByRole("heading", { name: "Privacy Policy" })).toBeVisible(); await expect(page.getByText("Back to app")).toBeVisible(); }); + + test("/privacy Back to app link navigates home", async ({ loggedInPage: page }) => { + await page.goto("/privacy"); + + await page.getByText("Back to app").click(); + await expect(page).toHaveURL("/"); + }); + + test("/analytics-consent page renders consent UI", async ({ browser }) => { + // Use unauthenticated context since analytics-consent is unguarded + const context = await browser.newContext({ + storageState: { cookies: [], origins: [] }, + }); + const page = await context.newPage(); + await page.goto("/analytics-consent"); + + // The page shows a heading and two buttons + await expect(page.getByText("Help improve SnapOtter")).toBeVisible(); + await expect(page.getByRole("button", { name: "Sure, sounds good" })).toBeVisible(); + await expect(page.getByRole("button", { name: "Not right now" })).toBeVisible(); + + await context.close(); + }); + + test("legacy /color-effects redirects to /adjust-colors", async ({ loggedInPage: page }) => { + await page.goto("/color-effects"); + + await expect(page).toHaveURL("/adjust-colors"); + }); +}); + +// --------------------------------------------------------------------------- +// Browser Back/Forward Navigation +// --------------------------------------------------------------------------- +test.describe("Browser Back/Forward Navigation", () => { + test("browser back button returns to previous page", async ({ loggedInPage: page }) => { + // Navigate: Home -> Fullscreen -> back should return to Home + await page.goto("/fullscreen"); + await expect(page).toHaveURL("/fullscreen"); + + await page.goBack(); + await expect(page).toHaveURL("/"); + }); + + test("browser forward button returns to next page after going back", async ({ + loggedInPage: page, + }) => { + await page.goto("/fullscreen"); + await expect(page).toHaveURL("/fullscreen"); + + await page.goBack(); + await expect(page).toHaveURL("/"); + + await page.goForward(); + await expect(page).toHaveURL("/fullscreen"); + }); + + test("multi-step back/forward through several pages", async ({ loggedInPage: page }) => { + // Navigate: Home -> /automate -> /files -> back -> back -> forward + await page.goto("/automate"); + await expect(page).toHaveURL("/automate"); + + await page.goto("/files"); + await expect(page).toHaveURL("/files"); + + await page.goBack(); + await expect(page).toHaveURL("/automate"); + + await page.goBack(); + await expect(page).toHaveURL("/"); + + await page.goForward(); + await expect(page).toHaveURL("/automate"); + }); }); diff --git a/tests/e2e/gui-responsive.spec.ts b/tests/e2e/gui-responsive.spec.ts index f6f66294..3ffd9a45 100644 --- a/tests/e2e/gui-responsive.spec.ts +++ b/tests/e2e/gui-responsive.spec.ts @@ -147,6 +147,38 @@ test.describe("Responsive - Tablet (768x1024)", () => { await context.close(); }); + + test("files page is accessible at tablet width", async ({ loggedInPage: page }) => { + await page.goto("/files"); + + await expect(page.getByText("My Files")).toBeVisible(); + await expect(page.getByRole("button", { name: /recent/i }).first()).toBeVisible(); + + const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth); + const clientWidth = await page.evaluate(() => document.documentElement.clientWidth); + expect(scrollWidth).toBeLessThanOrEqual(clientWidth); + }); + + test("no horizontal overflow on files page", async ({ loggedInPage: page }) => { + await page.goto("/files"); + + const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth); + const clientWidth = await page.evaluate(() => document.documentElement.clientWidth); + expect(scrollWidth).toBeLessThanOrEqual(clientWidth); + }); + + test("settings dialog fits within tablet viewport", async ({ loggedInPage: page }) => { + const sidebar = page.locator("aside"); + // On tablet, sidebar may or may not be visible; use it if visible, otherwise use menu + if (await sidebar.isVisible({ timeout: 2000 }).catch(() => false)) { + await sidebar.getByText("Settings").click(); + } + await expect(page.getByRole("heading", { name: "General" })).toBeVisible({ timeout: 10_000 }); + + const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth); + const clientWidth = await page.evaluate(() => document.documentElement.clientWidth); + expect(scrollWidth).toBeLessThanOrEqual(clientWidth); + }); }); // --------------------------------------------------------------------------- @@ -318,4 +350,57 @@ test.describe("Responsive - Mobile (375x667)", () => { const clientWidth = await page.evaluate(() => document.documentElement.clientWidth); expect(scrollWidth).toBeLessThanOrEqual(clientWidth); }); + + test("files page shows mobile tabs instead of desktop nav", async ({ loggedInPage: page }) => { + await page.goto("/files"); + + // Mobile tabs: "Recent" and "Upload" + await expect(page.getByRole("button", { name: "Recent" })).toBeVisible(); + await expect(page.getByRole("button", { name: "Upload" })).toBeVisible(); + + // Desktop nav "My Files" heading should not be visible + await expect(page.getByText("My Files")).not.toBeVisible(); + }); + + test("no horizontal overflow on files page", async ({ loggedInPage: page }) => { + await page.goto("/files"); + + const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth); + const clientWidth = await page.evaluate(() => document.documentElement.clientWidth); + expect(scrollWidth).toBeLessThanOrEqual(clientWidth); + }); + + test("dropzone is accessible and clickable on mobile", async ({ loggedInPage: page }) => { + // The dropzone should be visible and have the upload button + const dropzone = page.locator("section[aria-label='File drop zone']"); + await expect(dropzone).toBeVisible(); + await expect(page.getByText("Upload from computer")).toBeVisible(); + }); + + test("privacy policy page renders on mobile without overflow", async ({ loggedInPage: page }) => { + await page.goto("/privacy"); + + await expect(page.getByRole("heading", { name: "Privacy Policy" })).toBeVisible(); + + const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth); + const clientWidth = await page.evaluate(() => document.documentElement.clientWidth); + expect(scrollWidth).toBeLessThanOrEqual(clientWidth); + }); + + test("footer theme and language buttons are hidden on mobile", async ({ loggedInPage: page }) => { + // Footer is rendered only on desktop (!isMobile) + await expect(page.locator("button[title='Toggle Theme']")).not.toBeVisible(); + await expect(page.locator("button[title='Language']")).not.toBeVisible(); + }); + + test("settings dialog fits within mobile viewport", async ({ loggedInPage: page }) => { + const bottomNav = page.locator("nav.fixed"); + await bottomNav.getByText("Settings").click(); + + await expect(page.getByRole("heading", { name: "General" })).toBeVisible(); + + const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth); + const clientWidth = await page.evaluate(() => document.documentElement.clientWidth); + expect(scrollWidth).toBeLessThanOrEqual(clientWidth); + }); });