p19-bug-fixes
This commit is contained in:
347
e2e/tests/fixtures/test.ts
vendored
Normal file
347
e2e/tests/fixtures/test.ts
vendored
Normal file
@@ -0,0 +1,347 @@
|
||||
import { test as base, expect, type Locator, Page } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Viewport profiles for responsive testing
|
||||
*/
|
||||
export const VIEWPORT_PROFILES = {
|
||||
mobile: { width: 375, height: 667 },
|
||||
tablet: { width: 768, height: 1024 },
|
||||
desktop: { width: 1280, height: 720 },
|
||||
widescreen: { width: 1920, height: 1080 },
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Theme profiles for accessibility testing
|
||||
*/
|
||||
export const THEME_PROFILES = {
|
||||
light: "light",
|
||||
dark: "dark",
|
||||
contrast: "contrast",
|
||||
} as const;
|
||||
|
||||
export type ThemeProfile = keyof typeof THEME_PROFILES;
|
||||
export type ViewportProfile = keyof typeof VIEWPORT_PROFILES;
|
||||
|
||||
/**
|
||||
* Article data shape for deterministic testing
|
||||
*/
|
||||
export interface TestArticle {
|
||||
id: number;
|
||||
headline: string;
|
||||
summary: string;
|
||||
source_url: string;
|
||||
source_citation: string;
|
||||
published_at: string;
|
||||
image_url: string;
|
||||
summary_image_url?: string;
|
||||
tldr_points?: string[];
|
||||
summary_body?: string;
|
||||
language?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test fixture interface
|
||||
*/
|
||||
export interface ClawFortFixtures {
|
||||
/**
|
||||
* Navigate to application with optional article permalink
|
||||
*/
|
||||
gotoApp: (options?: {
|
||||
articleId?: number;
|
||||
policy?: "terms" | "attribution";
|
||||
}) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Set theme preference on the page
|
||||
*/
|
||||
setTheme: (theme: ThemeProfile) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Set viewport to a named profile
|
||||
*/
|
||||
setViewport: (profile: ViewportProfile) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Wait for hero section to be loaded
|
||||
*/
|
||||
waitForHero: () => Promise<Locator>;
|
||||
|
||||
/**
|
||||
* Wait for news feed to be loaded
|
||||
*/
|
||||
waitForFeed: () => Promise<Locator>;
|
||||
|
||||
/**
|
||||
* Get hero article data from page
|
||||
*/
|
||||
getHeroArticle: () => Promise<TestArticle | null>;
|
||||
|
||||
/**
|
||||
* Get feed articles data from page
|
||||
*/
|
||||
getFeedArticles: () => Promise<TestArticle[]>;
|
||||
|
||||
/**
|
||||
* Open summary modal for an article
|
||||
*/
|
||||
openSummaryModal: (articleId: number) => Promise<Locator>;
|
||||
|
||||
/**
|
||||
* Close summary modal
|
||||
*/
|
||||
closeSummaryModal: () => Promise<void>;
|
||||
|
||||
/**
|
||||
* Check if summary modal is open
|
||||
*/
|
||||
isSummaryModalOpen: () => Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Get stable selector for critical controls
|
||||
*/
|
||||
getControl: (name: string) => Locator;
|
||||
|
||||
/**
|
||||
* Wait for app to be fully initialized
|
||||
*/
|
||||
waitForAppReady: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended test with ClawFort fixtures
|
||||
*/
|
||||
export const test = base.extend<ClawFortFixtures>({
|
||||
gotoApp: async ({ page }, use) => {
|
||||
await use(async (options = {}) => {
|
||||
let url = "/";
|
||||
|
||||
if (options.articleId) {
|
||||
url = `/?article=${options.articleId}`;
|
||||
} else if (options.policy) {
|
||||
url = `/?policy=${options.policy}`;
|
||||
}
|
||||
|
||||
await page.goto(url);
|
||||
await page.waitForLoadState("networkidle");
|
||||
});
|
||||
},
|
||||
|
||||
setTheme: async ({ page }, use) => {
|
||||
await use(async (theme: ThemeProfile) => {
|
||||
// Open theme menu
|
||||
const themeButton = page.locator("#theme-menu-button");
|
||||
await themeButton.click();
|
||||
|
||||
// Select theme option
|
||||
const themeOption = page.locator(`[data-theme-option="${theme}"]`);
|
||||
await themeOption.click();
|
||||
|
||||
// Wait for theme to apply
|
||||
await page.waitForSelector(`html[data-theme="${theme}"]`, {
|
||||
timeout: 5000,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
setViewport: async ({ page }, use) => {
|
||||
await use(async (profile: ViewportProfile) => {
|
||||
const viewport = VIEWPORT_PROFILES[profile];
|
||||
await page.setViewportSize(viewport);
|
||||
});
|
||||
},
|
||||
|
||||
waitForHero: async ({ page }, use) => {
|
||||
await use(async () => {
|
||||
const hero = page
|
||||
.locator(
|
||||
'article[itemscope][itemtype="https://schema.org/NewsArticle"]',
|
||||
)
|
||||
.first();
|
||||
await hero.waitFor({ state: "visible", timeout: 15000 });
|
||||
return hero;
|
||||
});
|
||||
},
|
||||
|
||||
waitForFeed: async ({ page }, use) => {
|
||||
await use(async () => {
|
||||
const feed = page.locator('section:has(h2:has-text("Recent News"))');
|
||||
await feed.waitFor({ state: "visible", timeout: 15000 });
|
||||
return feed;
|
||||
});
|
||||
},
|
||||
|
||||
getHeroArticle: async ({ page }, use) => {
|
||||
await use(async () => {
|
||||
const hero = await page
|
||||
.locator(
|
||||
'article[itemscope][itemtype="https://schema.org/NewsArticle"]',
|
||||
)
|
||||
.first();
|
||||
|
||||
// Check if hero exists and has content
|
||||
const count = await hero.count();
|
||||
if (count === 0) return null;
|
||||
|
||||
// Extract hero article data
|
||||
const headline = await hero
|
||||
.locator("h1")
|
||||
.textContent()
|
||||
.catch(() => null);
|
||||
const summary = await hero
|
||||
.locator(".hero-summary")
|
||||
.textContent()
|
||||
.catch(() => null);
|
||||
const id = await hero
|
||||
.getAttribute("id")
|
||||
.then((id) => (id ? parseInt(id.replace("news-", "")) : null));
|
||||
|
||||
if (!headline || !id) return null;
|
||||
|
||||
return {
|
||||
id,
|
||||
headline,
|
||||
summary: summary || "",
|
||||
source_url: "",
|
||||
source_citation: "",
|
||||
published_at: new Date().toISOString(),
|
||||
image_url: "",
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
getFeedArticles: async ({ page }, use) => {
|
||||
await use(async () => {
|
||||
const articles = await page
|
||||
.locator(
|
||||
'article[itemscope][itemtype="https://schema.org/NewsArticle"]',
|
||||
)
|
||||
.all();
|
||||
const feedArticles: TestArticle[] = [];
|
||||
|
||||
for (const article of articles.slice(1)) {
|
||||
// Skip hero
|
||||
const id = await article
|
||||
.getAttribute("id")
|
||||
.then((id) => (id ? parseInt(id.replace("news-", "")) : null));
|
||||
const headline = await article
|
||||
.locator("h3")
|
||||
.textContent()
|
||||
.catch(() => null);
|
||||
const summary = await article
|
||||
.locator(".news-card-summary")
|
||||
.textContent()
|
||||
.catch(() => null);
|
||||
|
||||
if (id && headline) {
|
||||
feedArticles.push({
|
||||
id,
|
||||
headline,
|
||||
summary: summary || "",
|
||||
source_url: "",
|
||||
source_citation: "",
|
||||
published_at: new Date().toISOString(),
|
||||
image_url: "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return feedArticles;
|
||||
});
|
||||
},
|
||||
|
||||
openSummaryModal: async ({ page }, use) => {
|
||||
await use(async (articleId: number) => {
|
||||
// Find article and click its "Read TL;DR" button
|
||||
const article = page.locator(`#news-${articleId}`);
|
||||
const readButton = article.locator('button:has-text("Read TL;DR")');
|
||||
await readButton.click();
|
||||
|
||||
// Wait for modal to appear
|
||||
const modal = page
|
||||
.locator('[role="dialog"][aria-modal="true"]')
|
||||
.filter({ hasText: "TL;DR" });
|
||||
await modal.waitFor({ state: "visible", timeout: 5000 });
|
||||
|
||||
return modal;
|
||||
});
|
||||
},
|
||||
|
||||
closeSummaryModal: async ({ page }, use) => {
|
||||
await use(async () => {
|
||||
const closeButton = page.locator(
|
||||
'[role="dialog"] button:has-text("Close")',
|
||||
);
|
||||
await closeButton.click();
|
||||
|
||||
// Wait for modal to disappear
|
||||
const modal = page
|
||||
.locator('[role="dialog"][aria-modal="true"]')
|
||||
.filter({ hasText: "TL;DR" });
|
||||
await modal.waitFor({ state: "hidden", timeout: 5000 });
|
||||
});
|
||||
},
|
||||
|
||||
isSummaryModalOpen: async ({ page }, use) => {
|
||||
await use(async () => {
|
||||
const modal = page
|
||||
.locator('[role="dialog"][aria-modal="true"]')
|
||||
.filter({ hasText: "TL;DR" });
|
||||
return await modal.isVisible().catch(() => false);
|
||||
});
|
||||
},
|
||||
|
||||
getControl: async ({ page }, use) => {
|
||||
await use((name: string) => {
|
||||
// Stable selector strategy: prefer test-id, then role, then accessible name
|
||||
const selectors: Record<string, string> = {
|
||||
"theme-menu": "#theme-menu-button",
|
||||
"language-select": "#language-select",
|
||||
"back-to-top": '[aria-label="Back to top"]',
|
||||
"share-x": '[aria-label="Share on X"]',
|
||||
"share-whatsapp": '[aria-label="Share on WhatsApp"]',
|
||||
"share-linkedin": '[aria-label="Share on LinkedIn"]',
|
||||
"share-copy": '[aria-label="Copy article link"]',
|
||||
"modal-close": '[role="dialog"] button:has-text("Close")',
|
||||
"terms-link": 'button:has-text("Terms of Use")',
|
||||
"attribution-link": 'button:has-text("Attribution")',
|
||||
"hero-read-more": 'article:first-of-type button:has-text("Read TL;DR")',
|
||||
"skip-link": 'a[href="#main-content"]',
|
||||
};
|
||||
|
||||
const selector = selectors[name];
|
||||
if (!selector) {
|
||||
throw new Error(
|
||||
`Unknown control: ${name}. Available controls: ${Object.keys(selectors).join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
return page.locator(selector);
|
||||
});
|
||||
},
|
||||
|
||||
waitForAppReady: async ({ page }, use) => {
|
||||
await use(async () => {
|
||||
// Wait for page to be fully loaded
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Wait for Alpine.js to initialize
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
return (
|
||||
document.querySelector("html")?.hasAttribute("data-theme") ||
|
||||
document.readyState === "complete"
|
||||
);
|
||||
},
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
|
||||
// Wait for either hero or feed to be visible
|
||||
await Promise.race([
|
||||
page.waitForSelector("article[itemscope]", { timeout: 15000 }),
|
||||
page.waitForSelector('.text-6xl:has-text("🤖")', { timeout: 15000 }), // No news state
|
||||
]);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export { expect };
|
||||
Reference in New Issue
Block a user