Files
vue-seo-layer/tests/guard.test.ts
Jakub Zych b4680af0e3 feat: the build fails when an indexable route renders only in the browser (tracer)
- lib/guard.ts: pure checks (isNoindex with Nitro route-rule patterns,
  findOffenders over the app's ssr flag, every merged route rule,
  .client.vue pages and experimental.inlineRouteRules)
- module options seo.noindexRoutes and seo.indexable; declared noindex
  routes get X-Robots-Tag: noindex, nofollow through their route rule
- a production build fails with the D-70 message; nuxt dev and
  nuxt prepare only warn
- testing/guard-check.mjs builds four fixture apps (guard:check)
2026-11-04 11:42:08 +01:00

98 lines
5.0 KiB
TypeScript

import assert from 'node:assert/strict'
import test from 'node:test'
import { GUARD_MESSAGE, findOffenders, flattenPages, guardMessage, isNoindex, noindexRule, pagePattern, pagePatterns, patternCovers } from '../lib/guard.ts'
const base = { ssr: true, indexable: true, routeRules: {}, pages: [], noindexRoutes: [], inlineRouteRules: false }
test('isNoindex: Nitro route-rule patterns', () => {
assert.equal(isNoindex('/x', ['/x/**']), true)
assert.equal(isNoindex('/x/a/b', ['/x/**']), true)
assert.equal(isNoindex('/xy', ['/x/**']), false)
assert.equal(isNoindex('/x/a', ['/x/*']), true)
assert.equal(isNoindex('/x/a/b', ['/x/*']), false)
assert.equal(isNoindex('/x', ['/x/*']), false)
assert.equal(isNoindex('/konto', ['/konto']), true)
assert.equal(isNoindex('/konto/dane', ['/konto']), false)
assert.equal(isNoindex('/anything', ['/**']), true)
assert.equal(isNoindex('/x/a', []), false)
assert.equal(isNoindex('/x/a', ['']), false)
})
test('patternCovers: a pattern against another pattern', () => {
assert.equal(patternCovers('/p/**', '/p/**'), true)
assert.equal(patternCovers('/p/**', '/p/:id'), true)
assert.equal(patternCovers('/p/*', '/p/:id'), true)
assert.equal(patternCovers('/p/*', '/p/**'), false, 'one segment does not cover a catch-all')
assert.equal(patternCovers('/p/:id', '/p/edit'), true)
assert.equal(patternCovers('/p/edit', '/p/:id'), false, 'a static segment does not cover a parameter')
assert.equal(patternCovers('/**', '/**'), true)
})
test('pagePattern: Nuxt page paths become route-rule patterns', () => {
assert.equal(pagePattern('/posts/:slug()'), '/posts/:slug')
assert.equal(pagePattern('/:slug(.*)*'), '/**')
assert.equal(pagePattern('/'), '/')
assert.equal(pagePattern('/panel'), '/panel')
assert.deepEqual(pagePatterns('/a/:id?'), ['/a/:id', '/a'])
assert.deepEqual(pagePatterns('/a/:id()'), ['/a/:id'])
})
test('flattenPages: children get their full path', () => {
const flat = flattenPages([{ path: '/konto', children: [{ path: '', mode: 'client' }, { path: 'dane' }] }])
assert.deepEqual(
flat.map((p) => p.fullPath),
['/konto', '/konto', '/konto/dane'],
)
})
test('findOffenders: nothing to report on a server-rendered app', () => {
assert.deepEqual(findOffenders({ ...base, routeRules: { '/**': { swr: 300 } }, pages: [{ path: '/' }, { path: '/blog/:slug()' }] }), [])
})
test('findOffenders: a route rule with ssr false', () => {
assert.deepEqual(findOffenders({ ...base, routeRules: { '/x/**': { ssr: false }, '/y': { ssr: true } } }), ['/x/**'])
})
test('findOffenders: a declared noindex route passes', () => {
assert.deepEqual(findOffenders({ ...base, routeRules: { '/x/**': { ssr: false } }, noindexRoutes: ['/x/**'] }), [])
assert.deepEqual(findOffenders({ ...base, routeRules: { '/x/a': { ssr: false } }, noindexRoutes: ['/x/**'] }), [])
assert.deepEqual(findOffenders({ ...base, routeRules: { '/x/**': { ssr: false } }, noindexRoutes: ['/x/*'] }), ['/x/**'], 'a one-segment declaration does not cover a catch-all rule')
})
test('findOffenders: a .client.vue page, nested or not', () => {
assert.deepEqual(findOffenders({ ...base, pages: [{ path: '/panel', mode: 'client' }] }), ['/panel (.client.vue page)'])
assert.deepEqual(findOffenders({ ...base, pages: [{ path: '/konto', children: [{ path: 'dane', mode: 'client' }] }] }), ['/konto/dane (.client.vue page)'])
assert.deepEqual(findOffenders({ ...base, pages: [{ path: '/panel', mode: 'client' }], noindexRoutes: ['/panel'] }), [])
assert.deepEqual(findOffenders({ ...base, pages: [{ path: '/p/:id()', mode: 'client' }], noindexRoutes: ['/p/**'] }), [])
})
test('findOffenders: global ssr false, reported once', () => {
assert.deepEqual(findOffenders({ ...base, ssr: false, routeRules: { '/**': { ssr: false } } }), ['ssr: false (the whole app)'])
assert.deepEqual(findOffenders({ ...base, ssr: false, noindexRoutes: ['/**'] }), [])
})
test('findOffenders: inline route rules are refused', () => {
const out = findOffenders({ ...base, inlineRouteRules: true })
assert.equal(out.length, 1)
assert.match(out[0]!, /experimental\.inlineRouteRules/)
})
test('findOffenders: seo.indexable false checks nothing', () => {
assert.deepEqual(findOffenders({ ...base, indexable: false, ssr: false, routeRules: { '/x/**': { ssr: false } }, inlineRouteRules: true }), [])
})
test('guardMessage: the D-70 message lists the offenders', () => {
const msg = guardMessage(['/x/**', '/panel (.client.vue page)'])
assert.ok(msg.startsWith(`${GUARD_MESSAGE}: /x/**, /panel (.client.vue page). `))
assert.ok(msg.includes('seo.noindexRoutes'))
})
test('noindexRule: adds X-Robots-Tag and keeps the rest of the rule and an own header', () => {
assert.deepEqual(noindexRule(undefined), { headers: { 'x-robots-tag': 'noindex, nofollow' } })
assert.deepEqual(noindexRule({ ssr: false, headers: { 'cache-control': 'no-store' } }), {
ssr: false,
headers: { 'cache-control': 'no-store', 'x-robots-tag': 'noindex, nofollow' },
})
assert.deepEqual(noindexRule({ headers: { 'X-Robots-Tag': 'none' } }), { headers: { 'X-Robots-Tag': 'none' } })
})