import { SELECTORS } from "../../fixtures/selectors"; import { expect, test } from "../../fixtures/test"; import { hasHorizontalOverflow, isClipped, VIEWPORT_SIZES, type ViewportSize, } from "../../fixtures/viewports"; test.describe("Responsive Breakpoint Tests @smoke", () => { for (const viewport of VIEWPORT_SIZES) { test.describe(`${viewport} viewport`, () => { test.beforeEach(async ({ gotoApp, waitForAppReady, setViewport }) => { await setViewport(viewport); await gotoApp(); await waitForAppReady(); }); test("page has no horizontal overflow", async ({ page }) => { // Check body for overflow const bodyOverflow = await hasHorizontalOverflow(page, "body"); expect(bodyOverflow).toBe(false); // Check main content const mainOverflow = await hasHorizontalOverflow(page, "main"); expect(mainOverflow).toBe(false); // Check hero section const heroOverflow = await hasHorizontalOverflow( page, SELECTORS.hero.root, ); expect(heroOverflow).toBe(false); // Check feed section const feedOverflow = await hasHorizontalOverflow( page, SELECTORS.feed.root, ); expect(feedOverflow).toBe(false); }); test("hero section is not clipped", async ({ page, waitForHero }) => { const hero = await waitForHero(); const isHeroClipped = await isClipped(page, SELECTORS.hero.root); expect(isHeroClipped).toBe(false); }); test("feed articles are not clipped", async ({ page, waitForFeed }) => { const feed = await waitForFeed(); const isFeedClipped = await isClipped(page, SELECTORS.feed.root); expect(isFeedClipped).toBe(false); }); test("modal fits within viewport", async ({ page, waitForHero, setViewport, }) => { // Open modal const hero = await waitForHero(); await hero.locator(SELECTORS.hero.readButton).click(); const modal = page.locator(SELECTORS.summaryModal.root); await expect(modal).toBeVisible(); // Get viewport dimensions const viewport = await page.evaluate(() => ({ width: window.innerWidth, height: window.innerHeight, })); // Get modal dimensions const modalBox = await modal.boundingBox(); expect(modalBox).not.toBeNull(); // Modal should fit within viewport (with some padding) expect(modalBox!.width).toBeLessThanOrEqual(viewport.width); expect(modalBox!.height).toBeLessThanOrEqual(viewport.height * 0.96); // max-h-[96vh] }); test("interactive controls are reachable", async ({ page, waitForFeed, }) => { const feed = await waitForFeed(); const firstArticle = feed.locator(SELECTORS.feed.articles).first(); // Check read button is visible and clickable const readButton = firstArticle.locator( SELECTORS.feed.articleReadButton, ); await expect(readButton).toBeVisible(); await expect(readButton).toBeEnabled(); // Check source link is visible if present const sourceLink = firstArticle.locator(SELECTORS.feed.articleSource); const hasSourceLink = (await sourceLink.count()) > 0; if (hasSourceLink) { await expect(sourceLink).toBeVisible(); } }); test("header controls remain accessible", async ({ page }) => { // Check logo is visible const logo = page.locator(SELECTORS.header.logo); await expect(logo).toBeVisible(); // Check theme button is visible const themeButton = page.locator(SELECTORS.header.themeMenuButton); await expect(themeButton).toBeVisible(); await expect(themeButton).toBeEnabled(); // Check language select is visible (may be hidden on very small screens) const languageSelect = page.locator(SELECTORS.header.languageSelect); const isVisible = await languageSelect.isVisible().catch(() => false); if (isVisible) { await expect(languageSelect).toBeEnabled(); } }); }); } }); test.describe("Responsive Layout Adaptations", () => { test("mobile shows single column feed", async ({ gotoApp, waitForAppReady, setViewport, waitForFeed, }) => { await setViewport("mobile"); await gotoApp(); await waitForAppReady(); const feed = await waitForFeed(); const articles = feed.locator(SELECTORS.feed.articles); // Articles should be in single column (full width) const firstArticle = articles.first(); const articleBox = await firstArticle.boundingBox(); // Get feed container width const feedBox = await feed.boundingBox(); // Article should take most of the width (single column) expect(articleBox!.width).toBeGreaterThan(feedBox!.width * 0.8); }); test("tablet shows appropriate layout", async ({ gotoApp, waitForAppReady, setViewport, waitForFeed, }) => { await setViewport("tablet"); await gotoApp(); await waitForAppReady(); const feed = await waitForFeed(); const articles = feed.locator(SELECTORS.feed.articles); // Should have multiple articles visible const count = await articles.count(); expect(count).toBeGreaterThanOrEqual(2); // Articles should be side by side (multi-column) const firstArticle = articles.first(); const secondArticle = articles.nth(1); const firstBox = await firstArticle.boundingBox(); const secondBox = await secondArticle.boundingBox(); // Second article should be to the right of first (or below in some layouts) expect(secondBox!.x).not.toBe(firstBox!.x); }); test("desktop shows multi-column feed", async ({ gotoApp, waitForAppReady, setViewport, waitForFeed, }) => { await setViewport("desktop"); await gotoApp(); await waitForAppReady(); const feed = await waitForFeed(); const articles = feed.locator(SELECTORS.feed.articles); // Should have multiple articles in a row const count = await articles.count(); expect(count).toBeGreaterThanOrEqual(3); // First three articles should be in a row const articleBoxes = await articles .slice(0, 3) .evaluateAll((els) => els.map((el) => el.getBoundingClientRect())); // Articles should be at different x positions (side by side) const xPositions = articleBoxes.map((box) => box.x); const uniqueXPositions = [...new Set(xPositions)]; expect(uniqueXPositions.length).toBeGreaterThanOrEqual(2); }); test("hero image maintains aspect ratio", async ({ gotoApp, waitForAppReady, setViewport, waitForHero, }) => { for (const viewport of ["mobile", "tablet", "desktop"] as ViewportSize[]) { await setViewport(viewport); await gotoApp(); await waitForAppReady(); const hero = await waitForHero(); const image = hero.locator(SELECTORS.hero.image); const box = await image.boundingBox(); expect(box).not.toBeNull(); // Image should have reasonable dimensions expect(box!.width).toBeGreaterThan(0); expect(box!.height).toBeGreaterThan(0); // Aspect ratio should be roughly maintained (wider than tall) expect(box!.width / box!.height).toBeGreaterThan(1); expect(box!.width / box!.height).toBeLessThan(5); } }); });