feat(playwright): integrate Playwright for end-to-end testing and update .gitignore

- Added Playwright as a development dependency for end-to-end testing.
- Updated package.json to include test scripts for Playwright.
- Enhanced .gitignore to exclude Playwright test results and cache files.
- Improved content extraction logic in various components to handle new content types.

Made-with: Cursor
This commit is contained in:
Dorian
2026-03-02 22:02:19 +00:00
parent 5414060225
commit 43ca3a7837
23 changed files with 653 additions and 13 deletions
@@ -66,9 +66,15 @@ function isWebsitesLikeResponse(text: string): boolean {
function extractUrlFromText(text: string): string | undefined {
const mdLink = /\[([^\]]*)\]\((https?:\/\/[^)]+)\)/.exec(text)
if (mdLink) return mdLink[2]
const bare = /(https?:\/\/[^\s)\]\"'<>]+)/.exec(text)
return bare ? bare[1] : undefined
const raw = mdLink ? mdLink[2] : (/(https?:\/\/[^\s)\]\"'<>]+)/.exec(text)?.[1])
if (!raw?.trim()) return undefined
try {
const u = new URL(raw.trim())
if (!/^https?:$/i.test(u.protocol)) return undefined
return u.href
} catch {
return undefined
}
}
function extractAuthorFromText(text: string): string | undefined {
@@ -90,9 +96,15 @@ function extractAuthorFromText(text: string): string | undefined {
function extractFirstImageFromText(text: string): string | undefined {
const mdImg = /!\[[^\]]*\]\((https?:\/\/[^)]+)\)/.exec(text)
if (mdImg) return mdImg[1]
const ext = /(https?:\/\/[^\s)\]\"'<>]+\.(?:jpg|jpeg|png|gif|webp)(?:\?[^\s)\]]*)?)/i.exec(text)
return ext ? ext[1] : undefined
const raw = mdImg ? mdImg[1] : (/(https?:\/\/[^\s)\]\"'<>]+\.(?:jpg|jpeg|png|gif|webp)(?:\?[^\s)\]]*)?)/i.exec(text)?.[1])
if (!raw?.trim()) return undefined
try {
const u = new URL(raw.trim())
if (!/^https?:$/i.test(u.protocol)) return undefined
return u.href
} catch {
return undefined
}
}
const MAGAZINE_CONTENT_MAX = 2000
@@ -109,12 +121,19 @@ function addSection(
const key = `${t.slice(0, 50)}`
if (seen.has(key)) return
seen.add(key)
const imgMatch = /!\[[^\]]*\]\((https?:\/\/[^)]+)\)/.exec(content)?.[1]
const imageUrl = imgMatch ? (() => {
try {
const u = new URL(imgMatch.trim())
return /^https?:$/i.test(u.protocol) ? u.href : undefined
} catch { return undefined }
})() : undefined
sections.push({
title: t,
content: c,
url: extractUrlFromText(content),
author: extractAuthorFromText(content),
imageUrl: /!\[[^\]]*\]\((https?:\/\/[^)]+)\)/.exec(content)?.[1],
imageUrl,
})
}
@@ -315,6 +334,23 @@ function looksLikeSong(title: string, artist: string): boolean {
}
const PODCAST_TAG_RE = /\[\[podcast:(p?\d+)\]\]/gi
const PODCAST_EXT_RE = /\[\[podcast_ext:([^|]+)\|([^|]+)(?:\|(\d{4}))?\]\]/gi
/** Reject obvious non-podcast phrases (documentation, mailing lists, etc.) */
function looksLikePodcast(title: string, host: string): boolean {
const t = title.toLowerCase()
const h = host.toLowerCase()
const bad = [
'bitcoin mailing list', 'mailing list', 'developer mailing list', 'gnusha.org',
'canonical source', 'formal dev', 'github', 'stackexchange', 'reddit', 'twitter',
'latest news', 'protocol updates', 'web search', 'training cutoff',
'documentation', 'bip discussion', 'bip 110', 'bitcoin bips',
]
for (const phrase of bad) {
if (t.includes(phrase) || h.includes(phrase)) return false
}
if (t.length > 80 || h.length > 50) return false
return true
}
const MARKDOWN_LINK_RE = /\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g
const SAFE_URL_SCHEME = /^https?:\/\//i
@@ -573,6 +609,7 @@ export function useContentPanel() {
while ((match = re.exec(text)) !== null) {
const title = match[1].trim()
const host = match[2].trim()
if (!looksLikePodcast(title, host)) continue
const year = match[3] ? parseInt(match[3], 10) : undefined
const key = `${title.toLowerCase()}|${host.toLowerCase()}`
if (seen.has(key)) continue
@@ -620,8 +657,8 @@ export function useContentPanel() {
const hasNews = (webResults.length > 0 || mergedWebsites.length > 0) && newsContext
const mergedNews = hasNews ? mergeNewsResults(webResults, panelRssArticles.value) : []
// Fetch RSS from website URLs to surface actual articles in News
if (mergedWebsites.length > 0) {
// Fetch RSS from website URLs only when news context — avoid surfacing RSS from docs/resource links
if (mergedWebsites.length > 0 && newsContext) {
const urls = mergedWebsites.map((w) => w.url)
fetchRssFromUrls(urls).then((articles) => {
if (articles.length === 0) return