// Vitest uses globals (configured in vite.config.mjs: test.globals = true). // Pure logic tests for the news image prompt builder helpers. // These helpers live in StudioNewsEditor.jsx — this file duplicates them // so they can be tested in isolation without pulling in the full editor bundle. // ── Minimal copies of the prompt-builder helpers ────────────────────────── const NEWS_PROMPT_TYPE_MOODS = { announcement: 'Futuristic', release: 'Software Release', editorial: 'Editorial', opinion: 'Editorial', tutorial: 'Clean Instructional', platform_update: 'Modern Tech', event: 'Futuristic', challenge: 'Futuristic', interview: 'Editorial', spotlight: 'Editorial', archive: 'Retro Tech', industry_news: 'Modern Tech', review: 'Modern Tech', roundup: 'Modern Tech', } const NEWS_PROMPT_TYPE_ADDONS = { release: 'Use a glossy software-release poster style with product UI panels, feature highlights, and a polished launch atmosphere.', announcement: 'Use a clean announcement-poster style with a strong headline, clear hero image, and supporting modules that communicate the main update quickly.', editorial: 'Use a refined editorial magazine-cover style with a strong visual concept, cleaner composition, and slightly more cinematic atmosphere.', opinion: 'Use a refined editorial magazine-cover style with a strong visual concept, cleaner composition, and slightly more cinematic atmosphere.', event: 'Use a conference or event-poster style with keynote energy, glowing screens, stage-like lighting, and a premium event atmosphere.', tutorial: 'Use a clear structured instructional poster style with organized UI panels, workflow callouts, and helpful visual hierarchy.', platform_update: 'Use a modern platform-update style with system UI visuals, feature modules, and a polished ecosystem presentation.', archive: 'Use a retro-tech editorial style inspired by early 2000s computer magazines, with classic hardware, vintage UI influences, and modern polished lighting.', } const NEWS_PROMPT_KEYWORD_PATTERNS = [ { keywords: ['apple', 'wwdc', 'ios', 'macos', 'iphone', 'ipad', 'swift'], addon: 'Use a sleek developer-conference atmosphere with modern device screens, app ecosystem visuals, and a premium keynote mood.', }, { keywords: ['google', 'gemini', 'google i/o', 'android', 'pixel', 'tensorflow'], addon: 'Use a colorful futuristic creative AI studio style with glowing panels, image and video creation tools, search elements, and generative media visuals.', }, { keywords: ['intel', 'amd', 'processor', 'cpu', 'gpu', 'nvidia', 'radeon', 'chip'], addon: 'Use a retro computing hardware feature style with processor chips, technical callouts, old-school PC references, and magazine-cover energy.', }, { keywords: ['skin', 'theme', 'desktop', 'customize', 'customization', 'rainmeter', 'widget'], addon: 'Use a desktop customization promo style with theme previews, icon panels, widget windows, and a glossy desktop software aesthetic.', }, { keywords: ['ai', 'artificial intelligence', 'llm', 'chatgpt', 'openai', 'midjourney', 'stable diffusion', 'generative'], addon: 'Use a colorful futuristic creative AI studio style with glowing panels, generative media outputs, neural network visuals, and advanced AI tool interfaces.', }, ] function resolveNewsPromptHeadline(data) { return String(data.title || data.meta_title || '').trim() || 'Skinbase News' } function resolveNewsPromptSubheadline(data) { const raw = String(data.excerpt || data.meta_description || '').replace(/<[^>]*>/g, '').trim() if (raw) { const words = raw.split(/\s+/) return words.slice(0, 18).join(' ') + (words.length > 18 ? '…' : '') } const plain = String(data.content || '').replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim() if (plain) { const sentence = plain.split(/[.!?]/)[0].trim() if (sentence.length > 10) { const words = sentence.split(/\s+/) return words.slice(0, 18).join(' ') } } return 'Latest technology and creative industry update' } function resolveNewsPromptTopic(data) { const parts = [] const cat = String(data.category || '').trim() if (cat) parts.push(cat) const tagList = (Array.isArray(data.tag_names) ? data.tag_names : []).slice(0, 5).filter(Boolean) if (tagList.length) parts.push(tagList.join(', ')) if (!parts.length) { const words = String(data.title || '').split(/\s+/).filter((w) => w.length > 3).slice(0, 4) if (words.length) parts.push(words.join(' ')) } return parts.join(' · ') || 'Technology and digital culture news' } function resolveNewsPromptType(data) { const raw = String(data.type || '').trim() if (!raw) return 'News' return raw.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) } function resolveNewsPromptMood(data) { const type = String(data.type || '').toLowerCase().replace(/\s+/g, '_') return NEWS_PROMPT_TYPE_MOODS[type] || 'Modern Tech' } function resolveNewsPromptTypeAddon(data) { const type = String(data.type || '').toLowerCase().replace(/\s+/g, '_') return NEWS_PROMPT_TYPE_ADDONS[type] || '' } function resolveNewsPromptKeywordAddon(data) { const title = String(data.title || '').toLowerCase() const tags = (Array.isArray(data.tag_names) ? data.tag_names : []).join(' ').toLowerCase() const category = String(data.category || '').toLowerCase() const combined = `${title} ${tags} ${category}` const addons = [] for (const pattern of NEWS_PROMPT_KEYWORD_PATTERNS) { if (pattern.keywords.some((kw) => combined.includes(kw))) { addons.push(pattern.addon) } } return [...new Set(addons)].join('\n') } function buildNewsImagePrompt(data) { const headline = resolveNewsPromptHeadline(data) const subheadline = resolveNewsPromptSubheadline(data) const topic = resolveNewsPromptTopic(data) const newsType = resolveNewsPromptType(data) const mood = resolveNewsPromptMood(data) const typeAddon = resolveNewsPromptTypeAddon(data) const keywordAddon = resolveNewsPromptKeywordAddon(data) const lines = [ 'Create a premium Skinbase news cover image in 16:9 aspect ratio.', '', `Headline: "${headline}"`, `Subheadline: "${subheadline}"`, `Topic: ${topic}`, `News type: ${newsType}`, `Mood: ${mood}`, ] if (typeAddon) lines.push('', typeAddon) if (keywordAddon) lines.push('', keywordAddon) return lines.join('\n') } // ───────────────────────────────────────────────────────────────────────────── describe('resolveNewsPromptHeadline', () => { it('returns title when present', () => { expect(resolveNewsPromptHeadline({ title: 'My Title' })).toBe('My Title') }) it('falls back to meta_title', () => { expect(resolveNewsPromptHeadline({ title: '', meta_title: 'Meta Title' })).toBe('Meta Title') }) it('returns fallback when both are empty', () => { expect(resolveNewsPromptHeadline({})).toBe('Skinbase News') }) }) describe('resolveNewsPromptSubheadline', () => { it('returns excerpt truncated to 18 words', () => { const long = 'word '.repeat(25).trim() const result = resolveNewsPromptSubheadline({ excerpt: long }) expect(result.endsWith('…')).toBe(true) expect(result.split(/\s+/).filter((w) => !w.startsWith('…') && w !== '…').length).toBeLessThanOrEqual(18) }) it('returns excerpt as-is when short', () => { expect(resolveNewsPromptSubheadline({ excerpt: 'Short excerpt.' })).toBe('Short excerpt.') }) it('strips HTML tags from excerpt', () => { expect(resolveNewsPromptSubheadline({ excerpt: '
Clean text
' })).toBe('Clean text') }) it('returns fallback when all fields are empty', () => { expect(resolveNewsPromptSubheadline({})).toBe('Latest technology and creative industry update') }) it('falls back to meta_description when excerpt is missing', () => { expect(resolveNewsPromptSubheadline({ meta_description: 'Meta desc' })).toBe('Meta desc') }) }) describe('resolveNewsPromptTopic', () => { it('includes category and tags', () => { const result = resolveNewsPromptTopic({ category: 'Tech News', tag_names: ['AI', 'Google'] }) expect(result).toContain('Tech News') expect(result).toContain('AI') expect(result).toContain('Google') }) it('falls back to title keywords when no category or tags', () => { const result = resolveNewsPromptTopic({ title: 'Some Long Title With Words' }) expect(result.length).toBeGreaterThan(0) }) it('returns generic fallback when all are empty', () => { expect(resolveNewsPromptTopic({})).toBe('Technology and digital culture news') }) it('caps tags at 5', () => { const data = { tag_names: ['a', 'b', 'c', 'd', 'e', 'f', 'g'] } const result = resolveNewsPromptTopic(data) expect(result.split(',').length).toBeLessThanOrEqual(5) }) }) describe('resolveNewsPromptType', () => { it('capitalizes and humanizes underscored types', () => { expect(resolveNewsPromptType({ type: 'platform_update' })).toBe('Platform Update') }) it('returns "News" when type is missing', () => { expect(resolveNewsPromptType({})).toBe('News') }) it('handles simple types correctly', () => { expect(resolveNewsPromptType({ type: 'editorial' })).toBe('Editorial') expect(resolveNewsPromptType({ type: 'release' })).toBe('Release') }) }) describe('resolveNewsPromptMood', () => { it('returns correct mood for known types', () => { expect(resolveNewsPromptMood({ type: 'archive' })).toBe('Retro Tech') expect(resolveNewsPromptMood({ type: 'tutorial' })).toBe('Clean Instructional') expect(resolveNewsPromptMood({ type: 'announcement' })).toBe('Futuristic') expect(resolveNewsPromptMood({ type: 'release' })).toBe('Software Release') }) it('returns Modern Tech fallback for unknown type', () => { expect(resolveNewsPromptMood({ type: 'unknown_type' })).toBe('Modern Tech') expect(resolveNewsPromptMood({})).toBe('Modern Tech') }) }) describe('resolveNewsPromptTypeAddon', () => { it('returns addon text for known types', () => { expect(resolveNewsPromptTypeAddon({ type: 'release' })).toContain('software-release poster') expect(resolveNewsPromptTypeAddon({ type: 'event' })).toContain('conference or event-poster') expect(resolveNewsPromptTypeAddon({ type: 'archive' })).toContain('retro-tech editorial') }) it('returns empty string for unknown type', () => { expect(resolveNewsPromptTypeAddon({ type: 'unknown' })).toBe('') expect(resolveNewsPromptTypeAddon({})).toBe('') }) }) describe('resolveNewsPromptKeywordAddon', () => { it('detects apple/WWDC keyword in title', () => { const result = resolveNewsPromptKeywordAddon({ title: 'Apple WWDC 2026 Keynote', tag_names: [] }) expect(result).toContain('developer-conference atmosphere') }) it('detects AI keyword in tags', () => { const result = resolveNewsPromptKeywordAddon({ title: 'New Update', tag_names: ['AI', 'Gemini'] }) expect(result).toContain('creative AI studio') }) it('detects desktop customization keyword', () => { const result = resolveNewsPromptKeywordAddon({ title: 'Best desktop customization tools', tag_names: [] }) expect(result).toContain('desktop customization promo') }) it('returns empty string when no keywords match', () => { const result = resolveNewsPromptKeywordAddon({ title: 'Random Article', tag_names: ['general'] }) expect(result).toBe('') }) it('deduplicates addons when multiple patterns match the same text', () => { // "AI" matches both ai pattern; ensure no duplicates const result = resolveNewsPromptKeywordAddon({ title: 'ai ai ai', tag_names: ['ai', 'artificial intelligence'] }) const lines = result.split('\n').filter(Boolean) expect(lines.length).toBe(new Set(lines).size) }) }) describe('buildNewsImagePrompt', () => { it('includes the article headline in the prompt', () => { const prompt = buildNewsImagePrompt({ title: 'Google I/O 2026 Keynote', type: 'announcement' }) expect(prompt).toContain('Google I/O 2026 Keynote') }) it('includes the type addon for release articles', () => { const prompt = buildNewsImagePrompt({ title: 'App Launch', type: 'release' }) expect(prompt).toContain('software-release poster') }) it('includes the keyword addon when tags match', () => { const prompt = buildNewsImagePrompt({ title: 'New skin', tag_names: ['desktop', 'theme'], type: 'release' }) expect(prompt).toContain('desktop customization promo') }) it('uses fallback headline when title is missing', () => { const prompt = buildNewsImagePrompt({}) expect(prompt).toContain('Skinbase News') }) it('contains the 16:9 aspect ratio instruction', () => { const prompt = buildNewsImagePrompt({ title: 'Test' }) expect(prompt).toContain('16:9') }) it('contains the mood line', () => { const prompt = buildNewsImagePrompt({ title: 'Tutorial Article', type: 'tutorial' }) expect(prompt).toContain('Clean Instructional') }) })