165 lines
4.3 KiB
TypeScript
165 lines
4.3 KiB
TypeScript
/**
|
|
* 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",
|
|
};
|