diff --git a/tests/e2e/gui-responsive.spec.ts b/tests/e2e/gui-responsive.spec.ts deleted file mode 100644 index eaff4754..00000000 --- a/tests/e2e/gui-responsive.spec.ts +++ /dev/null @@ -1,891 +0,0 @@ -import { expect, openSettings, test, uploadTestImage } from "./helpers"; - -// --------------------------------------------------------------------------- -// Viewport definitions -// --------------------------------------------------------------------------- -const DESKTOP = { width: 1280, height: 720 }; -const TABLET = { width: 768, height: 1024 }; -const MOBILE = { width: 375, height: 667 }; - -// --------------------------------------------------------------------------- -// Desktop (1280x720) -// --------------------------------------------------------------------------- -test.describe("Responsive - Desktop (1280x720)", () => { - test.use({ viewport: DESKTOP }); - - test("home page shows sidebar and tool panel", async ({ loggedInPage: page }) => { - const sidebar = page.locator("aside"); - await expect(sidebar).toBeVisible(); - - // Tool panel with search - await expect(page.getByPlaceholder(/search/i).first()).toBeVisible(); - - // No mobile bottom nav - await expect(page.locator("nav.fixed").filter({ hasText: "Tools" })).not.toBeVisible(); - }); - - test("home page has no horizontal overflow", async ({ loggedInPage: page }) => { - const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth); - const clientWidth = await page.evaluate(() => document.documentElement.clientWidth); - expect(scrollWidth).toBeLessThanOrEqual(clientWidth); - }); - - test("fullscreen grid shows category cards in multi-column layout", async ({ - loggedInPage: page, - }) => { - await page.goto("/fullscreen"); - - await expect(page.getByText("Essentials")).toBeVisible(); - await expect(page.getByPlaceholder(/search/i)).toBeVisible(); - - const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth); - const clientWidth = await page.evaluate(() => document.documentElement.clientWidth); - expect(scrollWidth).toBeLessThanOrEqual(clientWidth); - }); - - test("tool page shows side-by-side layout", async ({ loggedInPage: page }) => { - await page.goto("/resize"); - - // Tool name visible in the settings panel - await expect(page.getByText("Resize").first()).toBeVisible(); - // Dropzone visible in main area - await expect(page.getByText("Upload from computer")).toBeVisible(); - }); - - test("automate page shows tool palette and pipeline builder side by side", async ({ - loggedInPage: page, - }) => { - await page.goto("/automate"); - - await expect(page.getByText("Tool Palette")).toBeVisible(); - await expect(page.getByText("Pipeline Builder")).toBeVisible(); - }); - - test("login page shows split layout with marketing panel", async ({ browser }) => { - const context = await browser.newContext({ - storageState: { cookies: [], origins: [] }, - viewport: DESKTOP, - }); - const page = await context.newPage(); - await page.goto("/login"); - - await expect(page.getByRole("heading", { name: /login/i })).toBeVisible(); - await expect(page.getByText("Your images. Stay yours.")).toBeVisible(); - - await context.close(); - }); - - test("files page shows three-column layout", async ({ loggedInPage: page }) => { - await page.goto("/files"); - - // Left nav with "My Files" - await expect(page.getByText("My Files")).toBeVisible(); - }); - - test("settings dialog fits within viewport", async ({ loggedInPage: page }) => { - await openSettings(page); - - const dialogBox = page.locator("[class*='max-w']").filter({ hasText: "General" }).first(); - const box = await dialogBox.boundingBox(); - if (box) { - expect(box.x).toBeGreaterThanOrEqual(0); - expect(box.y).toBeGreaterThanOrEqual(0); - expect(box.x + box.width).toBeLessThanOrEqual(DESKTOP.width + 1); - expect(box.y + box.height).toBeLessThanOrEqual(DESKTOP.height + 1); - } - }); - - test("no horizontal overflow on automate page", async ({ loggedInPage: page }) => { - await page.goto("/automate"); - - const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth); - const clientWidth = await page.evaluate(() => document.documentElement.clientWidth); - expect(scrollWidth).toBeLessThanOrEqual(clientWidth); - }); - - test("tool page after upload has no overflow", async ({ loggedInPage: page }) => { - await page.goto("/resize"); - await uploadTestImage(page); - - const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth); - const clientWidth = await page.evaluate(() => document.documentElement.clientWidth); - expect(scrollWidth).toBeLessThanOrEqual(clientWidth); - }); - - test("all sidebar items are within viewport", async ({ loggedInPage: page }) => { - const sidebar = page.locator("aside"); - const box = await sidebar.boundingBox(); - if (box) { - expect(box.x).toBeGreaterThanOrEqual(0); - expect(box.x + box.width).toBeLessThanOrEqual(DESKTOP.width + 1); - } - }); - - test("footer buttons are within viewport", async ({ loggedInPage: page }) => { - const themeBtn = page.locator("button[title='Toggle Theme']"); - await expect(themeBtn).toBeVisible(); - const box = await themeBtn.boundingBox(); - if (box) { - expect(box.x).toBeGreaterThanOrEqual(0); - expect(box.x + box.width).toBeLessThanOrEqual(DESKTOP.width + 1); - } - }); - - test("privacy page has no horizontal overflow", async ({ loggedInPage: page }) => { - await page.goto("/privacy"); - - 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("dropzone is visible and clickable on desktop", async ({ loggedInPage: page }) => { - const dropzone = page.locator("section[aria-label='File drop zone']"); - await expect(dropzone).toBeVisible(); - await expect(page.getByText("Upload from computer")).toBeVisible(); - }); - - test("all text on desktop home page is readable (font-size >= 12px)", async ({ - loggedInPage: page, - }) => { - const smallText = await page.evaluate(() => { - const elements = document.querySelectorAll("p, span, a, button, label, h1, h2, h3, h4, h5"); - let count = 0; - for (const el of elements) { - const style = window.getComputedStyle(el); - const fontSize = Number.parseFloat(style.fontSize); - if (fontSize < 12 && el.textContent && el.textContent.trim().length > 0) { - count++; - } - } - return count; - }); - // Allow a small number of decorative/badge elements with smaller text - expect(smallText).toBeLessThanOrEqual(5); - }); - - test("fullscreen grid interactive elements are reachable", async ({ loggedInPage: page }) => { - await page.goto("/fullscreen"); - - // Search bar should be within viewport - const searchInput = page.getByPlaceholder(/search/i); - await expect(searchInput).toBeVisible(); - const box = await searchInput.boundingBox(); - if (box) { - expect(box.x).toBeGreaterThanOrEqual(0); - expect(box.x + box.width).toBeLessThanOrEqual(DESKTOP.width + 1); - } - - // Toggle details button should be reachable - const toggleBtn = page.getByRole("button", { name: /hide details|show details/i }).first(); - await expect(toggleBtn).toBeVisible(); - }); -}); - -// --------------------------------------------------------------------------- -// Tablet (768x1024) -// --------------------------------------------------------------------------- -test.describe("Responsive - Tablet (768x1024)", () => { - test.use({ viewport: TABLET }); - - test("home page layout renders without horizontal overflow", async ({ loggedInPage: page }) => { - const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth); - const clientWidth = await page.evaluate(() => document.documentElement.clientWidth); - expect(scrollWidth).toBeLessThanOrEqual(clientWidth); - }); - - test("fullscreen grid renders with no overflow", async ({ loggedInPage: page }) => { - await page.goto("/fullscreen"); - - await expect(page.getByPlaceholder(/search/i)).toBeVisible(); - - const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth); - const clientWidth = await page.evaluate(() => document.documentElement.clientWidth); - expect(scrollWidth).toBeLessThanOrEqual(clientWidth); - }); - - test("tool page is accessible", async ({ loggedInPage: page }) => { - await page.goto("/resize"); - - await expect(page.getByText("Resize").first()).toBeVisible(); - await expect(page.getByText("Upload from computer")).toBeVisible(); - }); - - test("automate page is accessible", async ({ loggedInPage: page }) => { - await page.goto("/automate"); - - // Pipeline builder or mobile heading should be visible - await expect(page.getByText(/pipeline|automate/i).first()).toBeVisible(); - }); - - test("login page renders correctly", async ({ browser }) => { - const context = await browser.newContext({ - storageState: { cookies: [], origins: [] }, - viewport: TABLET, - }); - const page = await context.newPage(); - await page.goto("/login"); - - await expect(page.getByRole("heading", { name: /login/i })).toBeVisible(); - await expect(page.getByLabel("Username")).toBeVisible(); - await expect(page.getByLabel("Password")).toBeVisible(); - - 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 }) => { - await openSettings(page); - - 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 automate page", async ({ loggedInPage: page }) => { - await page.goto("/automate"); - - const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth); - const clientWidth = await page.evaluate(() => document.documentElement.clientWidth); - expect(scrollWidth).toBeLessThanOrEqual(clientWidth); - }); - - test("tool page after upload has no overflow", async ({ loggedInPage: page }) => { - await page.goto("/resize"); - await uploadTestImage(page); - - const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth); - const clientWidth = await page.evaluate(() => document.documentElement.clientWidth); - expect(scrollWidth).toBeLessThanOrEqual(clientWidth); - }); - - test("privacy page has no overflow at tablet width", 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("login page has no horizontal overflow at tablet width", async ({ browser }) => { - const context = await browser.newContext({ - storageState: { cookies: [], origins: [] }, - viewport: TABLET, - }); - const page = await context.newPage(); - await page.goto("/login"); - - const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth); - const clientWidth = await page.evaluate(() => document.documentElement.clientWidth); - expect(scrollWidth).toBeLessThanOrEqual(clientWidth); - - await context.close(); - }); - - test("dropzone is visible on tablet home page", async ({ loggedInPage: page }) => { - const dropzone = page.locator("section[aria-label='File drop zone']"); - await expect(dropzone).toBeVisible(); - await expect(page.getByText("Upload from computer")).toBeVisible(); - }); - - test("tool page dropzone is visible on tablet", async ({ loggedInPage: page }) => { - await page.goto("/resize"); - - const dropzone = page.locator("[class*='border-dashed']").first(); - await expect(dropzone).toBeVisible(); - await expect(page.getByText("Upload from computer")).toBeVisible(); - }); - - test("settings dialog bounding box stays within tablet viewport", async ({ - loggedInPage: page, - }) => { - await openSettings(page); - - const dialogBox = page.locator("[class*='max-w']").filter({ hasText: "General" }).first(); - const box = await dialogBox.boundingBox(); - if (box) { - expect(box.x).toBeGreaterThanOrEqual(0); - expect(box.y).toBeGreaterThanOrEqual(0); - expect(box.x + box.width).toBeLessThanOrEqual(TABLET.width + 1); - expect(box.y + box.height).toBeLessThanOrEqual(TABLET.height + 1); - } - }); - - test("fullscreen grid interactive elements are reachable at tablet", async ({ - loggedInPage: page, - }) => { - await page.goto("/fullscreen"); - - const searchInput = page.getByPlaceholder(/search/i); - await expect(searchInput).toBeVisible(); - - // Can interact with search - await searchInput.fill("resize"); - await expect(page.getByRole("link", { name: /^Resize/ }).first()).toBeVisible(); - }); - - test("all text on home page is readable (font-size >= 12px)", async ({ loggedInPage: page }) => { - const smallText = await page.evaluate(() => { - const elements = document.querySelectorAll("p, span, a, button, label, h1, h2, h3, h4, h5"); - let count = 0; - for (const el of elements) { - const style = window.getComputedStyle(el); - const fontSize = Number.parseFloat(style.fontSize); - if (fontSize < 12 && el.textContent && el.textContent.trim().length > 0) { - count++; - } - } - return count; - }); - // Allow a small number of decorative/badge elements with smaller text - expect(smallText).toBeLessThanOrEqual(5); - }); -}); - -// --------------------------------------------------------------------------- -// Mobile (375x667) -// --------------------------------------------------------------------------- -test.describe("Responsive - Mobile (375x667)", () => { - test.use({ viewport: MOBILE }); - - test("home page shows top bar with hamburger menu", async ({ loggedInPage: page }) => { - // Hamburger menu button (Menu icon) - const hamburger = page - .locator("button") - .filter({ has: page.locator("svg") }) - .first(); - await expect(hamburger).toBeVisible(); - - // SnapOtter branding in top bar - await expect(page.getByText("SnapOtter").first()).toBeVisible(); - }); - - test("bottom navigation bar is visible with correct items", async ({ loggedInPage: page }) => { - // Bottom nav bar - const bottomNav = page.locator("nav.fixed"); - await expect(bottomNav).toBeVisible(); - - // Check for nav items: Tools, Automate, Files, Settings - await expect(bottomNav.getByText("Tools")).toBeVisible(); - await expect(bottomNav.getByText("Automate")).toBeVisible(); - await expect(bottomNav.getByText("Files")).toBeVisible(); - await expect(bottomNav.getByText("Settings")).toBeVisible(); - }); - - test("desktop sidebar is not visible on mobile", async ({ loggedInPage: page }) => { - // The aside element used by desktop sidebar should not be visible - await expect(page.locator("aside")).not.toBeVisible(); - }); - - test("hamburger opens sidebar overlay", async ({ loggedInPage: page }) => { - // Click the hamburger button (first button in the top bar) - const topBar = page.locator(".fixed").filter({ hasText: "SnapOtter" }).first(); - const hamburger = topBar.locator("button").first(); - await hamburger.click(); - - // Expanded sidebar overlay should appear with nav items - await expect(page.getByText("Tools").nth(1)).toBeVisible(); - await expect(page.getByText("Automate").nth(1)).toBeVisible(); - }); - - test("no horizontal overflow on home page", async ({ loggedInPage: page }) => { - 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 fullscreen page", async ({ loggedInPage: page }) => { - await page.goto("/fullscreen"); - - 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 tool page", async ({ loggedInPage: page }) => { - await page.goto("/resize"); - - 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 automate page", async ({ loggedInPage: page }) => { - await page.goto("/automate"); - - const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth); - const clientWidth = await page.evaluate(() => document.documentElement.clientWidth); - expect(scrollWidth).toBeLessThanOrEqual(clientWidth); - }); - - test("login page renders without marketing panel on mobile", async ({ browser }) => { - const context = await browser.newContext({ - storageState: { cookies: [], origins: [] }, - viewport: MOBILE, - }); - const page = await context.newPage(); - await page.goto("/login"); - - // Login form should be visible - await expect(page.getByRole("heading", { name: /login/i })).toBeVisible(); - await expect(page.getByLabel("Username")).toBeVisible(); - - // Marketing panel is hidden on mobile (lg:flex means only visible at lg+) - await expect(page.getByText("Your images. Stay yours.")).not.toBeVisible(); - - const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth); - const clientWidth = await page.evaluate(() => document.documentElement.clientWidth); - expect(scrollWidth).toBeLessThanOrEqual(clientWidth); - - await context.close(); - }); - - test("bottom nav Tools link navigates to home", async ({ loggedInPage: page }) => { - await page.goto("/automate"); - const bottomNav = page.locator("nav.fixed"); - await bottomNav.getByText("Tools").click(); - - await expect(page).toHaveURL("/"); - }); - - test("bottom nav Automate link navigates to /automate", async ({ loggedInPage: page }) => { - const bottomNav = page.locator("nav.fixed"); - await bottomNav.getByText("Automate").click(); - - await expect(page).toHaveURL("/automate"); - }); - - test("bottom nav Files link navigates to /files", async ({ loggedInPage: page }) => { - const bottomNav = page.locator("nav.fixed"); - await bottomNav.getByText("Files").click(); - - await expect(page).toHaveURL("/files"); - }); - - test("bottom nav Settings opens settings dialog", async ({ loggedInPage: page }) => { - const bottomNav = page.locator("nav.fixed"); - await bottomNav.getByText("Settings").click(); - - await expect(page.getByRole("heading", { name: "General" })).toBeVisible(); - }); - - test("fullscreen grid tools are accessible on mobile", async ({ loggedInPage: page }) => { - await page.goto("/fullscreen"); - - await expect(page.getByPlaceholder(/search/i)).toBeVisible(); - await expect(page.getByText("Essentials")).toBeVisible(); - - // Tool links should still work - const resizeLink = page.getByRole("link", { name: /^Resize/ }).first(); - await expect(resizeLink).toBeVisible(); - }); - - test("tool page is usable on mobile", async ({ loggedInPage: page }) => { - await page.goto("/resize"); - - await expect(page.getByText("Resize").first()).toBeVisible(); - await expect(page.getByText("Upload from computer")).toBeVisible(); - }); - - test("automate page mobile layout shows process button", async ({ loggedInPage: page }) => { - await page.goto("/automate"); - - await expect(page.getByText("Automate").first()).toBeVisible(); - const processBtn = page.getByRole("button", { name: /process/i }).first(); - await expect(processBtn).toBeVisible(); - await expect(processBtn).toBeDisabled(); - }); - - test("help dialog fits within mobile viewport", async ({ loggedInPage: page }) => { - // Open sidebar, then help - const topBar = page.locator(".fixed").filter({ hasText: "SnapOtter" }).first(); - const hamburger = topBar.locator("button").first(); - await hamburger.click(); - - // Click Help in expanded sidebar - await page.locator(".fixed").filter({ hasText: "Help" }).getByText("Help").click(); - - await expect(page.getByRole("heading", { name: "Help" })).toBeVisible(); - - const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth); - 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); - }); - - test("tool page after upload has no overflow on mobile", async ({ loggedInPage: page }) => { - await page.goto("/resize"); - await uploadTestImage(page); - - const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth); - const clientWidth = await page.evaluate(() => document.documentElement.clientWidth); - expect(scrollWidth).toBeLessThanOrEqual(clientWidth); - }); - - test("all text on mobile home page is readable (font-size >= 12px)", async ({ - loggedInPage: page, - }) => { - const smallText = await page.evaluate(() => { - const elements = document.querySelectorAll("p, span, a, button, label, h1, h2, h3, h4, h5"); - let count = 0; - for (const el of elements) { - const style = window.getComputedStyle(el); - const fontSize = Number.parseFloat(style.fontSize); - if (fontSize < 12 && el.textContent && el.textContent.trim().length > 0) { - count++; - } - } - return count; - }); - // Allow a small number of decorative/badge elements with smaller text - expect(smallText).toBeLessThanOrEqual(5); - }); - - test("bottom nav items are all within viewport bounds", async ({ loggedInPage: page }) => { - const bottomNav = page.locator("nav.fixed"); - const box = await bottomNav.boundingBox(); - if (box) { - expect(box.x).toBeGreaterThanOrEqual(0); - expect(box.x + box.width).toBeLessThanOrEqual(MOBILE.width + 1); - expect(box.y + box.height).toBeLessThanOrEqual(MOBILE.height + 1); - } - }); - - test("hamburger opens sidebar overlay with backdrop blur", async ({ loggedInPage: page }) => { - const topBar = page.locator(".fixed").filter({ hasText: "SnapOtter" }).first(); - const hamburger = topBar.locator("button").first(); - await hamburger.click(); - - // Backdrop should have backdrop-blur-sm class - const backdrop = page.locator("[class*='backdrop-blur']").first(); - await expect(backdrop).toBeVisible(); - }); - - test("login page all interactive elements reachable on mobile", async ({ browser }) => { - const context = await browser.newContext({ - storageState: { cookies: [], origins: [] }, - viewport: MOBILE, - }); - const page = await context.newPage(); - await page.goto("/login"); - - // Username input should be reachable and within viewport - const usernameInput = page.getByLabel("Username"); - await expect(usernameInput).toBeVisible(); - const box = await usernameInput.boundingBox(); - if (box) { - expect(box.x).toBeGreaterThanOrEqual(0); - expect(box.x + box.width).toBeLessThanOrEqual(MOBILE.width + 1); - } - - // Password input reachable - await expect(page.getByLabel("Password")).toBeVisible(); - - // Login button reachable - await expect(page.getByRole("button", { name: /login/i })).toBeVisible(); - - await context.close(); - }); - - test("SnapOtter branding text is readable on mobile", async ({ loggedInPage: page }) => { - const branding = page.getByText("SnapOtter").first(); - await expect(branding).toBeVisible(); - - const fontSize = await branding.evaluate((el) => - Number.parseFloat(window.getComputedStyle(el).fontSize), - ); - // Branding text should be at least 14px to be readable - expect(fontSize).toBeGreaterThanOrEqual(14); - }); - - test("no horizontal overflow on login page", async ({ browser }) => { - const context = await browser.newContext({ - storageState: { cookies: [], origins: [] }, - viewport: MOBILE, - }); - const page = await context.newPage(); - await page.goto("/login"); - - const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth); - const clientWidth = await page.evaluate(() => document.documentElement.clientWidth); - expect(scrollWidth).toBeLessThanOrEqual(clientWidth); - - await context.close(); - }); - - test("no horizontal overflow on editor page", async ({ loggedInPage: page }) => { - await page.goto("/editor"); - - 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 change-password page", async ({ loggedInPage: page }) => { - await page.goto("/change-password"); - - const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth); - const clientWidth = await page.evaluate(() => document.documentElement.clientWidth); - expect(scrollWidth).toBeLessThanOrEqual(clientWidth); - }); - - test("bottom nav active item shows visual indicator", async ({ loggedInPage: page }) => { - const bottomNav = page.locator("nav.fixed"); - await bottomNav.getByText("Automate").click(); - await expect(page).toHaveURL("/automate"); - - // The active item should have distinct styling (e.g. text color) - const activeItem = bottomNav.getByText("Automate"); - await expect(activeItem).toBeVisible(); - }); - - test("multiple tool pages have no overflow on mobile", async ({ loggedInPage: page }) => { - const tools = ["compress", "convert", "crop", "adjust-colors"]; - for (const tool of tools) { - await page.goto(`/${tool}`); - const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth); - const clientWidth = await page.evaluate(() => document.documentElement.clientWidth); - expect(scrollWidth).toBeLessThanOrEqual(clientWidth); - } - }); - - test("analytics consent page has no overflow on mobile", async ({ browser }) => { - const context = await browser.newContext({ - storageState: { cookies: [], origins: [] }, - viewport: MOBILE, - }); - const page = await context.newPage(); - await page.goto("/analytics-consent"); - - await expect(page.getByText("Help improve SnapOtter")).toBeVisible(); - - const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth); - const clientWidth = await page.evaluate(() => document.documentElement.clientWidth); - expect(scrollWidth).toBeLessThanOrEqual(clientWidth); - - await context.close(); - }); -}); - -// --------------------------------------------------------------------------- -// Cross-viewport parameterized overflow tests -// --------------------------------------------------------------------------- -test.describe("Responsive - Cross-Viewport Overflow", () => { - const VIEWPORTS = [ - { name: "Desktop", ...DESKTOP }, - { name: "Tablet", ...TABLET }, - { name: "Mobile", ...MOBILE }, - ]; - - const PAGES = [ - { path: "/", label: "Home" }, - { path: "/fullscreen", label: "Fullscreen Grid" }, - { path: "/automate", label: "Automate" }, - { path: "/files", label: "Files" }, - { path: "/editor", label: "Editor" }, - { path: "/privacy", label: "Privacy" }, - ]; - - for (const vp of VIEWPORTS) { - for (const pg of PAGES) { - test(`${pg.label} has no overflow at ${vp.name} (${vp.width}x${vp.height})`, async ({ - browser, - }) => { - const context = await browser.newContext({ - viewport: { width: vp.width, height: vp.height }, - }); - 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(pg.path); - await page.waitForTimeout(500); - - const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth); - const clientWidth = await page.evaluate(() => document.documentElement.clientWidth); - expect(scrollWidth).toBeLessThanOrEqual(clientWidth); - - await context.close(); - }); - } - } -}); - -// --------------------------------------------------------------------------- -// Desktop - Additional Layout Checks -// --------------------------------------------------------------------------- -test.describe("Responsive - Desktop Additional", () => { - test.use({ viewport: DESKTOP }); - - test("editor page renders within desktop viewport", async ({ loggedInPage: page }) => { - await page.goto("/editor"); - - const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth); - const clientWidth = await page.evaluate(() => document.documentElement.clientWidth); - expect(scrollWidth).toBeLessThanOrEqual(clientWidth); - }); - - test("sidebar width is consistent across pages", async ({ loggedInPage: page }) => { - const sidebar = page.locator("aside"); - const homeBox = await sidebar.boundingBox(); - - await page.goto("/fullscreen"); - const fullscreenBox = await page.locator("aside").boundingBox(); - - if (homeBox && fullscreenBox) { - expect(homeBox.width).toBe(fullscreenBox.width); - } - }); - - test("login page marketing panel is visible at desktop width", async ({ browser }) => { - const context = await browser.newContext({ - storageState: { cookies: [], origins: [] }, - viewport: DESKTOP, - }); - const page = await context.newPage(); - await page.goto("/login"); - - await expect(page.getByText("Your images. Stay yours.")).toBeVisible(); - - await context.close(); - }); -}); - -// --------------------------------------------------------------------------- -// Tablet - Additional Layout Checks -// --------------------------------------------------------------------------- -test.describe("Responsive - Tablet Additional", () => { - test.use({ viewport: TABLET }); - - test("editor page is accessible at tablet width", async ({ loggedInPage: page }) => { - await page.goto("/editor"); - - const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth); - const clientWidth = await page.evaluate(() => document.documentElement.clientWidth); - expect(scrollWidth).toBeLessThanOrEqual(clientWidth); - }); - - test("login page at tablet shows or hides marketing panel", async ({ browser }) => { - const context = await browser.newContext({ - storageState: { cookies: [], origins: [] }, - viewport: TABLET, - }); - const page = await context.newPage(); - await page.goto("/login"); - - // Login form should be visible regardless - await expect(page.getByRole("heading", { name: /login/i })).toBeVisible(); - - // Marketing panel visibility depends on breakpoint - // At 768px, lg breakpoint is 1024px, so marketing is hidden - await expect(page.getByText("Your images. Stay yours.")).not.toBeVisible(); - - await context.close(); - }); - - test("analytics consent page has no overflow at tablet", async ({ browser }) => { - const context = await browser.newContext({ - storageState: { cookies: [], origins: [] }, - viewport: TABLET, - }); - const page = await context.newPage(); - await page.goto("/analytics-consent"); - - const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth); - const clientWidth = await page.evaluate(() => document.documentElement.clientWidth); - expect(scrollWidth).toBeLessThanOrEqual(clientWidth); - - await context.close(); - }); - - test("change-password page has no overflow at tablet", async ({ loggedInPage: page }) => { - await page.goto("/change-password"); - - const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth); - const clientWidth = await page.evaluate(() => document.documentElement.clientWidth); - expect(scrollWidth).toBeLessThanOrEqual(clientWidth); - }); -}); diff --git a/tests/e2e/gui-visual-mobile.spec.ts b/tests/e2e/gui-visual-mobile.spec.ts deleted file mode 100644 index cd1054e1..00000000 --- a/tests/e2e/gui-visual-mobile.spec.ts +++ /dev/null @@ -1,543 +0,0 @@ -import { expect, test, uploadTestImage } from "./helpers"; - -const MOD = process.platform === "darwin" ? "Meta" : "Control"; - -// --------------------------------------------------------------------------- -// Helper: toggle theme via keyboard shortcut (Cmd/Ctrl+Shift+D) -// On mobile the Footer with the theme toggle button is not rendered, so -// the keyboard shortcut is the reliable way to switch themes. -// --------------------------------------------------------------------------- -async function setTheme(page: import("@playwright/test").Page, theme: "light" | "dark") { - const isDark = await page.evaluate(() => document.documentElement.classList.contains("dark")); - const wantDark = theme === "dark"; - if (isDark !== wantDark) { - await page.keyboard.press(`${MOD}+Shift+d`); - await page.waitForTimeout(300); - } -} - -// --------------------------------------------------------------------------- -// Helper: take a themed screenshot pair (light + dark) -// --------------------------------------------------------------------------- -async function takeThemedScreenshots(page: import("@playwright/test").Page, baseName: string) { - // Light theme - await setTheme(page, "light"); - await expect(page).toHaveScreenshot(`mobile-${baseName}-light.png`, { - fullPage: false, - }); - - // Dark theme - await setTheme(page, "dark"); - await expect(page).toHaveScreenshot(`mobile-${baseName}-dark.png`, { - fullPage: false, - }); - - // Reset to light for next test - await setTheme(page, "light"); -} - -// --------------------------------------------------------------------------- -// Mobile visual regression: 375x667 -// --------------------------------------------------------------------------- -test.describe("Visual Mobile (375x667)", () => { - test.use({ viewport: { width: 375, height: 667 } }); - - // ---- Login page (unauthenticated) ---- - test.describe("Login page", () => { - test.use({ storageState: { cookies: [], origins: [] } }); - - test("login page stacked layout - light and dark", async ({ page }) => { - await page.goto("/login"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - // Verify stacked layout: marketing panel should be hidden on mobile - await expect(page.getByRole("heading", { name: /login/i })).toBeVisible(); - await expect(page.getByText("Your images. Stay yours.")).not.toBeVisible(); - - await takeThemedScreenshots(page, "login-empty"); - }); - - test("login page filled with error - light and dark", async ({ page }) => { - await page.goto("/login"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - // Fill in invalid credentials and submit - await page.getByLabel("Username").fill("wronguser"); - await page.getByLabel("Password").fill("wrongpassword"); - await page.getByRole("button", { name: /login/i }).click(); - - // Wait for the error message to appear - await page.waitForTimeout(1000); - await expect(page.getByText(/invalid|incorrect|failed/i).first()).toBeVisible(); - - // Verify error fits within mobile viewport without overflow - await takeThemedScreenshots(page, "login-error"); - }); - }); - - // ---- Home page (empty, no file uploaded) ---- - test("home page empty - light and dark", async ({ loggedInPage: page }) => { - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - // Verify mobile layout: top bar with hamburger menu visible, no desktop sidebar - await expect(page.getByText("SnapOtter").first()).toBeVisible(); - await expect(page.locator("aside")).not.toBeVisible(); - - // Hamburger menu button should be visible on mobile - const hamburger = page - .locator( - "button[aria-label*='menu' i], button[aria-label*='Menu' i], button[class*='hamburger' i]", - ) - .first(); - const hasHamburger = await hamburger.isVisible({ timeout: 2000 }).catch(() => false); - if (hasHamburger) { - await expect(hamburger).toBeVisible(); - } - - // Bottom navigation bar visible - const bottomNav = page.locator("nav.fixed"); - await expect(bottomNav).toBeVisible(); - - await takeThemedScreenshots(page, "home-empty"); - }); - - // ---- Home page (file uploaded, Quick Actions visible) ---- - test("home page with file uploaded - light and dark", async ({ loggedInPage: page }) => { - await uploadTestImage(page); - await page.waitForTimeout(500); - - await expect(page.getByText("Quick Actions").first()).toBeVisible(); - - await takeThemedScreenshots(page, "home-uploaded"); - }); - - // ---- Fullscreen grid page (details shown - default) ---- - test("fullscreen grid details shown - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/fullscreen"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - await expect(page.getByText("Hide Details")).toBeVisible(); - - await takeThemedScreenshots(page, "fullscreen-details-shown"); - }); - - // ---- Fullscreen grid page (details hidden) ---- - test("fullscreen grid details hidden - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/fullscreen"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - // Toggle details off - await page.getByText("Hide Details").click(); - await page.waitForTimeout(300); - await expect(page.getByText("Show Details")).toBeVisible(); - - await takeThemedScreenshots(page, "fullscreen-details-hidden"); - }); - - // ---- Automate page (empty pipeline) ---- - test("automate page empty pipeline - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/automate"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - await expect(page.getByText(/pipeline|automate/i).first()).toBeVisible(); - - await takeThemedScreenshots(page, "automate-empty"); - }); - - // ---- Automate page (3 steps added + file uploaded) ---- - test("automate page with 3 steps and file - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/automate"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - // Add 3 pipeline steps - const resizeBtn = page.getByRole("button", { name: /resize/i }).first(); - const compressBtn = page.getByRole("button", { name: /compress/i }).first(); - const convertBtn = page.getByRole("button", { name: /convert/i }).first(); - - await resizeBtn.click(); - await page.waitForTimeout(300); - await compressBtn.click(); - await page.waitForTimeout(300); - await convertBtn.click(); - await page.waitForTimeout(300); - - // Upload a file to the pipeline - await uploadTestImage(page); - await page.waitForTimeout(500); - - await takeThemedScreenshots(page, "automate-3steps-file"); - }); - - // ---- Files page (empty) ---- - test("files page empty - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/files"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - // Verify no sidebar on mobile - await expect(page.locator("aside")).not.toBeVisible(); - - await takeThemedScreenshots(page, "files-empty"); - }); - - // ---- Settings dialog - General tab ---- - test("settings dialog general tab - light and dark", async ({ loggedInPage: page }) => { - // On mobile, open settings from the bottom nav bar - const bottomNav = page.locator("nav.fixed"); - await bottomNav.getByText("Settings").click(); - await expect(page.getByRole("heading", { name: "General" })).toBeVisible(); - await page.waitForTimeout(500); - - // Verify settings modal scrolls within mobile viewport - const dialog = page.getByRole("dialog"); - if (await dialog.isVisible({ timeout: 2000 }).catch(() => false)) { - const dialogBox = await dialog.boundingBox(); - if (dialogBox) { - expect(dialogBox.x + dialogBox.width).toBeLessThanOrEqual(375 + 1); - expect(dialogBox.y + dialogBox.height).toBeLessThanOrEqual(667 + 1); - } - } - - await takeThemedScreenshots(page, "settings-general"); - }); - - // ---- Settings dialog - People tab ---- - test("settings dialog people tab - light and dark", async ({ loggedInPage: page }) => { - const bottomNav = page.locator("nav.fixed"); - await bottomNav.getByText("Settings").click(); - await expect(page.getByRole("heading", { name: "General" })).toBeVisible(); - - // Navigate to People tab - await page.getByRole("button", { name: "People" }).click(); - await page.waitForTimeout(500); - - await takeThemedScreenshots(page, "settings-people"); - }); - - // ---- Settings dialog - About tab ---- - test("settings dialog about tab - light and dark", async ({ loggedInPage: page }) => { - const bottomNav = page.locator("nav.fixed"); - await bottomNav.getByText("Settings").click(); - await expect(page.getByRole("heading", { name: "General" })).toBeVisible(); - - // Navigate to About tab - await page.getByRole("button", { name: "About" }).click(); - await page.waitForTimeout(500); - - await takeThemedScreenshots(page, "settings-about"); - }); - - // ---- Help dialog ---- - test("help dialog - light and dark", async ({ loggedInPage: page }) => { - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - // On mobile, help is accessed via the bottom nav or hamburger menu - const bottomNav = page.locator("nav.fixed"); - const helpBtn = bottomNav.getByText("Help"); - if (await helpBtn.isVisible({ timeout: 2000 }).catch(() => false)) { - await helpBtn.click(); - } else { - // Fall back to keyboard shortcut or any visible help trigger - await page.getByRole("button", { name: /help/i }).first().click(); - } - const dialog = page.getByRole("dialog"); - await dialog.waitFor({ state: "visible", timeout: 5000 }); - await page.waitForTimeout(500); - - // Verify dialog (modal) scrolls within mobile viewport and does not overflow - const dialogBox = await dialog.boundingBox(); - if (dialogBox) { - expect(dialogBox.x).toBeGreaterThanOrEqual(0); - expect(dialogBox.y).toBeGreaterThanOrEqual(0); - expect(dialogBox.x + dialogBox.width).toBeLessThanOrEqual(375 + 1); - expect(dialogBox.y + dialogBox.height).toBeLessThanOrEqual(667 + 1); - } - - await takeThemedScreenshots(page, "help-dialog"); - }); - - // ---- Tool page - resize (empty, no file) ---- - test("resize tool empty - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/resize"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - // Verify mobile layout: dropzone fills viewport width, no sidebar - await expect(page.locator("aside")).not.toBeVisible(); - - // Dropzone should fill the mobile viewport width - const dropzone = page.locator("[class*='border-dashed']").first(); - if (await dropzone.isVisible({ timeout: 2000 }).catch(() => false)) { - const dropzoneBox = await dropzone.boundingBox(); - if (dropzoneBox) { - // Dropzone should span most of the 375px viewport width (allow padding) - expect(dropzoneBox.width).toBeGreaterThan(300); - expect(dropzoneBox.x + dropzoneBox.width).toBeLessThanOrEqual(375 + 1); - } - } - - await takeThemedScreenshots(page, "tool-resize-empty"); - }); - - // ---- Tool page - resize (file uploaded, settings visible) ---- - test("resize tool with file - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/resize"); - await uploadTestImage(page); - await page.waitForTimeout(500); - - // On mobile, tool settings should be collapsed by default (toggled via button) - const settingsToggle = page.getByRole("button", { name: /settings/i }).first(); - const settingsPanel = page.locator("[class*='settings'], [class*='Settings']").first(); - const isPanelVisible = await settingsPanel.isVisible({ timeout: 2000 }).catch(() => false); - // If settings are collapsed, expand them for the screenshot - if (!isPanelVisible && (await settingsToggle.isVisible({ timeout: 1000 }).catch(() => false))) { - await settingsToggle.click(); - await page.waitForTimeout(300); - } - - await takeThemedScreenshots(page, "tool-resize-settings"); - }); - - // ---- Tool page - compress (before-after result) ---- - test("compress tool before-after result - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/compress"); - await page.waitForLoadState("networkidle"); - - await uploadTestImage(page); - await page.waitForTimeout(1000); - - // Wait for the before-after slider to appear - const slider = page.locator("[class*='before-after'], [class*='BeforeAfter']").first(); - await slider.waitFor({ state: "visible", timeout: 15000 }).catch(() => {}); - await page.waitForTimeout(500); - - await takeThemedScreenshots(page, "tool-compress-result"); - }); - - // ---- Tool page - crop (interactive canvas) ---- - test("crop tool interactive canvas - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/crop"); - await page.waitForLoadState("networkidle"); - - await uploadTestImage(page); - await page.waitForTimeout(1000); - - const canvas = page.locator("canvas").first(); - await canvas.waitFor({ state: "visible", timeout: 10000 }).catch(() => {}); - await page.waitForTimeout(500); - - await takeThemedScreenshots(page, "tool-crop-canvas"); - }); - - // ---- Tool page - qr-generate (no-dropzone, QR preview) ---- - test("qr-generate tool with preview - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/qr-generate"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - // Enter text to generate a QR code - const textInput = page.locator("input[type='text'], textarea").first(); - await textInput.fill("https://snapotter.com"); - await page.waitForTimeout(1000); - - // Wait for QR preview to render - const preview = page.locator("img, canvas, svg").first(); - await preview.waitFor({ state: "visible", timeout: 10000 }).catch(() => {}); - await page.waitForTimeout(500); - - await takeThemedScreenshots(page, "tool-qr-generate-preview"); - }); - - // ---- Tool page - collage (template selection) ---- - test("collage tool template selection - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/collage"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - await takeThemedScreenshots(page, "tool-collage-templates"); - }); - - // ---- Analytics consent page ---- - test.describe("Analytics consent page", () => { - test.use({ storageState: { cookies: [], origins: [] } }); - - test("analytics consent page - light and dark", async ({ page }) => { - await page.goto("/analytics-consent"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - await takeThemedScreenshots(page, "analytics-consent"); - }); - }); - - // ---- Change password page ---- - test("change password page - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/change-password"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - // Verify form stacks vertically on mobile - await takeThemedScreenshots(page, "change-password"); - }); - - // ---- Privacy policy page ---- - test.describe("Privacy policy page", () => { - test.use({ storageState: { cookies: [], origins: [] } }); - - test("privacy policy page - light and dark", async ({ page }) => { - await page.goto("/privacy"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - await takeThemedScreenshots(page, "privacy-policy"); - }); - }); - - // ---- 404 Not Found page ---- - test("not found page - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/this-route-does-not-exist-404"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - // Verify no sidebar on mobile - await expect(page.locator("aside")).not.toBeVisible(); - - await takeThemedScreenshots(page, "not-found"); - }); - - // ---- Editor page (welcome/empty state) ---- - test("editor page welcome state - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/editor"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - await takeThemedScreenshots(page, "editor-welcome"); - }); - - // ---- Tool page - convert (empty state) ---- - test("convert tool empty - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/convert"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - // Verify mobile layout: no sidebar, dropzone fills viewport width - await expect(page.locator("aside")).not.toBeVisible(); - - await takeThemedScreenshots(page, "tool-convert-empty"); - }); - - // ---- Tool page - convert (file uploaded, format selection visible) ---- - test("convert tool with file - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/convert"); - await uploadTestImage(page); - await page.waitForTimeout(500); - - // On mobile, tool settings may be collapsed by default -- expand if needed - const settingsToggle = page.getByRole("button", { name: /settings/i }).first(); - const settingsPanel = page.locator("[class*='settings'], [class*='Settings']").first(); - const isPanelVisible = await settingsPanel.isVisible({ timeout: 2000 }).catch(() => false); - if (!isPanelVisible && (await settingsToggle.isVisible({ timeout: 1000 }).catch(() => false))) { - await settingsToggle.click(); - await page.waitForTimeout(300); - } - - await takeThemedScreenshots(page, "tool-convert-settings"); - }); - - // ---- Tool page - watermark-text (before-after mode) ---- - test("watermark-text tool empty - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/watermark-text"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - await takeThemedScreenshots(page, "tool-watermark-text-empty"); - }); - - // ---- Tool page - border (live-preview mode) ---- - test("border tool empty - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/border"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - await takeThemedScreenshots(page, "tool-border-empty"); - }); - - // ---- Settings dialog - Security tab ---- - test("settings dialog security tab - light and dark", async ({ loggedInPage: page }) => { - // On mobile, open settings from the bottom nav bar - const bottomNav = page.locator("nav.fixed"); - await bottomNav.getByText("Settings").click(); - await expect(page.getByRole("heading", { name: "General" })).toBeVisible(); - - // Navigate to Security tab - await page.getByRole("button", { name: "Security" }).click(); - await page.waitForTimeout(500); - - await takeThemedScreenshots(page, "settings-security"); - }); - - // ---- Mobile hamburger menu open state ---- - test("hamburger menu open state - light and dark", async ({ loggedInPage: page }) => { - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - // Open hamburger menu on mobile - const hamburger = page - .locator( - "button[aria-label*='menu' i], button[aria-label*='Menu' i], button[class*='hamburger' i]", - ) - .first(); - const hasHamburger = await hamburger.isVisible({ timeout: 2000 }).catch(() => false); - if (hasHamburger) { - await hamburger.click(); - await page.waitForTimeout(300); - } - - await takeThemedScreenshots(page, "hamburger-menu-open"); - }); - - // ---- Bottom navigation bar ---- - test("bottom nav bar - light and dark", async ({ loggedInPage: page }) => { - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - // Bottom navigation bar visible - const bottomNav = page.locator("nav.fixed"); - await expect(bottomNav).toBeVisible(); - - // Take element screenshot of the bottom nav - await setTheme(page, "light"); - await expect(bottomNav).toHaveScreenshot("mobile-bottom-nav-light.png"); - - await setTheme(page, "dark"); - await expect(bottomNav).toHaveScreenshot("mobile-bottom-nav-dark.png"); - - await setTheme(page, "light"); - }); - - // ---- Tool page - strip-metadata (no-comparison mode) ---- - test("strip-metadata tool empty - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/strip-metadata"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - await takeThemedScreenshots(page, "tool-strip-metadata-empty"); - }); - - // ---- Tool page - info (before-after mode) ---- - test("info tool empty - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/info"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - await takeThemedScreenshots(page, "tool-info-empty"); - }); -}); diff --git a/tests/e2e/gui-visual-tablet.spec.ts b/tests/e2e/gui-visual-tablet.spec.ts deleted file mode 100644 index c3df816b..00000000 --- a/tests/e2e/gui-visual-tablet.spec.ts +++ /dev/null @@ -1,473 +0,0 @@ -import { expect, openSettings, test, uploadTestImage } from "./helpers"; - -const MOD = process.platform === "darwin" ? "Meta" : "Control"; - -// --------------------------------------------------------------------------- -// Helper: toggle theme via keyboard shortcut (Cmd/Ctrl+Shift+D) -// The keyboard shortcut works reliably on all viewports. -// --------------------------------------------------------------------------- -async function setTheme(page: import("@playwright/test").Page, theme: "light" | "dark") { - const isDark = await page.evaluate(() => document.documentElement.classList.contains("dark")); - const wantDark = theme === "dark"; - if (isDark !== wantDark) { - await page.keyboard.press(`${MOD}+Shift+d`); - await page.waitForTimeout(300); - } -} - -// --------------------------------------------------------------------------- -// Helper: take a themed screenshot pair (light + dark) -// --------------------------------------------------------------------------- -async function takeThemedScreenshots(page: import("@playwright/test").Page, baseName: string) { - // Light theme - await setTheme(page, "light"); - await expect(page).toHaveScreenshot(`tablet-${baseName}-light.png`, { - fullPage: false, - }); - - // Dark theme - await setTheme(page, "dark"); - await expect(page).toHaveScreenshot(`tablet-${baseName}-dark.png`, { - fullPage: false, - }); - - // Reset to light for next test - await setTheme(page, "light"); -} - -// --------------------------------------------------------------------------- -// Tablet visual regression: 768x1024 -// --------------------------------------------------------------------------- -test.describe("Visual Tablet (768x1024)", () => { - test.use({ viewport: { width: 768, height: 1024 } }); - - // ---- Login page (unauthenticated) ---- - test.describe("Login page", () => { - test.use({ storageState: { cookies: [], origins: [] } }); - - test("login page empty form - light and dark", async ({ page }) => { - await page.goto("/login"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - await takeThemedScreenshots(page, "login-empty"); - }); - - test("login page filled with error - light and dark", async ({ page }) => { - await page.goto("/login"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - // Fill in invalid credentials and submit - await page.getByLabel("Username").fill("wronguser"); - await page.getByLabel("Password").fill("wrongpassword"); - await page.getByRole("button", { name: /login/i }).click(); - - // Wait for the error message to appear - await page.waitForTimeout(1000); - await expect(page.getByText(/invalid|incorrect|failed/i).first()).toBeVisible(); - - await takeThemedScreenshots(page, "login-error"); - }); - }); - - // ---- Home page (empty, no file uploaded) ---- - test("home page empty - light and dark", async ({ loggedInPage: page }) => { - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - // Verify sidebar transitions at tablet width: sidebar should collapse or narrow - const sidebar = page.locator("aside"); - if (await sidebar.isVisible({ timeout: 2000 }).catch(() => false)) { - const sidebarBox = await sidebar.boundingBox(); - if (sidebarBox) { - // Sidebar should be narrower than full desktop width (< 280px) - expect(sidebarBox.width).toBeLessThan(280); - } - } - - await takeThemedScreenshots(page, "home-empty"); - }); - - // ---- Home page (file uploaded, Quick Actions visible) ---- - test("home page with file uploaded - light and dark", async ({ loggedInPage: page }) => { - await uploadTestImage(page); - await page.waitForTimeout(500); - - await expect(page.getByText("Quick Actions").first()).toBeVisible(); - - await takeThemedScreenshots(page, "home-uploaded"); - }); - - // ---- Fullscreen grid page (details shown - default) ---- - test("fullscreen grid details shown - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/fullscreen"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - // Details are shown by default - await expect(page.getByText("Hide Details")).toBeVisible(); - - await takeThemedScreenshots(page, "fullscreen-details-shown"); - }); - - // ---- Fullscreen grid page (details hidden) ---- - test("fullscreen grid details hidden - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/fullscreen"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - // Toggle details off - await page.getByText("Hide Details").click(); - await page.waitForTimeout(300); - await expect(page.getByText("Show Details")).toBeVisible(); - - await takeThemedScreenshots(page, "fullscreen-details-hidden"); - }); - - // ---- Automate page (empty pipeline) ---- - test("automate page empty pipeline - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/automate"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - await expect(page.getByText(/pipeline|automate/i).first()).toBeVisible(); - - await takeThemedScreenshots(page, "automate-empty"); - }); - - // ---- Automate page (3 steps added + file uploaded) ---- - test("automate page with 3 steps and file - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/automate"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - // Add 3 pipeline steps - const resizeBtn = page.getByRole("button", { name: /resize/i }).first(); - const compressBtn = page.getByRole("button", { name: /compress/i }).first(); - const convertBtn = page.getByRole("button", { name: /convert/i }).first(); - - await resizeBtn.click(); - await page.waitForTimeout(300); - await compressBtn.click(); - await page.waitForTimeout(300); - await convertBtn.click(); - await page.waitForTimeout(300); - - // Upload a file to the pipeline - await uploadTestImage(page); - await page.waitForTimeout(500); - - await takeThemedScreenshots(page, "automate-3steps-file"); - }); - - // ---- Files page (empty) ---- - test("files page empty - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/files"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - await takeThemedScreenshots(page, "files-empty"); - }); - - // ---- Settings dialog - General tab ---- - test("settings dialog general tab - light and dark", async ({ loggedInPage: page }) => { - await openSettings(page); - await page.waitForTimeout(500); - - // Verify settings dialog fits within 768px tablet viewport - const dialog = page.getByRole("dialog"); - const dialogBox = await dialog.boundingBox(); - if (dialogBox) { - expect(dialogBox.x).toBeGreaterThanOrEqual(0); - expect(dialogBox.x + dialogBox.width).toBeLessThanOrEqual(768 + 1); - } - - await takeThemedScreenshots(page, "settings-general"); - }); - - // ---- Settings dialog - People tab ---- - test("settings dialog people tab - light and dark", async ({ loggedInPage: page }) => { - await openSettings(page); - - // Navigate to People tab - await page.getByRole("button", { name: "People" }).click(); - await page.waitForTimeout(500); - - await takeThemedScreenshots(page, "settings-people"); - }); - - // ---- Settings dialog - About tab ---- - test("settings dialog about tab - light and dark", async ({ loggedInPage: page }) => { - await openSettings(page); - - // Navigate to About tab - await page.getByRole("button", { name: "About" }).click(); - await page.waitForTimeout(500); - - await takeThemedScreenshots(page, "settings-about"); - }); - - // ---- Help dialog ---- - test("help dialog - light and dark", async ({ loggedInPage: page }) => { - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - // Open help dialog via sidebar or button - const sidebar = page.locator("aside"); - if (await sidebar.isVisible({ timeout: 2000 }).catch(() => false)) { - await sidebar.getByText("Help").click(); - } else { - await page.getByRole("button", { name: /help/i }).click(); - } - await page.getByRole("dialog").waitFor({ state: "visible", timeout: 5000 }); - await page.waitForTimeout(500); - - // Verify dialog fits within tablet viewport - await takeThemedScreenshots(page, "help-dialog"); - }); - - // ---- Tool page - resize (empty, no file) ---- - test("resize tool empty - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/resize"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - await takeThemedScreenshots(page, "tool-resize-empty"); - }); - - // ---- Tool page - resize (file uploaded, settings visible) ---- - test("resize tool with file - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/resize"); - await uploadTestImage(page); - await page.waitForTimeout(500); - - // Verify settings panel layout at tablet width -- panel should not overflow viewport - await expect(page.getByText("Settings").first()).toBeVisible(); - const settingsPanel = page.locator("[class*='settings'], [class*='Settings']").first(); - if (await settingsPanel.isVisible({ timeout: 2000 }).catch(() => false)) { - const panelBox = await settingsPanel.boundingBox(); - if (panelBox) { - expect(panelBox.x + panelBox.width).toBeLessThanOrEqual(768 + 1); - } - } - - await takeThemedScreenshots(page, "tool-resize-settings"); - }); - - // ---- Tool page - compress (before-after result) ---- - test("compress tool before-after result - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/compress"); - await page.waitForLoadState("networkidle"); - - await uploadTestImage(page); - await page.waitForTimeout(1000); - - // Wait for the before-after slider to appear - const slider = page.locator("[class*='before-after'], [class*='BeforeAfter']").first(); - await slider.waitFor({ state: "visible", timeout: 15000 }).catch(() => {}); - await page.waitForTimeout(500); - - // Verify comparison mode renders within narrower tablet width - if (await slider.isVisible()) { - const sliderBox = await slider.boundingBox(); - if (sliderBox) { - expect(sliderBox.x + sliderBox.width).toBeLessThanOrEqual(768 + 1); - } - } - - await takeThemedScreenshots(page, "tool-compress-result"); - }); - - // ---- Tool page - crop (interactive canvas) ---- - test("crop tool interactive canvas - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/crop"); - await page.waitForLoadState("networkidle"); - - await uploadTestImage(page); - await page.waitForTimeout(1000); - - const canvas = page.locator("canvas").first(); - await canvas.waitFor({ state: "visible", timeout: 10000 }).catch(() => {}); - await page.waitForTimeout(500); - - await takeThemedScreenshots(page, "tool-crop-canvas"); - }); - - // ---- Tool page - qr-generate (no-dropzone, QR preview) ---- - test("qr-generate tool with preview - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/qr-generate"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - // Enter text to generate a QR code - const textInput = page.locator("input[type='text'], textarea").first(); - await textInput.fill("https://snapotter.com"); - await page.waitForTimeout(1000); - - // Wait for QR preview to render - const preview = page.locator("img, canvas, svg").first(); - await preview.waitFor({ state: "visible", timeout: 10000 }).catch(() => {}); - await page.waitForTimeout(500); - - await takeThemedScreenshots(page, "tool-qr-generate-preview"); - }); - - // ---- Tool page - collage (template selection) ---- - test("collage tool template selection - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/collage"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - await takeThemedScreenshots(page, "tool-collage-templates"); - }); - - // ---- Analytics consent page ---- - test.describe("Analytics consent page", () => { - test.use({ storageState: { cookies: [], origins: [] } }); - - test("analytics consent page - light and dark", async ({ page }) => { - await page.goto("/analytics-consent"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - await takeThemedScreenshots(page, "analytics-consent"); - }); - }); - - // ---- Change password page ---- - test("change password page - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/change-password"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - await takeThemedScreenshots(page, "change-password"); - }); - - // ---- Privacy policy page ---- - test.describe("Privacy policy page", () => { - test.use({ storageState: { cookies: [], origins: [] } }); - - test("privacy policy page - light and dark", async ({ page }) => { - await page.goto("/privacy"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - await takeThemedScreenshots(page, "privacy-policy"); - }); - }); - - // ---- 404 Not Found page ---- - test("not found page - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/this-route-does-not-exist-404"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - await takeThemedScreenshots(page, "not-found"); - }); - - // ---- Editor page (welcome/empty state) ---- - test("editor page welcome state - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/editor"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - await takeThemedScreenshots(page, "editor-welcome"); - }); - - // ---- Tool page - convert (empty state) ---- - test("convert tool empty - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/convert"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - await takeThemedScreenshots(page, "tool-convert-empty"); - }); - - // ---- Tool page - convert (file uploaded, format selection visible) ---- - test("convert tool with file - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/convert"); - await uploadTestImage(page); - await page.waitForTimeout(500); - - await expect(page.getByText("Settings").first()).toBeVisible(); - - // Verify settings panel fits within 768px tablet viewport - const settingsPanel = page.locator("[class*='settings'], [class*='Settings']").first(); - if (await settingsPanel.isVisible({ timeout: 2000 }).catch(() => false)) { - const panelBox = await settingsPanel.boundingBox(); - if (panelBox) { - expect(panelBox.x + panelBox.width).toBeLessThanOrEqual(768 + 1); - } - } - - await takeThemedScreenshots(page, "tool-convert-settings"); - }); - - // ---- Tool page - watermark-text (before-after mode) ---- - test("watermark-text tool empty - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/watermark-text"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - await takeThemedScreenshots(page, "tool-watermark-text-empty"); - }); - - // ---- Tool page - border (live-preview mode) ---- - test("border tool empty - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/border"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - await takeThemedScreenshots(page, "tool-border-empty"); - }); - - // ---- Settings dialog - Security tab ---- - test("settings dialog security tab - light and dark", async ({ loggedInPage: page }) => { - await openSettings(page); - - // Navigate to Security tab - await page.getByRole("button", { name: "Security" }).click(); - await page.waitForTimeout(500); - - // Verify dialog fits within 768px tablet viewport - const dialog = page.getByRole("dialog"); - const dialogBox = await dialog.boundingBox(); - if (dialogBox) { - expect(dialogBox.x + dialogBox.width).toBeLessThanOrEqual(768 + 1); - } - - await takeThemedScreenshots(page, "settings-security"); - }); - - // ---- Home page with search focused ---- - test("home page search focused - light and dark", async ({ loggedInPage: page }) => { - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - // Focus search bar via keyboard shortcut - const MOD_KEY = process.platform === "darwin" ? "Meta" : "Control"; - await page.keyboard.press(`${MOD_KEY}+k`); - await page.waitForTimeout(300); - - await takeThemedScreenshots(page, "home-search-focused"); - }); - - // ---- Tool page - strip-metadata (no-comparison mode) ---- - test("strip-metadata tool empty - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/strip-metadata"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - await takeThemedScreenshots(page, "tool-strip-metadata-empty"); - }); - - // ---- Tool page - info (before-after mode) ---- - test("info tool empty - light and dark", async ({ loggedInPage: page }) => { - await page.goto("/info"); - await page.waitForLoadState("networkidle"); - await page.waitForTimeout(500); - - await takeThemedScreenshots(page, "tool-info-empty"); - }); -});