All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 53s
117 lines
5.5 KiB
JavaScript
117 lines
5.5 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import zlib from 'node:zlib';
|
|
import { readFile } from 'node:fs/promises';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { dirname, join } from 'node:path';
|
|
import Fastify from 'fastify';
|
|
import fastifyCompress from '@fastify/compress';
|
|
import { registerFrontend, resolveAppVersion } from '../src/frontend.js';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const publicRoot = join(__dirname, '..', 'public');
|
|
|
|
// Zelfde volgorde als src/server.js: compress vóór de frontend-registratie.
|
|
async function makeApp(version = '1.2.3-test') {
|
|
const app = Fastify();
|
|
await app.register(fastifyCompress, {
|
|
global: true,
|
|
encodings: ['br', 'gzip'],
|
|
threshold: 1024,
|
|
brotliOptions: { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 4 } },
|
|
zlibOptions: { level: 6 },
|
|
});
|
|
await registerFrontend(app, { root: publicRoot, version });
|
|
await app.ready();
|
|
return app;
|
|
}
|
|
|
|
test('index.html wordt geserveerd met versie-injectie en no-cache', async () => {
|
|
const app = await makeApp();
|
|
const res = await app.inject({ url: '/' });
|
|
assert.equal(res.statusCode, 200);
|
|
assert.match(res.headers['content-type'], /text\/html/);
|
|
assert.equal(res.headers['cache-control'], 'no-cache');
|
|
assert.ok(res.payload.includes('?v=1.2.3-test'), 'versie hoort geïnjecteerd te zijn');
|
|
assert.ok(!res.payload.includes('__V__'), 'geen placeholder meer in de uitvoer');
|
|
const alias = await app.inject({ url: '/index.html' });
|
|
assert.equal(alias.headers['cache-control'], 'no-cache');
|
|
await app.close();
|
|
});
|
|
|
|
test('js en css zijn een jaar immutable; fonts en img korter zonder immutable', async () => {
|
|
const app = await makeApp();
|
|
for (const url of ['/js/core.js', '/css/teach.css']) {
|
|
const res = await app.inject({ url });
|
|
assert.equal(res.statusCode, 200, url);
|
|
assert.equal(res.headers['cache-control'], 'public, max-age=31536000, immutable', url);
|
|
}
|
|
const font = await app.inject({ url: '/fonts/quicksand.woff2' });
|
|
assert.equal(font.headers['cache-control'], 'public, max-age=604800');
|
|
assert.ok(font.headers.etag, 'etag als 304-vangnet voor ongeversioneerde assets');
|
|
const img = await app.inject({ url: '/img/logo.svg' });
|
|
assert.equal(img.headers['cache-control'], 'public, max-age=86400');
|
|
await app.close();
|
|
});
|
|
|
|
test('assets gaan gecomprimeerd over de lijn (gzip én brotli)', async () => {
|
|
const app = await makeApp();
|
|
const gz = await app.inject({ url: '/css/teach.css', headers: { 'accept-encoding': 'gzip' } });
|
|
assert.equal(gz.headers['content-encoding'], 'gzip');
|
|
assert.match(gz.headers.vary ?? '', /accept-encoding/i);
|
|
const cssRaw = zlib.gunzipSync(gz.rawPayload).toString('utf8');
|
|
assert.ok(cssRaw.includes('toolbar'), 'gzip-inhoud is geldige css');
|
|
const br = await app.inject({ url: '/js/core.js', headers: { 'accept-encoding': 'br' } });
|
|
assert.equal(br.headers['content-encoding'], 'br');
|
|
const jsRaw = zlib.brotliDecompressSync(br.rawPayload).toString('utf8');
|
|
assert.ok(jsRaw.includes('const VERSION'), 'brotli-inhoud is geldige js');
|
|
assert.ok(br.rawPayload.length < jsRaw.length / 2, 'compressie levert echt winst op');
|
|
await app.close();
|
|
});
|
|
|
|
test('conditional GET levert 304 zonder body', async () => {
|
|
const app = await makeApp();
|
|
const first = await app.inject({ url: '/fonts/quicksand.woff2' });
|
|
const res = await app.inject({ url: '/fonts/quicksand.woff2', headers: { 'if-none-match': first.headers.etag } });
|
|
assert.equal(res.statusCode, 304);
|
|
assert.equal(res.rawPayload.length, 0);
|
|
await app.close();
|
|
});
|
|
|
|
test('resolveAppVersion geeft de echte versie door en valt lokaal nooit stale terug', () => {
|
|
assert.equal(resolveAppVersion({ APP_VERSION: '0.4.0-beta' }), '0.4.0-beta');
|
|
const a = resolveAppVersion({});
|
|
const b = resolveAppVersion({ APP_VERSION: 'dev' });
|
|
assert.match(a, /^dev-\d+$/);
|
|
assert.match(b, /^dev-\d+$/);
|
|
});
|
|
|
|
test('index.html: elk script heeft defer + cache-busting, font-preload blijft ongeversioneerd', async () => {
|
|
const [html, server] = await Promise.all([
|
|
readFile(join(publicRoot, 'index.html'), 'utf8'),
|
|
readFile(join(__dirname, '..', 'src', 'server.js'), 'utf8'),
|
|
]);
|
|
const scripts = [...html.matchAll(/<script [^>]*src="([^"]+)"[^>]*>/g)];
|
|
assert.ok(scripts.length >= 30, 'alle app-scripts staan in index.html');
|
|
for (const [tag, src] of scripts) {
|
|
assert.match(src, /\?v=__V__$/, `script zonder cache-busting: ${src}`);
|
|
assert.match(tag, / defer/, `script zonder defer: ${src}`);
|
|
}
|
|
assert.match(html, /href="css\/teach\.css\?v=__V__"/);
|
|
/* de preload-URL moet byte-gelijk blijven aan de url() in teach.css,
|
|
anders downloadt de browser het font dubbel */
|
|
assert.match(html, /rel="preload" href="fonts\/quicksand\.woff2" /);
|
|
/* hook-volgorde: compress moet vóór de api- en frontend-registraties staan */
|
|
assert.ok(server.indexOf('fastifyCompress') < server.indexOf('register(api'),
|
|
'compress hoort vóór de api-registratie');
|
|
assert.ok(server.indexOf('fastifyCompress') < server.indexOf('registerFrontend(app'),
|
|
'compress hoort vóór de frontend-registratie');
|
|
});
|
|
|
|
test('mobiele leerlingweergave volgt de zichtbare viewport zonder witte onderstrook', async () => {
|
|
const css = await readFile(join(publicRoot, 'css', 'teach.css'), 'utf8');
|
|
assert.match(css, /html,body\{height:100%; height:100dvh;/);
|
|
assert.match(css, /body\{\s*position:relative;/);
|
|
const phone = css.slice(css.indexOf('Widgetinhoud op telefoonformaat'));
|
|
assert.match(phone, /body\.pupil-mode #pupilView\{[^}]*safe-area-inset-bottom[^}]*overscroll-behavior:contain;/);
|
|
});
|