Add Makefile, package.json, and initial configuration for VitePress and Slidev integration
- Introduced a Makefile for streamlined commands related to building, running, and managing presentations. - Created package.json to define project metadata, dependencies, and scripts for development and testing. - Added .gitignore entries to exclude build artifacts and temporary files from version control. - Included playwright.config.mjs for end-to-end testing setup with Playwright. - Added README.md for project documentation, outlining setup instructions and development guidelines.
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Après npm run build : dist-portal + dist-<slug>/ à la racine.
|
||||
*/
|
||||
import { test, describe } from 'node:test'
|
||||
import assert from 'node:assert'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const repoRoot = path.resolve(__dirname, '..')
|
||||
const portal = path.join(repoRoot, 'dist-portal')
|
||||
const hasPortal = fs.existsSync(portal)
|
||||
|
||||
describe('Artefacts build (dist-portal + dist-<slug>)', () => {
|
||||
test('portail + chaque deck', { skip: !hasPortal }, () => {
|
||||
assert.ok(fs.existsSync(path.join(portal, 'index.html')))
|
||||
assert.ok(fs.existsSync(path.join(portal, 'presentations', 'index.html')))
|
||||
|
||||
const data = JSON.parse(
|
||||
fs.readFileSync(path.join(repoRoot, 'portal', '.vitepress', 'data', 'presentations.json'), 'utf8'),
|
||||
)
|
||||
for (const p of data) {
|
||||
const slideRoot = path.join(repoRoot, `dist-${p.slug}`)
|
||||
const slideIndex = path.join(slideRoot, 'index.html')
|
||||
assert.ok(fs.existsSync(slideIndex), `deck manquant: dist-${p.slug}/index.html`)
|
||||
const presIndex = path.join(portal, 'presentations', p.slug, 'index.html')
|
||||
assert.ok(fs.existsSync(presIndex), `fiche manquante: dist-portal/presentations/${p.slug}/index.html`)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,146 @@
|
||||
import { test, describe } from 'node:test'
|
||||
import assert from 'node:assert'
|
||||
import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import {
|
||||
escHtml,
|
||||
escMd,
|
||||
pickFeatured,
|
||||
buildFeaturedHtml,
|
||||
readAllMeta,
|
||||
generateListPageMarkdown,
|
||||
generateDetailPageMarkdown,
|
||||
patchIndexFeaturedContent,
|
||||
FEATURED_START,
|
||||
FEATURED_END,
|
||||
} from '../tools/presentation-contracts.mjs'
|
||||
|
||||
function mockSitePath(p) {
|
||||
return p.startsWith('/') ? p : `/${p}`
|
||||
}
|
||||
|
||||
describe('escHtml / escMd', () => {
|
||||
test('escHtml neutralise < > & "', () => {
|
||||
assert.strictEqual(escHtml('<a href="x">'), '<a href="x">')
|
||||
})
|
||||
|
||||
test('escMd échappe les pipes tableaux markdown', () => {
|
||||
assert.strictEqual(escMd('a|b'), 'a\\|b')
|
||||
})
|
||||
})
|
||||
|
||||
describe('pickFeatured', () => {
|
||||
test('priorité aux entrées featured puis order', () => {
|
||||
const items = [
|
||||
{ slug: 'c', title: 'C', order: 3, featured: false },
|
||||
{ slug: 'a', title: 'A', order: 1, featured: true },
|
||||
{ slug: 'b', title: 'B', order: 2, featured: true },
|
||||
]
|
||||
const got = pickFeatured(items)
|
||||
assert.deepStrictEqual(
|
||||
got.map((x) => x.slug),
|
||||
['a', 'b'],
|
||||
)
|
||||
})
|
||||
|
||||
test('sans featured : trois premiers par order', () => {
|
||||
const items = [
|
||||
{ slug: 'z', title: 'Z', order: 10 },
|
||||
{ slug: 'a', title: 'A', order: 1 },
|
||||
{ slug: 'b', title: 'B', order: 2 },
|
||||
{ slug: 'c', title: 'C', order: 3 },
|
||||
]
|
||||
const got = pickFeatured(items)
|
||||
assert.deepStrictEqual(
|
||||
got.map((x) => x.slug),
|
||||
['a', 'b', 'c'],
|
||||
)
|
||||
})
|
||||
|
||||
test('liste vide', () => {
|
||||
assert.deepStrictEqual(pickFeatured([]), [])
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildFeaturedHtml', () => {
|
||||
test('génère des liens et échappe le titre', () => {
|
||||
const html = buildFeaturedHtml(
|
||||
[{ slug: 'x', title: '<evil>', description: 'd & co', order: 1 }],
|
||||
mockSitePath,
|
||||
)
|
||||
assert.match(html, /href="\/presentations\/x\/"/)
|
||||
assert.match(html, /<evil>/)
|
||||
assert.match(html, /d & co/)
|
||||
})
|
||||
|
||||
test('aucune présentation : commentaire', () => {
|
||||
assert.strictEqual(buildFeaturedHtml([], mockSitePath), '<!-- (aucune présentation) -->')
|
||||
})
|
||||
})
|
||||
|
||||
describe('readAllMeta (répertoire temporaire)', () => {
|
||||
test('ignore dossier sans meta.json', () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'talks-slides-'))
|
||||
fs.mkdirSync(path.join(dir, 'empty'), { recursive: true })
|
||||
const okDir = path.join(dir, 'ok')
|
||||
fs.mkdirSync(okDir, { recursive: true })
|
||||
fs.writeFileSync(path.join(okDir, 'meta.json'), JSON.stringify({ title: 'T', description: '', order: 1 }))
|
||||
const list = readAllMeta(dir)
|
||||
assert.strictEqual(list.length, 1)
|
||||
assert.strictEqual(list[0].slug, 'ok')
|
||||
assert.strictEqual(list[0].title, 'T')
|
||||
})
|
||||
})
|
||||
|
||||
describe('generateListPageMarkdown / generateDetailPageMarkdown', () => {
|
||||
const p = {
|
||||
slug: 'demo',
|
||||
title: 'Démo',
|
||||
description: 'Desc',
|
||||
tags: ['a', 'b'],
|
||||
order: 1,
|
||||
}
|
||||
|
||||
test('liste contient fiche + slides + tags', () => {
|
||||
const md = generateListPageMarkdown([p], mockSitePath)
|
||||
assert.match(md, /href="\/presentations\/demo\/"/)
|
||||
assert.match(md, /href="\/slides\/demo\/1"/)
|
||||
assert.match(md, /vp-raw/)
|
||||
assert.match(md, /`a` · `b`/)
|
||||
})
|
||||
|
||||
test('détail contient QrEncode et boutons', () => {
|
||||
const md = generateDetailPageMarkdown(p, mockSitePath)
|
||||
assert.match(md, /<QrEncode path="\/slides\/demo\/1" \/>/)
|
||||
assert.match(md, /class="vp-button vp-raw"/)
|
||||
assert.match(md, /Voir les slides/)
|
||||
assert.match(md, /## Partager les slides/)
|
||||
})
|
||||
|
||||
test('détail sans tags : pas de ligne Tags', () => {
|
||||
const md = generateDetailPageMarkdown({ ...p, tags: [] }, mockSitePath)
|
||||
assert.ok(!md.includes('**Tags :**'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('patchIndexFeaturedContent', () => {
|
||||
test('remplace la zone entre marqueurs', () => {
|
||||
const raw = `head\n${FEATURED_START}\nold\n${FEATURED_END}\ntail`
|
||||
const { ok, content } = patchIndexFeaturedContent(
|
||||
raw,
|
||||
[{ slug: 's', title: 'T', description: 'D', order: 1 }],
|
||||
mockSitePath,
|
||||
)
|
||||
assert.strictEqual(ok, true)
|
||||
assert.ok(content.includes('talk-card'))
|
||||
assert.ok(content.includes('head'))
|
||||
assert.ok(content.includes('tail'))
|
||||
assert.ok(!content.includes('old'))
|
||||
})
|
||||
|
||||
test('marqueurs absents : ok false', () => {
|
||||
const { ok } = patchIndexFeaturedContent('nope', [], mockSitePath)
|
||||
assert.strictEqual(ok, false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Invariant du dépôt : chaque slides/<slug>/meta.json a une entrée et des fichiers générés.
|
||||
* Exécuter après `npm run sync` (la CI lance sync avant les tests).
|
||||
*/
|
||||
import { test, describe, before } from 'node:test'
|
||||
import assert from 'node:assert'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const repoRoot = path.resolve(__dirname, '..')
|
||||
|
||||
before(() => {
|
||||
const r = spawnSync(process.execPath, [path.join(repoRoot, 'tools', 'sync-portal.mjs')], {
|
||||
cwd: repoRoot,
|
||||
encoding: 'utf8',
|
||||
})
|
||||
assert.strictEqual(r.status, 0, r.stderr || r.stdout)
|
||||
})
|
||||
|
||||
describe('Cohérence slides ↔ portail (après sync)', () => {
|
||||
test('presentations.json existe et liste les dossiers slides', () => {
|
||||
const jsonPath = path.join(repoRoot, 'portal', '.vitepress', 'data', 'presentations.json')
|
||||
assert.ok(fs.existsSync(jsonPath), 'presentations.json manquant')
|
||||
const data = JSON.parse(fs.readFileSync(jsonPath, 'utf8'))
|
||||
assert.ok(Array.isArray(data))
|
||||
assert.ok(data.length >= 1, 'au moins une présentation attendue dans ce dépôt')
|
||||
|
||||
const slideDirs = fs
|
||||
.readdirSync(path.join(repoRoot, 'slides'), { withFileTypes: true })
|
||||
.filter((d) => d.isDirectory() && !d.name.startsWith('_'))
|
||||
.map((d) => d.name)
|
||||
.filter((name) => fs.existsSync(path.join(repoRoot, 'slides', name, 'meta.json')))
|
||||
|
||||
assert.strictEqual(
|
||||
data.length,
|
||||
slideDirs.length,
|
||||
'nombre d’entrées JSON = nombre de dossiers slides avec meta.json',
|
||||
)
|
||||
|
||||
const slugs = new Set(data.map((p) => p.slug))
|
||||
for (const dir of slideDirs) {
|
||||
assert.ok(slugs.has(dir), `slug manquant pour le dossier slides/${dir}`)
|
||||
}
|
||||
})
|
||||
|
||||
test('chaque entrée a title, description, slug', () => {
|
||||
const jsonPath = path.join(repoRoot, 'portal', '.vitepress', 'data', 'presentations.json')
|
||||
const data = JSON.parse(fs.readFileSync(jsonPath, 'utf8'))
|
||||
for (const p of data) {
|
||||
assert.ok(typeof p.title === 'string' && p.title.length > 0, `title requis: ${p.slug}`)
|
||||
assert.ok(typeof p.description === 'string', `description requise: ${p.slug}`)
|
||||
assert.ok(typeof p.slug === 'string' && p.slug.length > 0)
|
||||
}
|
||||
})
|
||||
|
||||
test('chaque deck a un global-bottom.vue (pied de page portail)', () => {
|
||||
const data = JSON.parse(
|
||||
fs.readFileSync(path.join(repoRoot, 'portal', '.vitepress', 'data', 'presentations.json'), 'utf8'),
|
||||
)
|
||||
for (const p of data) {
|
||||
const gb = path.join(repoRoot, 'slides', p.slug, 'global-bottom.vue')
|
||||
assert.ok(fs.existsSync(gb), `slides/${p.slug}/global-bottom.vue manquant (pied de page)`)
|
||||
const raw = fs.readFileSync(gb, 'utf8')
|
||||
assert.ok(
|
||||
raw.includes('_shared/DeckFooter'),
|
||||
`global-bottom doit importer le pied de page partagé: ${p.slug}`,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
test('fiche et liste générées pour chaque slug', () => {
|
||||
const data = JSON.parse(
|
||||
fs.readFileSync(path.join(repoRoot, 'portal', '.vitepress', 'data', 'presentations.json'), 'utf8'),
|
||||
)
|
||||
const listMd = path.join(repoRoot, 'portal', 'presentations', 'index.md')
|
||||
const listContent = fs.readFileSync(listMd, 'utf8')
|
||||
|
||||
for (const p of data) {
|
||||
const detail = path.join(repoRoot, 'portal', 'presentations', p.slug, 'index.md')
|
||||
assert.ok(fs.existsSync(detail), `fiche manquante: presentations/${p.slug}/index.md`)
|
||||
const body = fs.readFileSync(detail, 'utf8')
|
||||
assert.match(body, new RegExp(`slides/${p.slug}/`, ''), 'lien deck dans la fiche')
|
||||
assert.match(body, /<QrEncode path="/, 'composant QR présent')
|
||||
assert.ok(listContent.includes(p.title), 'titre présent dans la liste')
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
import { test, describe, beforeEach } from 'node:test'
|
||||
import assert from 'node:assert'
|
||||
import { getSiteBase, sitePath, vitepressAppBase } from '../tools/site-base.mjs'
|
||||
|
||||
describe('site-base (SITE_BASE / chemins statiques)', () => {
|
||||
beforeEach(() => {
|
||||
delete process.env.SITE_BASE
|
||||
delete process.env.TALKS_PORTAL_DEV
|
||||
})
|
||||
|
||||
test('défaut : même comportement que la racine du site', () => {
|
||||
assert.strictEqual(getSiteBase(), '')
|
||||
assert.strictEqual(sitePath('/slides/foo/'), '/slides/foo/')
|
||||
assert.strictEqual(sitePath('/presentations/bar/'), '/presentations/bar/')
|
||||
})
|
||||
|
||||
test('sous-chemin sans slash final', () => {
|
||||
process.env.SITE_BASE = '/mon-depot'
|
||||
assert.strictEqual(getSiteBase(), '/mon-depot')
|
||||
assert.strictEqual(sitePath('/slides/foo/'), '/mon-depot/slides/foo/')
|
||||
})
|
||||
|
||||
test('sous-chemin avec slash final', () => {
|
||||
process.env.SITE_BASE = '/repo/'
|
||||
assert.strictEqual(getSiteBase(), '/repo')
|
||||
assert.strictEqual(sitePath('/slides/x/'), '/repo/slides/x/')
|
||||
})
|
||||
|
||||
test('chemins relatifs normalisés', () => {
|
||||
delete process.env.SITE_BASE
|
||||
assert.strictEqual(sitePath('slides/x/'), '/slides/x/')
|
||||
})
|
||||
|
||||
test('SITE_BASE vide explicite : traité comme /', () => {
|
||||
process.env.SITE_BASE = ''
|
||||
assert.strictEqual(getSiteBase(), '')
|
||||
})
|
||||
|
||||
test('TALKS_PORTAL_DEV=1 : ignore SITE_BASE (dev local)', () => {
|
||||
process.env.TALKS_PORTAL_DEV = '1'
|
||||
process.env.SITE_BASE = '/pages-projet/'
|
||||
assert.strictEqual(getSiteBase(), '')
|
||||
assert.strictEqual(sitePath('/slides/a/'), '/slides/a/')
|
||||
assert.strictEqual(vitepressAppBase(), '/')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Intégration : sync-portal sur un dépôt minimal (TALKS_ROOT) sans toucher au vrai portail.
|
||||
*/
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert'
|
||||
import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const repoRoot = path.resolve(__dirname, '..')
|
||||
|
||||
test('sync sur dépôt temporaire (TALKS_ROOT)', () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'talks-tmp-'))
|
||||
|
||||
fs.mkdirSync(path.join(tmp, 'portal'), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(tmp, 'portal', 'index.md'),
|
||||
['---', 'layout: doc', '---', '', '<!-- FEATURED_START -->', '<!-- FEATURED_END -->', ''].join('\n'),
|
||||
)
|
||||
|
||||
const hello = path.join(tmp, 'slides', 'hello-world')
|
||||
fs.mkdirSync(hello, { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(hello, 'meta.json'),
|
||||
JSON.stringify({
|
||||
title: 'Hello World',
|
||||
description: 'Test tmp',
|
||||
order: 1,
|
||||
featured: true,
|
||||
}),
|
||||
)
|
||||
fs.writeFileSync(path.join(hello, 'slides.md'), '---\n---\n\n# Slide\n')
|
||||
|
||||
const r = spawnSync(process.execPath, [path.join(repoRoot, 'tools', 'sync-portal.mjs')], {
|
||||
cwd: repoRoot,
|
||||
env: { ...process.env, TALKS_ROOT: tmp },
|
||||
encoding: 'utf8',
|
||||
})
|
||||
assert.strictEqual(r.status, 0, r.stderr || r.stdout)
|
||||
|
||||
const detail = path.join(tmp, 'portal', 'presentations', 'hello-world', 'index.md')
|
||||
assert.ok(fs.existsSync(detail), 'fiche générée')
|
||||
const body = fs.readFileSync(detail, 'utf8')
|
||||
assert.ok(body.includes('Hello World'))
|
||||
assert.ok(body.includes('<QrEncode path="/slides/hello-world/1"'))
|
||||
|
||||
const list = path.join(tmp, 'portal', 'presentations', 'index.md')
|
||||
assert.ok(fs.readFileSync(list, 'utf8').includes('Hello World'))
|
||||
|
||||
const featured = fs.readFileSync(path.join(tmp, 'portal', 'index.md'), 'utf8')
|
||||
assert.ok(featured.includes('talk-card'))
|
||||
})
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Tests du plugin slide-dists en dev VitePress (`/slides/<slug>/` → dist-<slug>/).
|
||||
*
|
||||
* Régression couverte (soumission initiale) :
|
||||
* En dev, des requêtes du type `GET /slides/<slug>/1.md?import&…` recevaient la page HTML
|
||||
* du portail VitePress (Content-Type: text/html) au lieu d’être ignorées ou traitées
|
||||
* comme des modules JS. Le navigateur affichait alors :
|
||||
* « Expected a JavaScript-or-Wasm module script but the server responded with a MIME type of text/html »
|
||||
* (middleware sirv trop large + repli SPA VitePress sur du HTML pour chemins inconnus.)
|
||||
*
|
||||
* Contrat : pour ces URLs « module Vite » sous /slides/, la réponse ne doit **jamais**
|
||||
* être du `text/html` (404 texte ou autre non-HTML est acceptable).
|
||||
*/
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import http from 'node:http'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { createServer } from 'vitepress'
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const deckIndex = path.join(root, 'dist-kubernetes-operators', 'index.html')
|
||||
if (!fs.existsSync(deckIndex)) {
|
||||
const e = { ...process.env, TALKS_PORTAL_DEV: '1' }
|
||||
delete e.SITE_BASE
|
||||
execFileSync(process.execPath, [path.join(root, 'tools', 'build-deck.mjs'), 'kubernetes-operators'], {
|
||||
cwd: root,
|
||||
stdio: 'inherit',
|
||||
env: e,
|
||||
})
|
||||
}
|
||||
|
||||
function get(url, headers = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const u = new URL(url)
|
||||
const opts = { hostname: u.hostname, port: u.port, path: u.pathname + u.search, headers }
|
||||
http
|
||||
.get(opts, (res) => {
|
||||
let body = ''
|
||||
res.on('data', (c) => {
|
||||
body += c
|
||||
})
|
||||
res.on('end', () => resolve({ status: res.statusCode, body, headers: res.headers }))
|
||||
})
|
||||
.on('error', reject)
|
||||
})
|
||||
}
|
||||
|
||||
/** Lance VitePress en mode dev local (TALKS_PORTAL_DEV), exécute le callback, puis ferme. */
|
||||
async function withDevPortal(callback) {
|
||||
const prevDev = process.env.TALKS_PORTAL_DEV
|
||||
const prevSite = process.env.SITE_BASE
|
||||
process.env.TALKS_PORTAL_DEV = '1'
|
||||
delete process.env.SITE_BASE
|
||||
const server = await createServer('portal', {
|
||||
server: { strictPort: true, host: true },
|
||||
})
|
||||
const port = 28740 + Math.floor(Math.random() * 200)
|
||||
await server.listen(port)
|
||||
try {
|
||||
await callback(port)
|
||||
} finally {
|
||||
await server.close()
|
||||
if (prevDev !== undefined) process.env.TALKS_PORTAL_DEV = prevDev
|
||||
else delete process.env.TALKS_PORTAL_DEV
|
||||
if (prevSite !== undefined) process.env.SITE_BASE = prevSite
|
||||
else delete process.env.SITE_BASE
|
||||
}
|
||||
}
|
||||
|
||||
test('VitePress dev : racine deck → 302 vers …/1 (première planche Slidev)', async () => {
|
||||
await withDevPortal(async (port) => {
|
||||
const base = `http://127.0.0.1:${port}`
|
||||
for (const path of ['/slides/kubernetes-operators/', '/slides/kubernetes-operators']) {
|
||||
const { status, headers } = await get(`${base}${path}`)
|
||||
assert.equal(status, 302, `${path} → attendu 302`)
|
||||
assert.equal(headers.location, '/slides/kubernetes-operators/1', `Location pour ${path}`)
|
||||
}
|
||||
const q = await get(`${base}/slides/kubernetes-operators/?embedded=1`)
|
||||
assert.equal(q.status, 302)
|
||||
assert.equal(q.headers.location, '/slides/kubernetes-operators/1?embedded=1')
|
||||
})
|
||||
})
|
||||
|
||||
test('VitePress dev : GET /slides/kubernetes-operators/1 → 200 (HTML Slidev)', async () => {
|
||||
await withDevPortal(async (port) => {
|
||||
const { status, body } = await get(`http://127.0.0.1:${port}/slides/kubernetes-operators/1`)
|
||||
assert.equal(status, 200, `attendu 200, reçu ${status}`)
|
||||
assert.match(body, /slidev|Slidev|#app/i, 'corps doit ressembler à une page Slidev')
|
||||
})
|
||||
})
|
||||
|
||||
test('Régression MIME module : *.md?import sous /slides/<slug>/ ne renvoie pas text/html (HAR)', async () => {
|
||||
const paths = [
|
||||
'/slides/kubernetes-operators/1.md?import&t=1775912428995',
|
||||
'/slides/kubernetes-operators/index.md?import',
|
||||
'/slides/kubernetes-operators/slides.md?import',
|
||||
]
|
||||
await withDevPortal(async (port) => {
|
||||
for (const p of paths) {
|
||||
const { status, headers, body } = await get(`http://127.0.0.1:${port}${p}`, {
|
||||
Accept: '*/*',
|
||||
'Sec-Fetch-Dest': 'script',
|
||||
'Sec-Fetch-Mode': 'cors',
|
||||
})
|
||||
const ct = String(headers['content-type'] || '')
|
||||
assert.ok(
|
||||
!ct.toLowerCase().includes('text/html'),
|
||||
`${p} : interdit text/html (reçu ${status}, Content-Type: ${ct || '(vide)'}) — régression MIME module`,
|
||||
)
|
||||
assert.ok(
|
||||
!/^<\s*!DOCTYPE\s+html/i.test(body.trim()),
|
||||
`${p} : le corps ne doit pas être un document HTML (ex. shell VitePress)`,
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test('VitePress dev : GET …/2 (route client Slidev) → 200 HTML deck', async () => {
|
||||
await withDevPortal(async (port) => {
|
||||
const { status, body } = await get(`http://127.0.0.1:${port}/slides/kubernetes-operators/2`, {
|
||||
Accept: 'text/html',
|
||||
})
|
||||
assert.equal(status, 200)
|
||||
assert.match(body, /slidev|Slidev|#app/i)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user