369 lines
11 KiB
TypeScript
369 lines
11 KiB
TypeScript
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();
|
|
});
|
|
});
|