import fs from 'node:fs/promises'; import type { AsyncExpectationResult } from '@vitest/expect'; import { HtmlValidate } from 'html-validate/node'; import type { Result } from 'html-validate/node'; import marked from 'marked'; import { describe, expect, test } from 'vitest'; import { extractMetadata } from '#shared/blog/metadata.ts'; import parseMarkdown from '#shared/parseMarkdown.ts'; import { Translator } from '#shared/translator.ts'; import type { Config } from '~~/locale/config.ts'; import allLocales from '~~/locale/locales.ts'; import type { Translations } from '~~/locale/translations.ts'; import { loadSuml } from '~~/server/loader.ts'; const validator = new HtmlValidate({ extends: [ 'html-validate:recommended', ], rules: { 'attr-case': 'off', 'element-required-attributes': 'off', 'no-deprecated-attr': 'off', 'no-inline-style': 'off', 'prefer-tbody': 'off', 'wcag/h30': 'off', 'wcag/h63': 'off', // these originate from the current markdown parser 'valid-id': 'off', 'no-trailing-whitespace': 'off', }, }); async function toBeValidHTML(actual: string): AsyncExpectationResult { const validatorReport = await validator.validateString(actual); const results: Result[] = validatorReport.results; const messages = results .flatMap((result) => result.messages) .map((message) => `${message.ruleId} at ${message.line}:${message.column} (${message.selector})\n\t${message.message}`); if (messages.length > 0) { return { message: () => `expected to be valid HTML\n\n${messages.join('\n')}`, pass: false, }; } else { return { message: () => 'expected to be invalid HTML', pass: true, }; } } interface CustomMatchers { toBeValidHTML(): Promise; } declare module 'vitest' { interface Assertion extends CustomMatchers {} } expect.extend({ toBeValidHTML }); const baseTranslations = await loadSuml('locale/_base/translations.suml'); describe.each(allLocales)('blog in $code', async ({ code }) => { const config = await loadSuml(`locale/${code}/config.suml`); const translations = await loadSuml(`locale/${code}/translations.suml`); const translator = new Translator(translations, baseTranslations, config); const blogDirectory = `locale/${code}/blog`; const outputDirectory = `test/output/locale/${code}/blog`; const slugs = (await fs.readdir(blogDirectory)) .filter((file) => file.endsWith('.md')) .map((file) => file.replace(/\.md$/, '')); test('contains posts when enabled', () => { if (config.links.blog) { expect(slugs).not.toHaveLength(0); } }); describe.each(slugs)('post %s', (slug) => { test('has valid metadata', async () => { const content = await fs.readFile(`${blogDirectory}/${slug}.md`, 'utf-8'); const metadata = extractMetadata(config, content); expect(metadata?.title).toBeTruthy(); expect(metadata?.authors.length).toBeGreaterThan(0); expect(metadata?.date).toMatch(/\d\d\d\d-\d\d-\d\d/); }); test('is valid HTML', async () => { const content = await fs.readFile(`${blogDirectory}/${slug}.md`, 'utf-8'); const parsed = marked(content); const blogEntry = await parseMarkdown(parsed, translator); await fs.mkdir(outputDirectory, { recursive: true }); await fs.writeFile(`${outputDirectory}/${slug}.html`, blogEntry.content ?? ''); await expect(blogEntry.content).toBeValidHTML(); }); }); });