/** * Stable selector strategy for ClawFort UI elements * * Strategy (in order of preference): * 1. data-testid attributes (most stable) * 2. ARIA roles with accessible names * 3. Semantic HTML elements with text content * 4. ID selectors (for unique elements) * 5. Structural selectors (last resort) */ export const SELECTORS = { // Header controls header: { root: "header", logo: 'a[href="/"]', languageSelect: "#language-select", themeMenuButton: "#theme-menu-button", themeMenu: "#theme-menu", themeOption: (theme: string) => `[data-theme-option="${theme}"]`, }, // Skip link skipLink: 'a[href="#main-content"]', // Hero section hero: { root: 'article[itemscope][itemtype="https://schema.org/NewsArticle"]:first-of-type', headline: "h1", summary: ".hero-summary", meta: ".hero-meta", readButton: 'button:has-text("Read TL;DR")', sourceLink: "a.source-link", image: 'img[fetchpriority="high"]', latestPill: ".hero-latest-pill", timePill: ".hero-time-pill", }, // News feed feed: { root: 'section:has(h2:has-text("Recent News"))', articles: 'article[itemscope][itemtype="https://schema.org/NewsArticle"]:not(:first-of-type)', article: (id: number) => `#news-${id}`, articleTitle: "h3", articleSummary: ".news-card-summary", articleReadButton: 'button:has-text("Read TL;DR")', articleSource: "a.source-link", }, // Summary modal summaryModal: { root: '[role="dialog"][aria-modal="true"]:has-text("TL;DR")', closeButton: 'button:has-text("Close")', headline: "h2", image: "img", tldrSection: 'h3:has-text("TL;DR")', tldrList: "ul", summarySection: 'h3:has-text("Summary")', summaryBody: ".modal-body-text", sourceSection: 'h3:has-text("Source and Citation")', sourceLink: 'a:has-text("Read Full Article")', shareSection: 'h3:has-text("Share")', shareX: '[aria-label="Share on X"]', shareWhatsApp: '[aria-label="Share on WhatsApp"]', shareLinkedIn: '[aria-label="Share on LinkedIn"]', shareCopy: '[aria-label="Copy article link"]', copySuccess: "text=Permalink copied.", poweredBy: "text=Powered by Perplexity", }, // Policy modals policyModal: { root: '[role="dialog"][aria-modal="true"]:has(h2)', closeButton: 'button:has-text("Close")', termsTitle: 'h2:has-text("Terms of Use")', attributionTitle: 'h2:has-text("Attribution and Ownership Disclaimer")', }, // Footer footer: { root: "footer", poweredBy: 'a[href*="perplexity"]', termsLink: 'button:has-text("Terms of Use")', attributionLink: 'button:has-text("Attribution")', githubLink: 'a:has-text("GitHub")', contactLink: 'a[href^="mailto:"]', contactHint: "#contact-hint", copyright: "text=All rights reserved", }, // Back to top backToTop: { root: '[aria-label="Back to top"]', icon: "svg", }, // Theme menu themeMenu: { root: "#theme-menu", options: '[role="menuitem"]', option: (theme: string) => `[data-theme-option="${theme}"]`, }, // Empty state emptyState: { root: '.text-6xl:has-text("🤖")', heading: 'h2:has-text("No News Yet")', }, } as const; /** * Get a stable locator string for a control */ export function getSelector(path: string): string { const parts = path.split("."); let current: any = SELECTORS; for (const part of parts) { if (current[part] === undefined) { throw new Error(`Invalid selector path: ${path}`); } current = current[part]; } if (typeof current === "function") { throw new Error(`Selector path requires parameter: ${path}`); } return current as string; } /** * Test ID attributes that should be added to the frontend for better test stability */ export const RECOMMENDED_TEST_IDS = { // Header "header-root": "header", "header-logo": "header-logo", "language-select": "language-select", "theme-menu-button": "theme-menu-button", // Hero "hero-article": "hero-article", "hero-headline": "hero-headline", "hero-read-button": "hero-read-button", // Feed "feed-section": "feed-section", "feed-article": (id: number) => `feed-article-${id}`, "feed-read-button": (id: number) => `feed-read-button-${id}`, // Modal "summary-modal": "summary-modal", "summary-modal-close": "summary-modal-close", "summary-modal-headline": "summary-modal-headline", // Footer "footer-root": "footer", "footer-contact": "footer-contact", // Back to top "back-to-top": "back-to-top", };