p19-bug-fixes

This commit is contained in:
2026-02-13 16:57:45 -05:00
parent e2406bf978
commit f6beedd68f
75 changed files with 11989 additions and 48 deletions

View File

@@ -0,0 +1,231 @@
import {
assertContrast,
checkContrast,
getComputedColors,
WCAG_CONTRAST,
} from "../../fixtures/accessibility";
import { SELECTORS } from "../../fixtures/selectors";
import { expect, test } from "../../fixtures/test";
import { THEMES, Theme } from "../../fixtures/themes";
test.describe("Color Contrast Across Themes", () => {
test.beforeEach(async ({ gotoApp, waitForAppReady }) => {
await gotoApp();
await waitForAppReady();
});
for (const theme of THEMES) {
test(`hero text has sufficient contrast in ${theme} theme @smoke`, async ({
page,
setTheme,
waitForHero,
}) => {
// Set theme
await setTheme(theme);
const hero = await waitForHero();
// Check headline contrast
const headline = hero.locator(SELECTORS.hero.headline);
await assertContrast(page, headline, WCAG_CONTRAST.largeText);
// Check summary contrast
const summary = hero.locator(SELECTORS.hero.summary);
await assertContrast(page, summary, WCAG_CONTRAST.normalText);
});
test(`feed card text has sufficient contrast in ${theme} theme`, async ({
page,
setTheme,
waitForFeed,
}) => {
// Set theme
await setTheme(theme);
const feed = await waitForFeed();
const firstArticle = feed.locator(SELECTORS.feed.articles).first();
// Check headline contrast
const headline = firstArticle.locator("h3");
await assertContrast(page, headline, WCAG_CONTRAST.largeText);
// Check summary contrast
const summary = firstArticle.locator(SELECTORS.feed.articleSummary);
await assertContrast(page, summary, WCAG_CONTRAST.normalText);
});
test(`modal text has sufficient contrast in ${theme} theme`, async ({
page,
setTheme,
waitForHero,
}) => {
// Set theme
await setTheme(theme);
// Open modal
const hero = await waitForHero();
await hero.locator(SELECTORS.hero.readButton).click();
const modal = page.locator(SELECTORS.summaryModal.root);
await expect(modal).toBeVisible();
// Check headline contrast
const headline = modal.locator(SELECTORS.summaryModal.headline);
await assertContrast(page, headline, WCAG_CONTRAST.largeText);
// Check body text contrast
const bodyText = modal.locator(SELECTORS.summaryModal.summaryBody);
await assertContrast(page, bodyText, WCAG_CONTRAST.normalText);
// Check TL;DR list contrast
const tldrList = modal.locator(SELECTORS.summaryModal.tldrList);
await assertContrast(page, tldrList, WCAG_CONTRAST.normalText);
});
test(`link colors have sufficient contrast in ${theme} theme`, async ({
page,
setTheme,
waitForFeed,
}) => {
// Set theme
await setTheme(theme);
const feed = await waitForFeed();
const firstArticle = feed.locator(SELECTORS.feed.articles).first();
// Check source link contrast
const sourceLink = firstArticle.locator(SELECTORS.feed.articleSource);
const hasSourceLink = (await sourceLink.count()) > 0;
if (hasSourceLink) {
await assertContrast(page, sourceLink, WCAG_CONTRAST.normalText);
}
});
test(`button text has sufficient contrast in ${theme} theme`, async ({
page,
setTheme,
waitForHero,
}) => {
// Set theme
await setTheme(theme);
const hero = await waitForHero();
// Check read button contrast
const readButton = hero.locator(SELECTORS.hero.readButton);
await assertContrast(page, readButton, WCAG_CONTRAST.normalText);
});
}
});
test.describe("Interactive State Contrast", () => {
test.beforeEach(async ({ gotoApp, waitForAppReady }) => {
await gotoApp();
await waitForAppReady();
});
test("hover state maintains sufficient contrast", async ({
page,
waitForFeed,
}) => {
const feed = await waitForFeed();
const firstArticle = feed.locator(SELECTORS.feed.articles).first();
const readButton = firstArticle.locator(SELECTORS.feed.articleReadButton);
// Get normal state colors
const normalColors = await getComputedColors(page, readButton);
// Hover over button
await readButton.hover();
await page.waitForTimeout(300); // Wait for transition
// Get hover state colors
const hoverColors = await getComputedColors(page, readButton);
// Both states should have sufficient contrast
const normalRatio = checkContrast(
normalColors.color,
normalColors.backgroundColor,
);
const hoverRatio = checkContrast(
hoverColors.color,
hoverColors.backgroundColor,
);
expect(normalRatio).toBeGreaterThanOrEqual(WCAG_CONTRAST.normalText);
expect(hoverRatio).toBeGreaterThanOrEqual(WCAG_CONTRAST.normalText);
});
test("focus state maintains sufficient contrast @smoke", async ({
page,
waitForFeed,
}) => {
const feed = await waitForFeed();
const firstArticle = feed.locator(SELECTORS.feed.articles).first();
const readButton = firstArticle.locator(SELECTORS.feed.articleReadButton);
// Get normal state colors
const normalColors = await getComputedColors(page, readButton);
// Focus the button
await readButton.focus();
// Get focus state colors
const focusColors = await getComputedColors(page, readButton);
// Both states should have sufficient contrast
const normalRatio = checkContrast(
normalColors.color,
normalColors.backgroundColor,
);
const focusRatio = checkContrast(
focusColors.color,
focusColors.backgroundColor,
);
expect(normalRatio).toBeGreaterThanOrEqual(WCAG_CONTRAST.normalText);
expect(focusRatio).toBeGreaterThanOrEqual(WCAG_CONTRAST.normalText);
});
});
test.describe("High Contrast Theme", () => {
test.beforeEach(async ({ gotoApp, waitForAppReady }) => {
await gotoApp();
await waitForAppReady();
});
test("contrast theme provides enhanced visibility @smoke", async ({
page,
setTheme,
waitForHero,
waitForFeed,
}) => {
// Set high contrast theme
await setTheme("contrast");
// Check hero
const hero = await waitForHero();
const headline = hero.locator(SELECTORS.hero.headline);
const headlineColors = await getComputedColors(page, headline);
const headlineRatio = checkContrast(
headlineColors.color,
headlineColors.backgroundColor,
);
// High contrast should provide very strong contrast (7:1 or better)
expect(headlineRatio).toBeGreaterThanOrEqual(7);
// Check feed
const feed = await waitForFeed();
const firstArticle = feed.locator(SELECTORS.feed.articles).first();
const articleHeadline = firstArticle.locator("h3");
const articleColors = await getComputedColors(page, articleHeadline);
const articleRatio = checkContrast(
articleColors.color,
articleColors.backgroundColor,
);
expect(articleRatio).toBeGreaterThanOrEqual(7);
});
});

View File

@@ -0,0 +1,130 @@
import {
getAccessibleName,
hasAccessibleName,
} from "../../fixtures/accessibility";
import { SELECTORS } from "../../fixtures/selectors";
import { expect, test } from "../../fixtures/test";
test.describe("Icon-Only Control Accessible Names @smoke", () => {
test.beforeEach(async ({ gotoApp, waitForAppReady }) => {
await gotoApp();
await waitForAppReady();
});
test("share buttons have accessible names", async ({ page, waitForHero }) => {
// Open modal to access share buttons
const hero = await waitForHero();
await hero.locator(SELECTORS.hero.readButton).click();
const modal = page.locator(SELECTORS.summaryModal.root);
await expect(modal).toBeVisible();
// Check X share button
const shareX = modal.locator(SELECTORS.summaryModal.shareX);
await expect(shareX).toHaveAttribute("aria-label", "Share on X");
expect(await hasAccessibleName(shareX)).toBe(true);
// Check WhatsApp share button
const shareWhatsApp = modal.locator(SELECTORS.summaryModal.shareWhatsApp);
await expect(shareWhatsApp).toHaveAttribute(
"aria-label",
"Share on WhatsApp",
);
expect(await hasAccessibleName(shareWhatsApp)).toBe(true);
// Check LinkedIn share button
const shareLinkedIn = modal.locator(SELECTORS.summaryModal.shareLinkedIn);
await expect(shareLinkedIn).toHaveAttribute(
"aria-label",
"Share on LinkedIn",
);
expect(await hasAccessibleName(shareLinkedIn)).toBe(true);
// Check copy link button
const shareCopy = modal.locator(SELECTORS.summaryModal.shareCopy);
await expect(shareCopy).toHaveAttribute("aria-label", "Copy article link");
expect(await hasAccessibleName(shareCopy)).toBe(true);
});
test("theme menu button has accessible name", async ({ page }) => {
const themeButton = page.locator(SELECTORS.header.themeMenuButton);
await expect(themeButton).toHaveAttribute("aria-label", "Open theme menu");
expect(await hasAccessibleName(themeButton)).toBe(true);
});
test("back to top button has accessible name", async ({ page }) => {
// Scroll down to make back-to-top visible
await page.evaluate(() => window.scrollTo(0, 500));
await page.waitForTimeout(500);
const backToTop = page.locator(SELECTORS.backToTop.root);
// Button may not be visible yet, but should have accessible name
const hasName = await hasAccessibleName(backToTop);
expect(hasName).toBe(true);
const name = await getAccessibleName(page, backToTop);
expect(name).toContain("top");
});
test("modal close button has accessible name", async ({
page,
waitForHero,
}) => {
// Open modal
const hero = await waitForHero();
await hero.locator(SELECTORS.hero.readButton).click();
const modal = page.locator(SELECTORS.summaryModal.root);
await expect(modal).toBeVisible();
// Check close button
const closeButton = modal.locator(SELECTORS.summaryModal.closeButton);
expect(await hasAccessibleName(closeButton)).toBe(true);
const name = await getAccessibleName(page, closeButton);
expect(name?.toLowerCase()).toContain("close");
});
test("policy modal close button has accessible name", async ({
page,
gotoApp,
}) => {
// Open policy modal
await gotoApp({ policy: "terms" });
await page.waitForTimeout(1000);
const modal = page.locator(SELECTORS.policyModal.root);
await expect(modal).toBeVisible();
// Check close button
const closeButton = modal.locator(SELECTORS.policyModal.closeButton);
expect(await hasAccessibleName(closeButton)).toBe(true);
const name = await getAccessibleName(page, closeButton);
expect(name?.toLowerCase()).toContain("close");
});
test("all interactive icons have aria-hidden on SVG", async ({
page,
waitForHero,
}) => {
// Open modal to access share buttons
const hero = await waitForHero();
await hero.locator(SELECTORS.hero.readButton).click();
const modal = page.locator(SELECTORS.summaryModal.root);
await expect(modal).toBeVisible();
// Check all SVGs in share buttons are aria-hidden
const svgs = modal.locator(".share-icon-btn svg");
const count = await svgs.count();
for (let i = 0; i < count; i++) {
const svg = svgs.nth(i);
const ariaHidden = await svg.getAttribute("aria-hidden");
expect(ariaHidden).toBe("true");
}
});
});

View File

@@ -0,0 +1,264 @@
import { hasFocusVisible } from "../../fixtures/accessibility";
import { SELECTORS } from "../../fixtures/selectors";
import { expect, test } from "../../fixtures/test";
test.describe("Keyboard Navigation @smoke", () => {
test.beforeEach(async ({ gotoApp, waitForAppReady }) => {
await gotoApp();
await waitForAppReady();
});
test("skip link is first focusable element", async ({ page }) => {
// Press Tab to focus skip link
await page.keyboard.press("Tab");
// Skip link should be focused
const skipLink = page.locator(SELECTORS.skipLink);
await expect(skipLink).toBeFocused();
// Skip link should be visible when focused
const isVisible = await skipLink.isVisible();
expect(isVisible).toBe(true);
});
test("skip link navigates to main content", async ({ page }) => {
// Focus and activate skip link
await page.keyboard.press("Tab");
await page.keyboard.press("Enter");
// Main content should be focused
const mainContent = page.locator("#main-content");
await expect(mainContent).toBeFocused();
});
test("header controls are keyboard accessible @smoke", async ({ page }) => {
// Tab through header controls
await page.keyboard.press("Tab"); // Skip link
await page.keyboard.press("Tab"); // Logo
await page.keyboard.press("Tab"); // Language select
const languageSelect = page.locator(SELECTORS.header.languageSelect);
await expect(languageSelect).toBeFocused();
await page.keyboard.press("Tab"); // Theme menu button
const themeButton = page.locator(SELECTORS.header.themeMenuButton);
await expect(themeButton).toBeFocused();
});
test("theme menu is keyboard operable", async ({ page }) => {
// Navigate to theme button
await page.keyboard.press("Tab"); // Skip link
await page.keyboard.press("Tab"); // Logo
await page.keyboard.press("Tab"); // Language select
await page.keyboard.press("Tab"); // Theme button
// Open theme menu with Enter
await page.keyboard.press("Enter");
// Menu should be visible
const menu = page.locator(SELECTORS.themeMenu.root);
await expect(menu).toBeVisible();
// Menu items should be focusable
const menuItems = menu.locator('[role="menuitem"]');
const count = await menuItems.count();
expect(count).toBeGreaterThan(0);
// Close menu with Escape
await page.keyboard.press("Escape");
await expect(menu).not.toBeVisible();
});
test("hero read button is keyboard accessible @smoke", async ({
page,
waitForHero,
}) => {
const hero = await waitForHero();
// Navigate to hero read button
// Skip header controls first
for (let i = 0; i < 4; i++) {
await page.keyboard.press("Tab");
}
// Hero read button should be focusable
const readButton = hero.locator(SELECTORS.hero.readButton);
// Check if button is in tab order by trying to focus it
let found = false;
for (let i = 0; i < 10; i++) {
const activeElement = await page.evaluate(
() =>
document.activeElement?.textContent?.trim() ||
document.activeElement?.getAttribute("aria-label"),
);
if (activeElement?.includes("Read TL;DR")) {
found = true;
break;
}
await page.keyboard.press("Tab");
}
expect(found).toBe(true);
});
test("feed articles are keyboard navigable", async ({
page,
waitForFeed,
}) => {
const feed = await waitForFeed();
// Get first article
const firstArticle = feed.locator(SELECTORS.feed.articles).first();
// Source link should be keyboard accessible
const sourceLink = firstArticle.locator(SELECTORS.feed.articleSource);
const hasSourceLink = (await sourceLink.count()) > 0;
if (hasSourceLink) {
// Tab to source link
let attempts = 0;
let sourceLinkFocused = false;
while (attempts < 20 && !sourceLinkFocused) {
await page.keyboard.press("Tab");
const href = await page.evaluate(() =>
document.activeElement?.getAttribute("href"),
);
if (href && href.startsWith("http")) {
sourceLinkFocused = true;
}
attempts++;
}
expect(sourceLinkFocused).toBe(true);
}
// Read button should be keyboard accessible
const readButton = firstArticle.locator(SELECTORS.feed.articleReadButton);
let attempts = 0;
let readButtonFocused = false;
while (attempts < 30 && !readButtonFocused) {
await page.keyboard.press("Tab");
const text = await page.evaluate(() =>
document.activeElement?.textContent?.trim(),
);
if (text === "Read TL;DR") {
readButtonFocused = true;
}
attempts++;
}
expect(readButtonFocused).toBe(true);
});
test("focus-visible is shown on interactive elements", async ({
page,
waitForHero,
}) => {
const hero = await waitForHero();
// Navigate to hero read button
for (let i = 0; i < 4; i++) {
await page.keyboard.press("Tab");
}
// Find focused element
const focusedElement = page.locator(":focus");
// Check that focused element has visible focus indicator
const hasVisibleFocus = await hasFocusVisible(page, focusedElement);
expect(hasVisibleFocus).toBe(true);
});
test("footer links are keyboard accessible @smoke", async ({ page }) => {
// Navigate to footer
const footer = page.locator(SELECTORS.footer.root);
await footer.scrollIntoViewIfNeeded();
// Tab through footer links
let foundFooterLink = false;
let attempts = 0;
while (attempts < 50 && !foundFooterLink) {
await page.keyboard.press("Tab");
const activeElement = await page.evaluate(() => document.activeElement);
// Check if we're in footer
const isInFooter = await page.evaluate(() => {
const active = document.activeElement;
const footer = document.querySelector("footer");
return footer?.contains(active);
});
if (isInFooter) {
foundFooterLink = true;
}
attempts++;
}
expect(foundFooterLink).toBe(true);
});
});
test.describe("Focus Management", () => {
test.beforeEach(async ({ gotoApp, waitForAppReady }) => {
await gotoApp();
await waitForAppReady();
});
test("focus moves to modal when opened @smoke", async ({
page,
waitForHero,
}) => {
const hero = await waitForHero();
// Click read button
await hero.locator(SELECTORS.hero.readButton).click();
// Modal should be visible
const modal = page.locator(SELECTORS.summaryModal.root);
await expect(modal).toBeVisible();
// Focus should be inside modal
const isFocusInModal = await page.evaluate(() => {
const modal = document.querySelector('[role="dialog"]');
const active = document.activeElement;
return modal?.contains(active);
});
expect(isFocusInModal).toBe(true);
});
test("focus is trapped within modal", async ({ page, waitForHero }) => {
const hero = await waitForHero();
// Open modal
await hero.locator(SELECTORS.hero.readButton).click();
// Tab multiple times
for (let i = 0; i < 20; i++) {
await page.keyboard.press("Tab");
// Check focus is still in modal
const isInModal = await page.evaluate(() => {
const modal = document.querySelector('[role="dialog"]');
const active = document.activeElement;
return modal?.contains(active);
});
expect(isInModal).toBe(true);
}
});
});

View File

@@ -0,0 +1,173 @@
import { SELECTORS } from "../../fixtures/selectors";
import { expect, test } from "../../fixtures/test";
test.describe("Policy Modal Accessibility @smoke", () => {
test.beforeEach(async ({ gotoApp, waitForAppReady }) => {
await gotoApp();
await waitForAppReady();
});
test("terms modal opens and has correct ARIA attributes", async ({
page,
}) => {
// Click terms link
const termsLink = page.locator(SELECTORS.footer.termsLink);
await termsLink.click();
// Modal should be visible
const modal = page.locator(SELECTORS.policyModal.root);
await expect(modal).toBeVisible();
// Check ARIA attributes
await expect(modal).toHaveAttribute("role", "dialog");
await expect(modal).toHaveAttribute("aria-modal", "true");
// Check aria-label
const ariaLabel = await modal.getAttribute("aria-label");
expect(ariaLabel).toMatch(/Terms|Attribution/);
});
test("attribution modal opens and has correct ARIA attributes", async ({
page,
}) => {
// Click attribution link
const attributionLink = page.locator(SELECTORS.footer.attributionLink);
await attributionLink.click();
// Modal should be visible
const modal = page.locator(SELECTORS.policyModal.root);
await expect(modal).toBeVisible();
// Check ARIA attributes
await expect(modal).toHaveAttribute("role", "dialog");
await expect(modal).toHaveAttribute("aria-modal", "true");
});
test("policy modal closes with escape key @smoke", async ({ page }) => {
// Open terms modal
const termsLink = page.locator(SELECTORS.footer.termsLink);
await termsLink.click();
const modal = page.locator(SELECTORS.policyModal.root);
await expect(modal).toBeVisible();
// Press escape
await page.keyboard.press("Escape");
await page.waitForTimeout(500);
// Modal should be closed
await expect(modal).not.toBeVisible();
});
test("policy modal closes with close button", async ({ page }) => {
// Open terms modal
const termsLink = page.locator(SELECTORS.footer.termsLink);
await termsLink.click();
const modal = page.locator(SELECTORS.policyModal.root);
await expect(modal).toBeVisible();
// Click close button
const closeButton = modal.locator(SELECTORS.policyModal.closeButton);
await closeButton.click();
// Modal should be closed
await expect(modal).not.toBeVisible();
});
test("policy modal closes with backdrop click", async ({ page }) => {
// Open terms modal
const termsLink = page.locator(SELECTORS.footer.termsLink);
await termsLink.click();
const modal = page.locator(SELECTORS.policyModal.root);
await expect(modal).toBeVisible();
// Click backdrop
const backdrop = page.locator(".fixed.inset-0.bg-black\\/70").first();
await backdrop.click();
// Modal should be closed
await page.waitForTimeout(500);
await expect(modal).not.toBeVisible();
});
test("focus returns to trigger after closing policy modal @smoke", async ({
page,
}) => {
// Open terms modal
const termsLink = page.locator(SELECTORS.footer.termsLink);
await termsLink.click();
const modal = page.locator(SELECTORS.policyModal.root);
await expect(modal).toBeVisible();
// Close modal
await page.keyboard.press("Escape");
await page.waitForTimeout(500);
// Focus should return to terms link
await expect(termsLink).toBeFocused();
});
test("focus is contained within policy modal", async ({ page }) => {
// Open terms modal
const termsLink = page.locator(SELECTORS.footer.termsLink);
await termsLink.click();
const modal = page.locator(SELECTORS.policyModal.root);
await expect(modal).toBeVisible();
// Tab multiple times
for (let i = 0; i < 10; i++) {
await page.keyboard.press("Tab");
// Check focus is still in modal
const isInModal = await page.evaluate(() => {
const modal = document.querySelector('[role="dialog"]');
const active = document.activeElement;
return modal?.contains(active);
});
expect(isInModal).toBe(true);
}
});
test("policy modal content is readable", async ({ page }) => {
// Open terms modal
const termsLink = page.locator(SELECTORS.footer.termsLink);
await termsLink.click();
const modal = page.locator(SELECTORS.policyModal.root);
await expect(modal).toBeVisible();
// Check title is present
const title = modal.locator(SELECTORS.policyModal.termsTitle);
await expect(title).toBeVisible();
await expect(title).toContainText("Terms of Use");
// Check content is present
const content = modal.locator(".modal-body-text");
const paragraphs = await content.locator("p").count();
expect(paragraphs).toBeGreaterThan(0);
});
test("policy modal has correct heading structure", async ({ page }) => {
// Open terms modal
const termsLink = page.locator(SELECTORS.footer.termsLink);
await termsLink.click();
const modal = page.locator(SELECTORS.policyModal.root);
await expect(modal).toBeVisible();
// Should have h2 heading
const heading = modal.locator("h2");
await expect(heading).toBeVisible();
// Check heading level
const headingLevel = await heading.evaluate((el) =>
el.tagName.toLowerCase(),
);
expect(headingLevel).toBe("h2");
});
});

View File

@@ -0,0 +1,368 @@
import { SELECTORS } from "../../fixtures/selectors";
import { expect, test } from "../../fixtures/test";
test.describe("Hero and Feed Browsing @smoke", () => {
test.beforeEach(async ({ gotoApp, waitForAppReady }) => {
await gotoApp();
await waitForAppReady();
});
test("hero section loads with article content", async ({
page,
waitForHero,
}) => {
const hero = await waitForHero();
// Hero should be visible
await expect(hero).toBeVisible();
// Hero should have headline
const headline = hero.locator(SELECTORS.hero.headline);
await expect(headline).toBeVisible();
await expect(headline).not.toBeEmpty();
// Hero should have summary
const summary = hero.locator(SELECTORS.hero.summary);
await expect(summary).toBeVisible();
await expect(summary).not.toBeEmpty();
// Hero should have "Read TL;DR" button
const readButton = hero.locator(SELECTORS.hero.readButton);
await expect(readButton).toBeVisible();
await expect(readButton).toBeEnabled();
// Hero should have image
const image = hero.locator(SELECTORS.hero.image);
await expect(image).toBeVisible();
});
test("news feed loads with multiple articles", async ({
page,
waitForFeed,
}) => {
const feed = await waitForFeed();
// Feed section should be visible
await expect(feed).toBeVisible();
// Should have "Recent News" heading
const heading = feed.locator("h2");
await expect(heading).toContainText("Recent News");
// Should have multiple article cards (at least 1)
const articles = feed.locator(SELECTORS.feed.articles);
const count = await articles.count();
expect(count).toBeGreaterThanOrEqual(1);
// Each article should have required elements
const firstArticle = articles.first();
await expect(firstArticle.locator("h3")).toBeVisible();
await expect(
firstArticle.locator(SELECTORS.feed.articleSummary),
).toBeVisible();
await expect(
firstArticle.locator(SELECTORS.feed.articleReadButton),
).toBeVisible();
});
test("feed article cards have correct structure", async ({
page,
waitForFeed,
}) => {
const feed = await waitForFeed();
const articles = feed.locator(SELECTORS.feed.articles);
// Check structure of first article
const firstArticle = articles.first();
// Should have image container
const imageContainer = firstArticle.locator(".relative.h-48");
await expect(imageContainer).toBeVisible();
// Should have content area
const contentArea = firstArticle.locator(".p-5");
await expect(contentArea).toBeVisible();
// Should have headline
const headline = firstArticle.locator("h3");
await expect(headline).toBeVisible();
await expect(headline).toHaveClass(/news-card-title/);
// Should have summary
const summary = firstArticle.locator(SELECTORS.feed.articleSummary);
await expect(summary).toBeVisible();
await expect(summary).toHaveClass(/news-card-summary/);
});
test("hero article displays correct metadata", async ({
page,
waitForHero,
}) => {
const hero = await waitForHero();
// Should have "LATEST" pill
const latestPill = hero.locator(SELECTORS.hero.latestPill);
await expect(latestPill).toBeVisible();
await expect(latestPill).toContainText("LATEST");
// Should have time ago
const timePill = hero.locator(SELECTORS.hero.timePill);
await expect(timePill).toBeVisible();
// Time should contain "ago" or "just now"
const timeText = await timePill.textContent();
expect(timeText).toMatch(/ago|just now/);
});
test("source link is present and clickable in hero", async ({
page,
waitForHero,
}) => {
const hero = await waitForHero();
const sourceLink = hero.locator(SELECTORS.hero.sourceLink);
// Source link may not be present if no source URL
const count = await sourceLink.count();
if (count > 0) {
await expect(sourceLink).toBeVisible();
await expect(sourceLink).toHaveAttribute("href");
await expect(sourceLink).toHaveAttribute("target", "_blank");
await expect(sourceLink).toHaveAttribute("rel", "noopener");
}
});
});
test.describe("Summary Modal Flows", () => {
test.beforeEach(async ({ gotoApp, waitForAppReady }) => {
await gotoApp();
await waitForAppReady();
});
test("opens summary modal from hero @smoke", async ({
page,
waitForHero,
isSummaryModalOpen,
}) => {
const hero = await waitForHero();
// Click "Read TL;DR" button in hero
const readButton = hero.locator(SELECTORS.hero.readButton);
await readButton.click();
// Modal should open
const isOpen = await isSummaryModalOpen();
expect(isOpen).toBe(true);
// Modal should have correct structure
const modal = page.locator(SELECTORS.summaryModal.root);
await expect(modal.locator(SELECTORS.summaryModal.headline)).toBeVisible();
await expect(
modal.locator(SELECTORS.summaryModal.tldrSection),
).toBeVisible();
await expect(
modal.locator(SELECTORS.summaryModal.summarySection),
).toBeVisible();
await expect(
modal.locator(SELECTORS.summaryModal.sourceSection),
).toBeVisible();
await expect(
modal.locator(SELECTORS.summaryModal.shareSection),
).toBeVisible();
});
test("opens summary modal from feed article @smoke", async ({
page,
waitForFeed,
isSummaryModalOpen,
}) => {
const feed = await waitForFeed();
// Get first feed article
const articles = feed.locator(SELECTORS.feed.articles);
const firstArticle = articles.first();
// Click "Read TL;DR" button
const readButton = firstArticle.locator(SELECTORS.feed.articleReadButton);
await readButton.click();
// Modal should open
const isOpen = await isSummaryModalOpen();
expect(isOpen).toBe(true);
// Modal headline should match article headline
const modal = page.locator(SELECTORS.summaryModal.root);
const articleHeadline = await firstArticle.locator("h3").textContent();
const modalHeadline = await modal
.locator(SELECTORS.summaryModal.headline)
.textContent();
expect(modalHeadline).toBe(articleHeadline);
});
test("closes summary modal via close button @smoke", async ({
page,
waitForHero,
isSummaryModalOpen,
closeSummaryModal,
}) => {
// Open modal from hero
const hero = await waitForHero();
await hero.locator(SELECTORS.hero.readButton).click();
// Verify modal is open
expect(await isSummaryModalOpen()).toBe(true);
// Close modal
await closeSummaryModal();
// Verify modal is closed
expect(await isSummaryModalOpen()).toBe(false);
});
test("closes summary modal via backdrop click", async ({
page,
waitForHero,
isSummaryModalOpen,
}) => {
// Open modal from hero
const hero = await waitForHero();
await hero.locator(SELECTORS.hero.readButton).click();
// Verify modal is open
expect(await isSummaryModalOpen()).toBe(true);
// Click backdrop (outside modal content)
const backdrop = page.locator(".fixed.inset-0.bg-black\\/70").first();
await backdrop.click();
// Verify modal is closed
await page.waitForTimeout(500);
expect(await isSummaryModalOpen()).toBe(false);
});
test("closes summary modal via escape key @smoke", async ({
page,
waitForHero,
isSummaryModalOpen,
}) => {
// Open modal from hero
const hero = await waitForHero();
await hero.locator(SELECTORS.hero.readButton).click();
// Verify modal is open
expect(await isSummaryModalOpen()).toBe(true);
// Press escape
await page.keyboard.press("Escape");
// Verify modal is closed
await page.waitForTimeout(500);
expect(await isSummaryModalOpen()).toBe(false);
});
test("modal displays correct content sections", async ({
page,
waitForHero,
}) => {
// Open modal from hero
const hero = await waitForHero();
await hero.locator(SELECTORS.hero.readButton).click();
const modal = page.locator(SELECTORS.summaryModal.root);
// Check all required sections are present
await expect(modal.locator(SELECTORS.summaryModal.headline)).toBeVisible();
await expect(modal.locator(SELECTORS.summaryModal.image)).toBeVisible();
await expect(
modal.locator(SELECTORS.summaryModal.tldrSection),
).toBeVisible();
await expect(
modal.locator(SELECTORS.summaryModal.summarySection),
).toBeVisible();
await expect(
modal.locator(SELECTORS.summaryModal.sourceSection),
).toBeVisible();
await expect(
modal.locator(SELECTORS.summaryModal.shareSection),
).toBeVisible();
await expect(modal.locator(SELECTORS.summaryModal.poweredBy)).toBeVisible();
// Check TL;DR list has items
const tldrList = modal.locator(SELECTORS.summaryModal.tldrList);
const tldrItems = await tldrList.locator("li").count();
expect(tldrItems).toBeGreaterThanOrEqual(1);
// Check summary body has content
const summaryBody = modal.locator(SELECTORS.summaryModal.summaryBody);
await expect(summaryBody).not.toBeEmpty();
// Check source link is present
const sourceLink = modal.locator(SELECTORS.summaryModal.sourceLink);
await expect(sourceLink).toBeVisible();
await expect(sourceLink).toHaveAttribute("target", "_blank");
});
test("modal share controls are present and accessible", async ({
page,
waitForHero,
}) => {
// Open modal from hero
const hero = await waitForHero();
await hero.locator(SELECTORS.hero.readButton).click();
const modal = page.locator(SELECTORS.summaryModal.root);
// Check all share buttons are present
await expect(modal.locator(SELECTORS.summaryModal.shareX)).toBeVisible();
await expect(
modal.locator(SELECTORS.summaryModal.shareWhatsApp),
).toBeVisible();
await expect(
modal.locator(SELECTORS.summaryModal.shareLinkedIn),
).toBeVisible();
await expect(modal.locator(SELECTORS.summaryModal.shareCopy)).toBeVisible();
// Check accessible labels
await expect(modal.locator(SELECTORS.summaryModal.shareX)).toHaveAttribute(
"aria-label",
"Share on X",
);
await expect(
modal.locator(SELECTORS.summaryModal.shareWhatsApp),
).toHaveAttribute("aria-label", "Share on WhatsApp");
await expect(
modal.locator(SELECTORS.summaryModal.shareLinkedIn),
).toHaveAttribute("aria-label", "Share on LinkedIn");
await expect(
modal.locator(SELECTORS.summaryModal.shareCopy),
).toHaveAttribute("aria-label", "Copy article link");
});
test("modal returns to feed context after closing", async ({
page,
waitForHero,
waitForFeed,
isSummaryModalOpen,
}) => {
// Wait for feed to be visible
await waitForFeed();
// Open modal from hero
const hero = await waitForHero();
await hero.locator(SELECTORS.hero.readButton).click();
// Close modal
await page.keyboard.press("Escape");
await page.waitForTimeout(500);
// Verify modal is closed
expect(await isSummaryModalOpen()).toBe(false);
// Verify we're still on the same page (no navigation occurred)
await expect(page).toHaveURL(/\/$/);
// Verify feed is still visible
const feed = page.locator(SELECTORS.feed.root);
await expect(feed).toBeVisible();
});
});

View File

@@ -0,0 +1,139 @@
import { SELECTORS } from "../../fixtures/selectors";
import { expect, test } from "../../fixtures/test";
test.describe("Footer Link Rendering @smoke", () => {
test.beforeEach(async ({ gotoApp, waitForAppReady }) => {
await gotoApp();
await waitForAppReady();
});
test("footer renders GitHub link when configured", async ({ page }) => {
const githubLink = page.locator(SELECTORS.footer.githubLink);
// Check if GitHub link exists (may or may not be present based on config)
const count = await githubLink.count();
if (count > 0) {
// If present, should be visible and have correct attributes
await expect(githubLink).toBeVisible();
await expect(githubLink).toHaveAttribute("href");
await expect(githubLink).toHaveAttribute("target", "_blank");
await expect(githubLink).toHaveAttribute("rel", "noopener");
// Should link to GitHub
const href = await githubLink.getAttribute("href");
expect(href).toContain("github.com");
}
});
test("footer renders contact email link when configured", async ({
page,
}) => {
const contactLink = page.locator(SELECTORS.footer.contactLink);
// Check if contact link exists (may or may not be present based on config)
const count = await contactLink.count();
if (count > 0) {
// If present, should be visible and have correct attributes
await expect(contactLink).toBeVisible();
await expect(contactLink).toHaveAttribute("href");
// Should be mailto link
const href = await contactLink.getAttribute("href");
expect(href).toMatch(/^mailto:/);
// Should have email text
const text = await contactLink.textContent();
expect(text).toContain("@");
}
});
test("footer layout is stable regardless of link configuration", async ({
page,
}) => {
const footer = page.locator(SELECTORS.footer.root);
// Footer should always be visible
await expect(footer).toBeVisible();
// Footer should have consistent structure
const poweredBy = footer.locator(SELECTORS.footer.poweredBy);
await expect(poweredBy).toBeVisible();
const termsLink = footer.locator(SELECTORS.footer.termsLink);
await expect(termsLink).toBeVisible();
const attributionLink = footer.locator(SELECTORS.footer.attributionLink);
await expect(attributionLink).toBeVisible();
const copyright = footer.locator("text=All rights reserved");
await expect(copyright).toBeVisible();
});
test("footer links are interactive", async ({ page }) => {
// Terms link should open modal
const termsLink = page.locator(SELECTORS.footer.termsLink);
await termsLink.click();
const termsModal = page.locator(SELECTORS.policyModal.root);
await expect(termsModal).toBeVisible();
await expect(
termsModal.locator(SELECTORS.policyModal.termsTitle),
).toBeVisible();
// Close modal
await page.keyboard.press("Escape");
await page.waitForTimeout(500);
// Attribution link should open modal
const attributionLink = page.locator(SELECTORS.footer.attributionLink);
await attributionLink.click();
const attributionModal = page.locator(SELECTORS.policyModal.root);
await expect(attributionModal).toBeVisible();
await expect(
attributionModal.locator(SELECTORS.policyModal.attributionTitle),
).toBeVisible();
});
test("footer links have proper accessible names", async ({ page }) => {
// Terms link
const termsLink = page.locator(SELECTORS.footer.termsLink);
const termsText = await termsLink.textContent();
expect(termsText?.toLowerCase()).toContain("terms");
// Attribution link
const attributionLink = page.locator(SELECTORS.footer.attributionLink);
const attributionText = await attributionLink.textContent();
expect(attributionText?.toLowerCase()).toContain("attribution");
// GitHub link (if present)
const githubLink = page.locator(SELECTORS.footer.githubLink);
const githubCount = await githubLink.count();
if (githubCount > 0) {
const githubText = await githubLink.textContent();
expect(githubText?.toLowerCase()).toContain("github");
}
});
test("footer is responsive across viewports", async ({
page,
setViewport,
}) => {
const viewports = ["mobile", "tablet", "desktop"] as const;
for (const viewport of viewports) {
await setViewport(viewport);
const footer = page.locator(SELECTORS.footer.root);
await expect(footer).toBeVisible();
// Footer should not overflow
const footerBox = await footer.boundingBox();
const viewportWidth = await page.evaluate(() => window.innerWidth);
expect(footerBox!.width).toBeLessThanOrEqual(viewportWidth);
}
});
});

View File

@@ -0,0 +1,198 @@
import { SELECTORS } from "../../fixtures/selectors";
import { expect, test } from "../../fixtures/test";
test.describe("Contact Email Tooltip @smoke", () => {
test.beforeEach(async ({ gotoApp, waitForAppReady }) => {
await gotoApp();
await waitForAppReady();
});
test("tooltip appears on mouse hover", async ({ page }) => {
const contactLink = page.locator(SELECTORS.footer.contactLink);
// Check if contact link exists
const count = await contactLink.count();
if (count === 0) {
test.skip();
return;
}
// Hover over contact link
await contactLink.hover();
await page.waitForTimeout(500);
// Tooltip should appear
const tooltip = page.locator(SELECTORS.footer.contactHint);
await expect(tooltip).toBeVisible();
// Tooltip should have text
const tooltipText = await tooltip.textContent();
expect(tooltipText).toBeTruthy();
expect(tooltipText!.length).toBeGreaterThan(0);
});
test("tooltip disappears on mouse leave", async ({ page }) => {
const contactLink = page.locator(SELECTORS.footer.contactLink);
// Check if contact link exists
const count = await contactLink.count();
if (count === 0) {
test.skip();
return;
}
// Hover over contact link
await contactLink.hover();
await page.waitForTimeout(500);
const tooltip = page.locator(SELECTORS.footer.contactHint);
await expect(tooltip).toBeVisible();
// Move mouse away
await page.mouse.move(0, 0);
await page.waitForTimeout(500);
// Tooltip should disappear
await expect(tooltip).not.toBeVisible();
});
test("tooltip follows mouse movement", async ({ page }) => {
const contactLink = page.locator(SELECTORS.footer.contactLink);
// Check if contact link exists
const count = await contactLink.count();
if (count === 0) {
test.skip();
return;
}
// Hover over contact link
await contactLink.hover();
await page.waitForTimeout(500);
const tooltip = page.locator(SELECTORS.footer.contactHint);
await expect(tooltip).toBeVisible();
// Get initial position
const initialBox = await tooltip.boundingBox();
// Move mouse slightly
const linkBox = await contactLink.boundingBox();
await page.mouse.move(
linkBox!.x + linkBox!.width / 2 + 20,
linkBox!.y + linkBox!.height / 2,
);
await page.waitForTimeout(200);
// Tooltip should still be visible
await expect(tooltip).toBeVisible();
});
test("tooltip appears on keyboard focus", async ({ page }) => {
const contactLink = page.locator(SELECTORS.footer.contactLink);
// Check if contact link exists
const count = await contactLink.count();
if (count === 0) {
test.skip();
return;
}
// Focus contact link
await contactLink.focus();
await page.waitForTimeout(500);
// Tooltip should appear
const tooltip = page.locator(SELECTORS.footer.contactHint);
await expect(tooltip).toBeVisible();
});
test("tooltip disappears on keyboard blur", async ({ page }) => {
const contactLink = page.locator(SELECTORS.footer.contactLink);
// Check if contact link exists
const count = await contactLink.count();
if (count === 0) {
test.skip();
return;
}
// Focus contact link
await contactLink.focus();
await page.waitForTimeout(500);
const tooltip = page.locator(SELECTORS.footer.contactHint);
await expect(tooltip).toBeVisible();
// Blur contact link
await page.evaluate(() => (document.activeElement as HTMLElement)?.blur());
await page.waitForTimeout(500);
// Tooltip should disappear
await expect(tooltip).not.toBeVisible();
});
test("tooltip content is safe and appropriate", async ({ page }) => {
const contactLink = page.locator(SELECTORS.footer.contactLink);
// Check if contact link exists
const count = await contactLink.count();
if (count === 0) {
test.skip();
return;
}
// Hover to show tooltip
await contactLink.hover();
await page.waitForTimeout(500);
const tooltip = page.locator(SELECTORS.footer.contactHint);
const tooltipText = await tooltip.textContent();
// Should not contain inappropriate content
const inappropriateWords = [
"profanity",
"offensive",
"racist",
"sexist",
"misogynistic",
];
for (const word of inappropriateWords) {
expect(tooltipText?.toLowerCase()).not.toContain(word);
}
// Should contain helpful text
expect(tooltipText).toBeTruthy();
expect(tooltipText!.length).toBeGreaterThan(5);
});
test("tooltip does not trap focus", async ({ page }) => {
const contactLink = page.locator(SELECTORS.footer.contactLink);
// Check if contact link exists
const count = await contactLink.count();
if (count === 0) {
test.skip();
return;
}
// Focus contact link
await contactLink.focus();
await page.waitForTimeout(500);
// Tooltip should be visible
const tooltip = page.locator(SELECTORS.footer.contactHint);
await expect(tooltip).toBeVisible();
// Tab away
await page.keyboard.press("Tab");
await page.waitForTimeout(500);
// Tooltip should disappear
await expect(tooltip).not.toBeVisible();
// Focus should have moved
const isStillFocused = await contactLink.isFocused();
expect(isStillFocused).toBe(false);
});
});

View File

@@ -0,0 +1,191 @@
import { SELECTORS } from "../../fixtures/selectors";
import { expect, test } from "../../fixtures/test";
test.describe("Deep Link Permalink Tests @smoke", () => {
test.beforeEach(async ({ waitForAppReady }) => {
await waitForAppReady();
});
test("valid article permalink opens modal automatically", async ({
page,
gotoApp,
isSummaryModalOpen,
}) => {
// First get an article ID from the feed
await gotoApp();
await page.waitForSelector(SELECTORS.feed.articles, { timeout: 10000 });
const firstArticle = page.locator(SELECTORS.feed.articles).first();
const articleId = await firstArticle
.getAttribute("id")
.then((id) => (id ? parseInt(id.replace("news-", "")) : null));
expect(articleId).not.toBeNull();
// Navigate to article permalink
await gotoApp({ articleId: articleId! });
await page.waitForTimeout(2000); // Wait for modal to open
// Modal should be open
const isOpen = await isSummaryModalOpen();
expect(isOpen).toBe(true);
// Modal should show correct article
const modal = page.locator(SELECTORS.summaryModal.root);
const modalHeadline = await modal
.locator(SELECTORS.summaryModal.headline)
.textContent();
const articleHeadline = await firstArticle.locator("h3").textContent();
expect(modalHeadline).toBe(articleHeadline);
});
test("invalid article permalink shows error state", async ({
page,
gotoApp,
}) => {
// Navigate to invalid article ID
await gotoApp({ articleId: 999999 });
await page.waitForTimeout(2000);
// Should not show summary modal
const modal = page.locator(SELECTORS.summaryModal.root);
await expect(modal).not.toBeVisible();
// Should still show the page (not crash)
const hero = page.locator(SELECTORS.hero.root);
const feed = page.locator(SELECTORS.feed.root);
const heroVisible = await hero.isVisible().catch(() => false);
const feedVisible = await feed.isVisible().catch(() => false);
expect(heroVisible || feedVisible).toBe(true);
});
test("hero-origin modal flow via permalink", async ({
page,
gotoApp,
isSummaryModalOpen,
}) => {
// Get hero article ID
await gotoApp();
await page.waitForSelector(SELECTORS.hero.root, { timeout: 10000 });
const hero = page.locator(SELECTORS.hero.root);
const heroId = await hero
.getAttribute("id")
.then((id) => (id ? parseInt(id.replace("news-", "")) : null));
expect(heroId).not.toBeNull();
// Navigate directly to hero article
await gotoApp({ articleId: heroId! });
await page.waitForTimeout(2000);
// Modal should open
const isOpen = await isSummaryModalOpen();
expect(isOpen).toBe(true);
// Modal should show hero article content
const modal = page.locator(SELECTORS.summaryModal.root);
const modalHeadline = await modal
.locator(SELECTORS.summaryModal.headline)
.textContent();
const heroHeadline = await hero.locator("h1").textContent();
expect(modalHeadline).toBe(heroHeadline);
});
test("closing permalink modal updates URL", async ({
page,
gotoApp,
isSummaryModalOpen,
}) => {
// Open via permalink
await gotoApp({ articleId: 1 });
await page.waitForTimeout(2000);
// URL should have article parameter
await expect(page).toHaveURL(/\?article=\d+/);
// Close modal
await page.keyboard.press("Escape");
await page.waitForTimeout(500);
// URL should be cleaned up (parameter removed)
await expect(page).toHaveURL(/\/$/);
await expect(page).not.toHaveURL(/\?article=/);
});
test("modal state persists on page refresh", async ({
page,
gotoApp,
isSummaryModalOpen,
}) => {
// Open via permalink
await gotoApp({ articleId: 1 });
await page.waitForTimeout(2000);
// Verify modal is open
expect(await isSummaryModalOpen()).toBe(true);
// Refresh page
await page.reload();
await page.waitForTimeout(2000);
// Modal should still be open
expect(await isSummaryModalOpen()).toBe(true);
});
});
test.describe("Policy Modal Deep Links", () => {
test("terms policy modal opens via URL parameter", async ({
page,
gotoApp,
}) => {
await gotoApp({ policy: "terms" });
await page.waitForTimeout(1000);
// Policy modal should be visible
const modal = page.locator(SELECTORS.policyModal.root);
await expect(modal).toBeVisible();
// Should show terms title
const title = modal.locator(SELECTORS.policyModal.termsTitle);
await expect(title).toBeVisible();
});
test("attribution policy modal opens via URL parameter", async ({
page,
gotoApp,
}) => {
await gotoApp({ policy: "attribution" });
await page.waitForTimeout(1000);
// Policy modal should be visible
const modal = page.locator(SELECTORS.policyModal.root);
await expect(modal).toBeVisible();
// Should show attribution title
const title = modal.locator(SELECTORS.policyModal.attributionTitle);
await expect(title).toBeVisible();
});
test("closing policy modal clears URL parameter", async ({
page,
gotoApp,
}) => {
await gotoApp({ policy: "terms" });
await page.waitForTimeout(1000);
// URL should have policy parameter
await expect(page).toHaveURL(/\?policy=terms/);
// Close modal
const modal = page.locator(SELECTORS.policyModal.root);
await modal.locator(SELECTORS.policyModal.closeButton).click();
await page.waitForTimeout(500);
// URL should be cleaned up
await expect(page).toHaveURL(/\/$/);
await expect(page).not.toHaveURL(/\?policy=/);
});
});

View File

@@ -0,0 +1,193 @@
import { SELECTORS } from "../../fixtures/selectors";
import { expect, test } from "../../fixtures/test";
test.describe("Source CTA and Share Interactions", () => {
test.beforeEach(async ({ gotoApp, waitForAppReady, waitForHero }) => {
await gotoApp();
await waitForAppReady();
// Open modal from hero
const hero = await waitForHero();
await hero.locator(SELECTORS.hero.readButton).click();
await expect(page.locator(SELECTORS.summaryModal.root)).toBeVisible();
});
let page: any;
test("source link opens in new tab @smoke", async ({ page: p, context }) => {
page = p;
const modal = page.locator(SELECTORS.summaryModal.root);
const sourceLink = modal.locator(SELECTORS.summaryModal.sourceLink);
// Check link attributes
await expect(sourceLink).toHaveAttribute("target", "_blank");
await expect(sourceLink).toHaveAttribute("rel", "noopener");
await expect(sourceLink).toHaveAttribute("href");
// Click should open new tab
const [newPage] = await Promise.all([
context.waitForEvent("page"),
sourceLink.click(),
]);
// New page should have loaded
expect(newPage).toBeDefined();
await newPage.close();
// Modal should remain open
await expect(modal).toBeVisible();
});
test("share on X opens correct URL", async ({ page: p, context }) => {
page = p;
const modal = page.locator(SELECTORS.summaryModal.root);
const shareX = modal.locator(SELECTORS.summaryModal.shareX);
// Get article headline for URL verification
const headline = await modal
.locator(SELECTORS.summaryModal.headline)
.textContent();
// Click should open X share in new tab
const [newPage] = await Promise.all([
context.waitForEvent("page"),
shareX.click(),
]);
// Verify URL contains X intent
const url = newPage.url();
expect(url).toContain("x.com/intent/tweet");
expect(url).toContain(encodeURIComponent(headline || ""));
await newPage.close();
});
test("share on WhatsApp opens correct URL", async ({ page: p, context }) => {
page = p;
const modal = page.locator(SELECTORS.summaryModal.root);
const shareWhatsApp = modal.locator(SELECTORS.summaryModal.shareWhatsApp);
// Get article headline for URL verification
const headline = await modal
.locator(SELECTORS.summaryModal.headline)
.textContent();
// Click should open WhatsApp share in new tab
const [newPage] = await Promise.all([
context.waitForEvent("page"),
shareWhatsApp.click(),
]);
// Verify URL contains WhatsApp share
const url = newPage.url();
expect(url).toContain("wa.me");
expect(url).toContain(encodeURIComponent(headline || ""));
await newPage.close();
});
test("share on LinkedIn opens correct URL", async ({ page: p, context }) => {
page = p;
const modal = page.locator(SELECTORS.summaryModal.root);
const shareLinkedIn = modal.locator(SELECTORS.summaryModal.shareLinkedIn);
// Click should open LinkedIn share in new tab
const [newPage] = await Promise.all([
context.waitForEvent("page"),
shareLinkedIn.click(),
]);
// Verify URL contains LinkedIn share
const url = newPage.url();
expect(url).toContain("linkedin.com/sharing");
await newPage.close();
});
test("copy link button copies permalink to clipboard @smoke", async ({
page: p,
context,
}) => {
page = p;
const modal = page.locator(SELECTORS.summaryModal.root);
const copyButton = modal.locator(SELECTORS.summaryModal.shareCopy);
// Grant clipboard permissions
await context.grantPermissions(["clipboard-read", "clipboard-write"]);
// Click copy button
await copyButton.click();
// Wait for success message
const successMessage = modal.locator(SELECTORS.summaryModal.copySuccess);
await expect(successMessage).toBeVisible();
await expect(successMessage).toContainText("Permalink copied");
// Verify clipboard content
const clipboardContent = await page.evaluate(() =>
navigator.clipboard.readText(),
);
expect(clipboardContent).toContain("/?article=");
expect(clipboardContent).toMatch(/http/);
});
test("copy link does not navigate away", async ({ page: p }) => {
page = p;
const modal = page.locator(SELECTORS.summaryModal.root);
const copyButton = modal.locator(SELECTORS.summaryModal.shareCopy);
// Get current URL
const currentUrl = page.url();
// Click copy button
await copyButton.click();
// Wait a moment
await page.waitForTimeout(1000);
// URL should not have changed
await expect(page).toHaveURL(currentUrl);
// Modal should still be open
await expect(modal).toBeVisible();
});
test("navigation is preserved after share interactions", async ({
page: p,
context,
}) => {
page = p;
const modal = page.locator(SELECTORS.summaryModal.root);
// Interact with multiple share buttons
const shareButtons = [
modal.locator(SELECTORS.summaryModal.shareX),
modal.locator(SELECTORS.summaryModal.shareWhatsApp),
modal.locator(SELECTORS.summaryModal.shareLinkedIn),
];
for (const button of shareButtons) {
if (await button.isVisible()) {
const [newPage] = await Promise.all([
context.waitForEvent("page"),
button.click(),
]);
await newPage.close();
// Modal should remain open after each interaction
await expect(modal).toBeVisible();
}
}
// Close modal
await page.keyboard.press("Escape");
await page.waitForTimeout(500);
// Should be back on main page without navigation
await expect(page).toHaveURL(/\/$/);
// Feed should be visible
const feed = page.locator(SELECTORS.feed.root);
await expect(feed).toBeVisible();
});
});

View File

@@ -0,0 +1,233 @@
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);
}
});
});

View File

@@ -0,0 +1,250 @@
import { SELECTORS } from "../../fixtures/selectors";
import { expect, test } from "../../fixtures/test";
import { getStickyPosition, ViewportSize } from "../../fixtures/viewports";
test.describe("Sticky Header Behavior @smoke", () => {
test.beforeEach(async ({ gotoApp, waitForAppReady }) => {
await gotoApp();
await waitForAppReady();
});
test("header is sticky on scroll", async ({ page }) => {
const header = page.locator(SELECTORS.header.root);
// Check initial position
const initialPosition = await getStickyPosition(
page,
SELECTORS.header.root,
);
expect(initialPosition.isSticky).toBe(true);
// Scroll down
await page.evaluate(() => window.scrollTo(0, 500));
await page.waitForTimeout(500);
// Header should still be at top
const scrolledPosition = await getStickyPosition(
page,
SELECTORS.header.root,
);
expect(scrolledPosition.top).toBeLessThanOrEqual(10); // Allow small offset
expect(scrolledPosition.isSticky).toBe(true);
});
test("header shrinks on scroll", async ({ page }) => {
const header = page.locator(SELECTORS.header.root);
const headerContainer = header.locator("> div");
// Get initial height
const initialHeight = await headerContainer.evaluate(
(el) => el.offsetHeight,
);
// Scroll down
await page.evaluate(() => window.scrollTo(0, 300));
await page.waitForTimeout(500);
// Get scrolled height
const scrolledHeight = await headerContainer.evaluate(
(el) => el.offsetHeight,
);
// Header should shrink (or stay same, but not grow)
expect(scrolledHeight).toBeLessThanOrEqual(initialHeight);
});
test("header maintains glass effect on scroll", async ({ page }) => {
// Scroll down
await page.evaluate(() => window.scrollTo(0, 500));
await page.waitForTimeout(500);
// Check header has backdrop blur
const hasBlur = await page.evaluate(() => {
const header = document.querySelector("header");
if (!header) return false;
const style = window.getComputedStyle(header);
return style.backdropFilter.includes("blur");
});
expect(hasBlur).toBe(true);
});
});
test.describe("Sticky Footer Behavior", () => {
test.beforeEach(async ({ gotoApp, waitForAppReady }) => {
await gotoApp();
await waitForAppReady();
});
test("footer is sticky at bottom", async ({ page }) => {
const footer = page.locator(SELECTORS.footer.root);
// Check footer is visible
await expect(footer).toBeVisible();
// Check footer position
const footerBox = await footer.boundingBox();
const viewportHeight = await page.evaluate(() => window.innerHeight);
// Footer should be at bottom of viewport
expect(footerBox!.y + footerBox!.height).toBeGreaterThanOrEqual(
viewportHeight - 10,
);
});
test("footer does not overlap main content", async ({ page }) => {
const footer = page.locator(SELECTORS.footer.root);
const mainContent = page.locator("main");
// Get bounding boxes
const footerBox = await footer.boundingBox();
const mainBox = await mainContent.boundingBox();
// Main content should have padding at bottom to account for footer
const bodyPadding = await page.evaluate(() => {
const body = document.body;
const style = window.getComputedStyle(body);
return parseInt(style.paddingBottom || "0");
});
expect(bodyPadding).toBeGreaterThan(0);
});
});
test.describe("Back to Top Behavior @smoke", () => {
test.beforeEach(async ({ gotoApp, waitForAppReady }) => {
await gotoApp();
await waitForAppReady();
});
test("back to top is hidden initially", async ({ page }) => {
const backToTop = page.locator(SELECTORS.backToTop.root);
// Should not be visible at top of page
const isVisible = await backToTop.isVisible().catch(() => false);
expect(isVisible).toBe(false);
});
test("back to top appears on scroll", async ({ page }) => {
// Scroll down
await page.evaluate(() => window.scrollTo(0, 800));
await page.waitForTimeout(500);
const backToTop = page.locator(SELECTORS.backToTop.root);
// Should be visible after scroll
await expect(backToTop).toBeVisible();
});
test("back to top scrolls to top when clicked", async ({ page }) => {
// Scroll down
await page.evaluate(() => window.scrollTo(0, 1000));
await page.waitForTimeout(500);
// Click back to top
const backToTop = page.locator(SELECTORS.backToTop.root);
await backToTop.click();
// Wait for scroll animation
await page.waitForTimeout(1000);
// Should be at top
const scrollPosition = await page.evaluate(() => window.scrollY);
expect(scrollPosition).toBeLessThanOrEqual(50);
});
test("back to top is accessible", async ({ page }) => {
// Scroll down to make visible
await page.evaluate(() => window.scrollTo(0, 800));
await page.waitForTimeout(500);
const backToTop = page.locator(SELECTORS.backToTop.root);
// Should have aria-label
await expect(backToTop).toHaveAttribute("aria-label");
// Should be keyboard focusable
await backToTop.focus();
await expect(backToTop).toBeFocused();
});
});
test.describe("Sticky Behavior Across Breakpoints", () => {
test("header and footer work on mobile", async ({
gotoApp,
waitForAppReady,
setViewport,
page,
}) => {
await setViewport("mobile");
await gotoApp();
await waitForAppReady();
// Check header is sticky
const header = page.locator(SELECTORS.header.root);
await expect(header).toBeVisible();
// Scroll down
await page.evaluate(() => window.scrollTo(0, 500));
await page.waitForTimeout(500);
// Header should still be visible
await expect(header).toBeVisible();
// Footer should be visible
const footer = page.locator(SELECTORS.footer.root);
await expect(footer).toBeVisible();
});
test("header and footer work on tablet", async ({
gotoApp,
waitForAppReady,
setViewport,
page,
}) => {
await setViewport("tablet");
await gotoApp();
await waitForAppReady();
// Check header is sticky
const header = page.locator(SELECTORS.header.root);
await expect(header).toBeVisible();
// Scroll down
await page.evaluate(() => window.scrollTo(0, 500));
await page.waitForTimeout(500);
// Header should still be visible
await expect(header).toBeVisible();
// Footer should be visible
const footer = page.locator(SELECTORS.footer.root);
await expect(footer).toBeVisible();
});
test("header and footer work on desktop", async ({
gotoApp,
waitForAppReady,
setViewport,
page,
}) => {
await setViewport("desktop");
await gotoApp();
await waitForAppReady();
// Check header is sticky
const header = page.locator(SELECTORS.header.root);
await expect(header).toBeVisible();
// Scroll down
await page.evaluate(() => window.scrollTo(0, 500));
await page.waitForTimeout(500);
// Header should still be visible
await expect(header).toBeVisible();
// Footer should be visible
const footer = page.locator(SELECTORS.footer.root);
await expect(footer).toBeVisible();
});
});