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:
@@ -74,8 +74,12 @@ defineEmits<{ back: [] }>()
|
||||
const { isDark } = useTheme()
|
||||
|
||||
const articleDomain = computed(() => {
|
||||
const url = props.article?.url
|
||||
if (!url || typeof url !== 'string') return ''
|
||||
try {
|
||||
return new URL(props.article.url).hostname.replace(/^www\./, '')
|
||||
const u = new URL(url)
|
||||
if (!/^https?:$/i.test(u.protocol)) return ''
|
||||
return u.hostname.replace(/^www\./, '')
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="relative flex-1 flex flex-col min-h-0 overflow-hidden">
|
||||
<div
|
||||
v-if="contextType === 'film' || contextType === 'song' || contextType === 'podcast' || contextType === 'websites'"
|
||||
v-if="['film','song','podcast','news','websites','magazine'].includes(contextType)"
|
||||
class="flex-1 flex flex-col min-h-0"
|
||||
>
|
||||
<div
|
||||
@@ -75,7 +75,7 @@ import LoadingFilmGrid from './LoadingFilmGrid.vue'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
contextType?: 'film' | 'song' | 'podcast' | 'websites' | 'generic'
|
||||
contextType?: 'film' | 'song' | 'podcast' | 'news' | 'websites' | 'magazine' | 'generic'
|
||||
}>(),
|
||||
{ contextType: 'film' }
|
||||
)
|
||||
@@ -86,7 +86,9 @@ const contextLabel = computed(() => {
|
||||
if (props.contextType === 'film') return 'Film recommendations'
|
||||
if (props.contextType === 'song') return 'Song recommendations'
|
||||
if (props.contextType === 'podcast') return 'Podcast recommendations'
|
||||
if (props.contextType === 'news') return 'Articles'
|
||||
if (props.contextType === 'websites') return 'Websites'
|
||||
if (props.contextType === 'magazine') return 'Brief'
|
||||
return 'Content'
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -340,6 +340,8 @@ const loaderContextType = computed(() => {
|
||||
const q = (lastUser?.content ?? '').toLowerCase()
|
||||
if (/\b(song|music|track|album|band|artist|listen)\b/.test(q)) return 'song'
|
||||
if (/\b(podcast|episode|show|listen to)\b/.test(q)) return 'podcast'
|
||||
if (/\b(news|latest|recent|current|what'?s happening|what are people saying)\b/.test(q)) return 'news'
|
||||
if (/\b(bip|protocol|debate|sentiment|bearish|bull case|macro)\b/.test(q)) return 'magazine'
|
||||
if (/\b(website|websites|where to check|best places|check online|resources?|sources?)\b/.test(q)) return 'websites'
|
||||
return contentType.value
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user