// @vitest-environment nuxt import { describe, it, expect, beforeEach } from 'vitest'; import { mockNuxtImport } from '@nuxt/test-utils/runtime'; import { useSeoLinks } from '../use-seo-links'; import { useSeoOverridesState } from '../use-seo-overrides'; import { useEnabledLanguageCodes } from '../use-enabled-locales'; let mockRoute = { path: '/videos', query: {} as Record }; mockNuxtImport('useRoute', () => () => mockRoute); const mockI18n = { locale: { value: 'en' }, locales: { value: ['en', 'fr', 'de', 'ja', 'zh'] }, }; const mockLocalePath = (path: string, locale: string) => { const bare = path.replace(/^\/(?:it|fr|es|ja|de|ko|ru|zh)(?=\/|$)/, ''); return locale === 'en' ? bare : `/${locale}${bare}`; }; describe('useSeoLinks', () => { beforeEach(() => { mockRoute = { path: '/videos', query: {} }; mockI18n.locale.value = 'en'; useSeoOverridesState().value = null; useEnabledLanguageCodes().value = []; }); it('canonical URL', () => { const { canonicalUrl } = useSeoLinks('https://example.com', mockI18n, mockLocalePath); expect(canonicalUrl.value).toBe('https://example.com/videos'); }); it('canonical с query params', () => { mockRoute = { path: '/videos', query: { page: '2', sort: 'popular' } }; const { canonicalUrl } = useSeoLinks('https://example.com', mockI18n, mockLocalePath); expect(canonicalUrl.value).toContain('page=2'); expect(canonicalUrl.value).toContain('sort=popular'); }); it('исключает sort при alonePage', () => { mockRoute = { path: '/videos', query: { page: '2', sort: 'popular' } }; useSeoOverridesState().value = { path: '/videos', alonePage: true }; const { canonicalUrl } = useSeoLinks('https://example.com', mockI18n, mockLocalePath); expect(canonicalUrl.value).toContain('page=2'); expect(canonicalUrl.value).not.toContain('sort'); }); it('alonePage с чужого пути не применяется', () => { mockRoute = { path: '/videos', query: { page: '2', sort: 'popular' } }; useSeoOverridesState().value = { path: '/models', alonePage: true }; const { canonicalUrl } = useSeoLinks('https://example.com', mockI18n, mockLocalePath); expect(canonicalUrl.value).toContain('sort=popular'); }); it('page=1 не попадает в canonical', () => { mockRoute = { path: '/videos', query: { page: '1', sort: 'popular' } }; const { canonicalUrl } = useSeoLinks('https://example.com', mockI18n, mockLocalePath); expect(canonicalUrl.value).toBe('https://example.com/videos?sort=popular'); }); it('page > 1 в canonical остаётся', () => { mockRoute = { path: '/videos', query: { page: '2' } }; const { canonicalUrl } = useSeoLinks('https://example.com', mockI18n, mockLocalePath); expect(canonicalUrl.value).toContain('page=2'); }); it('noindex убирает hreflang-альтернативы', () => { mockRoute = { path: '/search/anal/videos', query: {} }; useSeoOverridesState().value = { path: '/search/anal/videos', noindex: true }; const { canonicalUrl, alternateLinks } = useSeoLinks('https://example.com', mockI18n, mockLocalePath); expect(alternateLinks.value).toEqual([]); expect(canonicalUrl.value).toBe('https://example.com/search/anal/videos'); }); it('alternate links для всех локалей', () => { const { alternateLinks } = useSeoLinks('https://example.com', mockI18n, mockLocalePath); const hreflangs = alternateLinks.value.map(l => l.hreflang); expect(hreflangs).toContain('en'); expect(hreflangs).toContain('fr'); expect(hreflangs).toContain('de'); expect(hreflangs).toContain('ja-JP'); expect(hreflangs).toContain('zh-CN'); expect(hreflangs).toContain('x-default'); }); it('alternate links только для включённых на сайте локалей', () => { useEnabledLanguageCodes().value = ['en', 'fr']; const { alternateLinks } = useSeoLinks('https://example.com', mockI18n, mockLocalePath); const hreflangs = alternateLinks.value.map(l => l.hreflang); expect(hreflangs).toContain('en'); expect(hreflangs).toContain('fr'); expect(hreflangs).toContain('x-default'); expect(hreflangs).not.toContain('de'); expect(hreflangs).not.toContain('ja-JP'); expect(hreflangs).not.toContain('zh-CN'); }); it('текущая локаль остаётся в alternate, даже если её нет в списке включённых', () => { mockRoute = { path: '/ja/videos', query: {} }; mockI18n.locale.value = 'ja'; useEnabledLanguageCodes().value = ['en', 'fr']; const { alternateLinks } = useSeoLinks('https://example.com', mockI18n, mockLocalePath); expect(alternateLinks.value.map(l => l.hreflang)).toContain('ja-JP'); }); it('ja маппится в ja-JP', () => { const { alternateLinks } = useSeoLinks('https://example.com', mockI18n, mockLocalePath); const jaLink = alternateLinks.value.find(l => l.hreflang === 'ja-JP'); expect(jaLink).toBeDefined(); expect(jaLink!.href).toContain('/ja/videos'); }); it('x-default указывает на en', () => { const { alternateLinks } = useSeoLinks('https://example.com', mockI18n, mockLocalePath); const xDefault = alternateLinks.value.find(l => l.hreflang === 'x-default'); expect(xDefault!.href).toBe('https://example.com/videos'); }); it('canonicalLocale переводит canonical на указанную локаль', () => { mockRoute = { path: '/ja/videos/some-video', query: {} }; mockI18n.locale.value = 'ja'; useSeoOverridesState().value = { path: '/ja/videos/some-video', canonicalLocale: 'en' }; const { canonicalUrl } = useSeoLinks('https://example.com', mockI18n, mockLocalePath); expect(canonicalUrl.value).toBe('https://example.com/videos/some-video'); }); it('canonicalLocale сохраняет допустимые query params', () => { mockRoute = { path: '/ja/videos/some-video', query: { page: '2', foo: 'bar' } }; mockI18n.locale.value = 'ja'; useSeoOverridesState().value = { path: '/ja/videos/some-video', canonicalLocale: 'en' }; const { canonicalUrl } = useSeoLinks('https://example.com', mockI18n, mockLocalePath); expect(canonicalUrl.value).toContain('/videos/some-video'); expect(canonicalUrl.value).not.toContain('/ja/'); expect(canonicalUrl.value).toContain('page=2'); expect(canonicalUrl.value).not.toContain('foo'); }); it('canonicalLocale убирает hreflang-альтернативы', () => { mockRoute = { path: '/ja/videos/some-video', query: {} }; mockI18n.locale.value = 'ja'; useSeoOverridesState().value = { path: '/ja/videos/some-video', canonicalLocale: 'en' }; const { alternateLinks } = useSeoLinks('https://example.com', mockI18n, mockLocalePath); expect(alternateLinks.value).toEqual([]); }); it('canonicalLocale игнорируется для чужого пути', () => { mockRoute = { path: '/ja/videos/other-video', query: {} }; mockI18n.locale.value = 'ja'; useSeoOverridesState().value = { path: '/ja/videos/some-video', canonicalLocale: 'en' }; const { canonicalUrl } = useSeoLinks('https://example.com', mockI18n, mockLocalePath); expect(canonicalUrl.value).toBe('https://example.com/ja/videos/other-video'); }); it('canonicalLocale игнорируется, если совпадает с текущей локалью', () => { mockRoute = { path: '/videos/some-video', query: {} }; useSeoOverridesState().value = { path: '/videos/some-video', canonicalLocale: 'en' }; const { canonicalUrl } = useSeoLinks('https://example.com', mockI18n, mockLocalePath); expect(canonicalUrl.value).toBe('https://example.com/videos/some-video'); }); it('без override canonical остаётся на текущей локали', () => { mockRoute = { path: '/ja/videos/some-video', query: {} }; mockI18n.locale.value = 'ja'; const { canonicalUrl } = useSeoLinks('https://example.com', mockI18n, mockLocalePath); expect(canonicalUrl.value).toBe('https://example.com/ja/videos/some-video'); }); it('игнорирует неразрешённые query params', () => { mockRoute = { path: '/videos', query: { page: '2', foo: 'bar', categories: 'teen' } }; const { canonicalUrl } = useSeoLinks('https://example.com', mockI18n, mockLocalePath); expect(canonicalUrl.value).toContain('page=2'); expect(canonicalUrl.value).toContain('categories=teen'); expect(canonicalUrl.value).not.toContain('foo'); }); });