Files
bookhoard/vite.config.ts
T
John O'Keefe e5315f1b94 test(web): vitest suites for markdown, filters, password rules, api, storage, toast
Test infrastructure uses the vite-plus stack: vp test (bundled Vitest 5),
jsdom environment configured in vite.config.ts, colocated web/src/*.test.ts
files that tsc type-checks, vp fmt formats, and the bundler never imports.

- markdown.test.ts pins the notes XSS defenses: parser-level html escaping,
  DOMPurify FORBID_TAGS, scheme checks, footnote rendering, html->markdown
  round-trips incl. the headingless-table rule.
- filter_builder.test.ts pins the deep-link wire contract (ruleToFlatParam),
  smart-serialization rules (isFlatable), and pruneIncomplete semantics.
- password_validation.test.ts covers the rules fetch + fallback and the
  full submit-gating matrix through the real DOM flow (debounce included).
- api.test.ts pins response/error contracts incl. the HTTP-status and
  Unknown-error fallbacks and the 404-as-null progress contract.
- storage.test.ts covers token/theme/library round-trips, the __all__
  sentinel, and the SSR cookie.
- toast.test.ts pins config completeness for every ToastType (the missing
  'warning' entry regression) and render behavior.

Found + fixed while writing these: the no-useless-escape fix in
d7d14957 had de-escaped the ']' in hasSpecialChar's character class,
closing it early — special-character validation silently stopped
matching most specials. The new password suite caught it immediately.

jsdom chosen over happy-dom after empirical comparison: DOMPurify's
sanitizer output is mangled under happy-dom (forbidden tags survive),
which would have made the security suite test nothing.

Note: AGENTS.md gains a 'Web (TypeScript) tests' section documenting the
workflow and the happy-dom gotcha.
2026-10-21 15:34:29 -04:00

77 lines
2.2 KiB
TypeScript

import { defineConfig } from "vite-plus";
import { cpSync, mkdirSync, readdirSync, rmSync } from "node:fs";
import { join } from "node:path";
const pdfjsAssets = () => ({
name: "pdfjs-assets",
closeBundle() {
const outDir = "web/static/vendor/pdfjs";
const srcDir = "node_modules/@bookhoard/foliate-js/vendor/pdfjs";
mkdirSync(outDir, { recursive: true });
cpSync(join(srcDir, "standard_fonts"), join(outDir, "standard_fonts"), {
recursive: true,
});
cpSync(join(srcDir, "cmaps"), join(outDir, "cmaps"), { recursive: true });
},
});
// emptyOutDir must stay false (web/static also holds tracked assets),
// so hashed chunks from previous builds would otherwise accumulate
// forever and leak into Docker images via the build context. Remove
// any *-<hash>.js/.css/.mjs(.map) that this build did not produce.
const cleanStaleChunks = () => {
const produced = new Set<string>();
return {
name: "clean-stale-chunks",
generateBundle(_options, bundle) {
for (const fileName of Object.keys(bundle)) produced.add(fileName);
},
closeBundle() {
const outDir = "web/static";
const chunkRe = /-[A-Za-z0-9_-]{8}\.(js|css|mjs)(\.map)?$/;
for (const f of readdirSync(outDir)) {
if (chunkRe.test(f) && !produced.has(f)) {
rmSync(join(outDir, f));
}
}
},
};
};
export default defineConfig({
// web/static holds vendored and build-output assets (htmx.min.js,
// compiled css) that must never be reformatted; only web/src is fmt scope.
fmt: {
ignorePatterns: ["web/static/**"],
},
test: {
environment: "jsdom",
include: ["web/src/**/*.test.ts"],
},
resolve: {
alias: {
"@": "/web/src",
"foliate-js": "/node_modules/@bookhoard/foliate-js",
},
},
base: "/static/",
plugins: [pdfjsAssets(), cleanStaleChunks()],
build: {
outDir: "web/static",
emptyOutDir: false,
assetsDir: "",
rollupOptions: {
input: {
main: "web/src/main.ts",
reader: "web/src/reader/reader.ts",
},
output: {
entryFileNames: "[name].js",
},
},
target: "esnext",
sourcemap: true,
minify: true,
},
});