import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { flushPromises } from '@vue/test-utils' import { initializeLocale } from '@/i18n' import type { LocalizedSeoContext } from '@/lib/localizedSeo' import type { ResolvedSeo } from '@/lib/seoTypes' import router from '@/router' import SeoLanguageCards from '../SeoLanguageCards.vue' import { byText, mountInApp, text, unmountApp } from './directoryKit' const PATTERN = 'https://journal.example.com/{lang}/{slug}' const CANONICAL = 'https://journal.example.com/fr/comment-nous-chiffrons-une-renovation' const ALTERNATES = [ { id: 10, lang: 'en', story_id: 5, slug: 'how-we-price-a-bathroom-renovation', url: 'https://journal.example.com/en/how-we-price-a-bathroom-renovation', }, { id: 11, lang: 'de', story_id: 5, slug: 'so-kalkulieren-wir-eine-badsanierung', url: null }, ] function resolved(locale = 'fr_FR'): ResolvedSeo { return { title: 'Couts de renovation | Journal', description: '', canonical_url: CANONICAL, robots: 'index, follow', og: { type: 'article', title: '', description: '', image: '', image_alt: '', url: '', site_name: 'Journal', locale, }, twitter: { card: 'summary', title: '', description: '', image: '', image_alt: '' }, image: { url: '', alt: '', source: 'none' }, authors: [], published_at: null, modified_at: '2026-09-26T11:02:00Z', fallbacks: { title: 'title', description: 'none', image: 'none', canonical: 'pattern' }, json_ld: {}, alternates: ALTERNATES, } as ResolvedSeo } function context(overrides: Partial = {}): LocalizedSeoContext { return { lang: 'fr', siteName: 'Journal', siteKey: 'journal', pattern: PATTERN, defaultLang: 'en', disabledLangs: ['es'], published: true, socialState: 'derived', resolved: resolved(), ...overrides, } } function card(id: string) { return document.body.querySelector(`[data-testid="${id}"]`) } beforeEach(() => { localStorage.clear() initializeLocale() }) afterEach(() => { unmountApp() vi.restoreAllMocks() }) describe('Search and sharing cards for one language', () => { it('shows the canonical with its source note', async () => { await mountInApp(SeoLanguageCards, { context: context() }) const canonical = text(card('seo-canonical-card')) expect(canonical).toContain('Canonical') expect(canonical).toContain(CANONICAL) expect(canonical).toContain(`Built from the Public URL pattern ${PATTERN}.`) }) it('lists the published alternates with their URL, or their slug without one, and the disabled note', async () => { await mountInApp(SeoLanguageCards, { context: context() }) const rows = [...document.body.querySelectorAll('[data-testid="seo-alternate-row"]')] expect(rows.map((row) => text(row))).toEqual([ 'en https://journal.example.com/en/how-we-price-a-bathroom-renovation Published', 'de so-kalkulieren-wir-eine-badsanierung Published', ]) expect(rows[0]!.querySelector('[aria-label]')!.getAttribute('aria-label')).toBe('English') const alternates = text(card('seo-alternates-card')) expect(alternates).toContain('Alternate languages') expect(alternates).toContain('Published versions in enabled languages only.') expect(alternates).toContain('Drafts and the disabled Spanish language are left out.') }) it('shows the derived social locale', async () => { await mountInApp(SeoLanguageCards, { context: context() }) const social = text(card('seo-social-card')) expect(social).toContain('Social locale') expect(social).toContain('og:locale') expect(social).toContain('Derived: fr_FR') }) it('shows Social locale not set when nothing resolves', async () => { await mountInApp(SeoLanguageCards, { context: context({ resolved: resolved('') }) }) expect(text(card('seo-social-card'))).toContain('Social locale not set') }) it('replaces the canonical and alternates with Public URL not configured and opens its settings', async () => { const push = vi.spyOn(router, 'push').mockResolvedValue(undefined) await mountInApp(SeoLanguageCards, { context: context({ pattern: null }) }) expect(card('seo-canonical-card')).toBeNull() expect(card('seo-alternates-card')).toBeNull() expect(text(document.body)).toContain('Public URL not configured') expect(text(document.body)).toContain( 'Add a public URL pattern for Journal to show page and canonical addresses. API links by article ID still work.', ) expect(card('seo-social-card')).not.toBeNull() byText('button', 'Open Public URL settings')!.click() await flushPromises() expect(push).toHaveBeenCalledWith('/settings/sites/journal/public-url') }) })