feat: Initial prototype

This commit is contained in:
abdus committed 2026-10-10 12:51:33 +02:00
commit 819f6a61cd
327 files changed
+252446

No files matched your search

+84
View File
@@ -0,0 +1,84 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {readFile, readdir} from 'node:fs/promises';
import {fonts} from '../font-catalog.js';
import {render, registerFont, renderLoaded, resolveFont, FONT_CACHE} from '../renderer.js';
test('catalog includes every bundled FIGlet font', async () => {
const files = await readdir(new URL('../fonts/figlet/', import.meta.url));
assert.deepEqual(fonts, files.filter(file => file.endsWith('.flf')).map(file => file.slice(0, -4)).sort());
});
for (const font of fonts) {
test(`renders ${font} from its local font file`, async () => {
registerFont(font, await readFile(new URL(`../fonts/figlet/${font}.flf`, import.meta.url), 'utf8'));
const art = renderLoaded('Hello World! 123', font);
assert.equal(typeof art, 'string');
assert.match(art, /\S/);
});
}
test('Standard preserves lowercase lettering and the familiar FIGlet shape', () => {
assert.equal(renderLoaded('Hi', 'Standard'), [
' _ _ _ ', ' | | | (_)', ' | |_| | |', ' | _ | |', ' |_| |_|_|',
].join('\n'));
});
test('old lowercase font links resolve to their library names', () => {
for (const [name, expected] of [['standard', 'Standard'], ['slant', 'Slant'], ['block', 'Block'], ['banner', 'Banner'], ['compact', 'compact'], ['unknown', 'Standard']]) {
assert.equal(resolveFont(name), expected);
}
});
test('missing font requests can be retried', async () => {
const originalFetch = globalThis.fetch;
const data = await readFile(new URL('../fonts/figlet/Standard.flf', import.meta.url), 'utf8');
const figlet = (await import('../vendor/figlet.js')).default;
delete figlet.figFonts.Standard;
try {
globalThis.fetch = async () => new Response('', {status: 404});
await assert.rejects(render('Hi', 'Standard'), /Cannot load font/);
globalThis.fetch = async () => new Response(data);
assert.match(await render('Hi', 'Standard'), /\S/);
} finally {
globalThis.fetch = originalFetch;
}
});
test('caches only selected fonts and reuses them when offline', async () => {
const originalFetch = globalThis.fetch;
const originalCaches = globalThis.caches;
const entries = new Map();
const figlet = (await import('../vendor/figlet.js')).default;
for (const font of ['Standard', 'Slant', 'Small']) delete figlet.figFonts[font];
try {
globalThis.caches = {
async open(name) {
assert.equal(name, FONT_CACHE);
return {
async match(url) { return entries.get(url)?.clone(); },
async put(url, response) { entries.set(url, response.clone()); },
};
},
};
globalThis.fetch = async url => new Response(await readFile(new URL(url), 'utf8'));
await render('Hello', 'Standard');
await render('Hello', 'Slant');
assert.equal(entries.size, 2);
assert.ok([...entries.keys()].every(url => /\/(Standard|Slant)\.flf$/.test(url)));
delete figlet.figFonts.Standard;
globalThis.fetch = async () => { throw new Error('Offline'); };
assert.match(await render('Offline', 'Standard'), /\S/);
await assert.rejects(render('Offline', 'Small'), /Offline/);
assert.equal(entries.size, 2);
} finally {
globalThis.fetch = originalFetch;
if (originalCaches === undefined) delete globalThis.caches;
else globalThis.caches = originalCaches;
}
});
test('rejects unknown fonts and overlong input', async () => {
await assert.rejects(render('Hello', 'unknown'), /Unknown/);
await assert.rejects(render('x'.repeat(501)), /too long/);
});
+47
View File
@@ -0,0 +1,47 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {normalizeText, render, styles} from '../bitmap.js';
test('compact lettering renders a known phrase with correct spacing', () => {
assert.equal(render('HI', 'compact'), [
'# # #####', '# # #', '# # #', '##### #',
'# # #', '# # #', '# # #####',
].join('\n'));
});
for (const style of styles) {
test(`${style} renders digits, punctuation and multiline input`, () => {
const first = render('Hello 123!', style);
assert.equal(first.split('\n').length, 7);
assert.match(first, /\S/);
assert.equal(render('Hello 123!\r\nWorld?', style), `${first}\n\n${render('World?', style)}`);
});
}
for (const [text, expected] of [
['çığöşü İ', 'CIGOSU I'],
['café', 'CAFE'],
['abc 123!?', 'ABC 123!?'],
]) {
test(`normalizes ${text}`, () => assert.equal(normalizeText(text), expected));
}
test('unsupported characters have a visible fallback', () => {
assert.equal(render('🙂'), render('?'));
});
test('all printable ASCII characters have distinct supported glyphs', () => {
for (let code = 32; code <= 126; code++) {
const char = String.fromCharCode(code);
if (char === '?') continue;
assert.notEqual(render(char), render('?'), `missing ${char}`);
}
});
test('banner uses the original normalized letter as ink', () => {
assert.match(render('a', 'banner'), /^ AAA\n/);
assert.doesNotMatch(render('a', 'banner'), /#/);
});
test('rejects invalid styles and overlong text', () => {
assert.throws(() => render('hello', 'unknown'), /Unknown/);
assert.throws(() => render('x'.repeat(501)), /too long/);
assert.doesNotThrow(() => render('x'.repeat(500)));
});
+150
View File
@@ -0,0 +1,150 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import vm from 'node:vm';
import {readFile, access} from 'node:fs/promises';
const source = await readFile(new URL('../sw.js', import.meta.url), 'utf8');
function worker({scope = 'https://ascii.example/pwa/', version = 'v1', stores = new Map(), missing = ''} = {}) {
const handlers = {};
let claimed = false;
let activated = false;
let fetched = 0;
const context = vm.createContext({
URL,
self: {
registration: {scope}, location: {origin: new URL(scope).origin},
addEventListener(name, callback) { handlers[name] = callback; },
clients: {async claim() { claimed = true; }},
skipWaiting() { activated = true; },
},
caches: {
async keys() { return [...stores.keys()]; },
async delete(name) { return stores.delete(name); },
async open(name) {
if (!stores.has(name)) stores.set(name, new Map());
const store = stores.get(name);
return {
async addAll(urls) {
if (urls.some(url => url.endsWith(missing)) && missing) throw Error('Missing asset');
for (const url of urls) {
const relative = new URL(url).pathname.slice(new URL(scope).pathname.length);
await access(new URL(`../${relative || 'index.html'}`, import.meta.url));
}
// Cache.addAll commits only after the whole batch succeeds.
for (const url of urls) store.set(url, {url});
},
async match(url) { return store.get(url); },
};
},
},
async fetch(request) { fetched++; throw Error(`Offline: ${request.url}`); },
});
vm.runInContext(source.replace(/import .*;\n/, '').replace(/const VERSION = 'v\d+'/, `const VERSION = '${version}'`), context);
return {
stores,
get claimed() { return claimed; },
get activated() { return activated; },
get fetched() { return fetched; },
async lifecycle(name) {
let result;
handlers[name]({waitUntil(promise) { result = promise; }});
await result;
},
message(data) { handlers.message({data}); },
async request(url, {method = 'GET', mode = 'navigate'} = {}) {
let result;
handlers.fetch({request: {url, method, mode}, respondWith(promise) { result = promise; }});
return result ? await result : undefined;
},
};
}
for (const scope of ['https://ascii.example/', 'https://ascii.example/pwa/']) {
test(`offline shell preserves supported navigation at ${scope}`, async () => {
const sw = worker({scope});
await sw.lifecycle('install');
await sw.lifecycle('activate');
for (const cache of sw.stores.values()) {
assert.equal([...cache.keys()].some(url => url.endsWith('.flf')), false);
}
assert.equal(sw.claimed, true);
assert.equal(sw.activated, false, 'initial install does not force activation');
assert.equal((await sw.request(`${scope}?text=Hello&font=compact`)).url, scope);
assert.equal((await sw.request(`${scope}index.html?text=World`)).url, scope);
for (const cache of sw.stores.values()) {
for (const url of cache.keys()) {
assert.ok(await sw.request(`${url}?v=1`, {mode: 'cors'}), `offline asset ${url}`);
}
}
assert.equal(sw.fetched, 0);
});
}
test('bypasses API routes, unknown navigation, cross-origin resources and writes', async () => {
const sw = worker();
for (const [url, options] of [
['https://ascii.example/api/render', {}],
['https://ascii.example/pwa/not-a-route', {}],
['https://other.example/pwa/app.js', {mode: 'cors'}],
['https://ascii.example/pwa/app.js', {method: 'POST', mode: 'cors'}],
['https://ascii.example/pwa/unrelated.js', {mode: 'cors'}],
]) assert.equal(await sw.request(url, options), undefined);
});
test('version upgrade prepares a complete cache, waits, then cleans only this app scope', async () => {
const stores = new Map([['unrelated-cache', new Map()]]);
const old = worker({stores});
await old.lifecycle('install');
// Create an unrelated installation cache without attempting to load its shell.
const otherKey = `ascii-pwa-${encodeURIComponent('https://ascii.example/other/')}-v1`;
stores.set(otherKey, new Map());
const fontKey = `ascii-pwa-fonts-${encodeURIComponent('https://ascii.example/pwa/')}-figlet-1.8.2`;
stores.set(fontKey, new Map([['Standard.flf', {used: true}]]));
const next = worker({version: 'v2', stores});
await next.lifecycle('install');
assert.equal(next.activated, false);
assert.ok([...stores.keys()].some(key => key.endsWith('-v1')));
assert.equal((await old.request('https://ascii.example/pwa/')).url, 'https://ascii.example/pwa/');
next.message('ACTIVATE_UPDATE');
assert.equal(next.activated, true);
await next.lifecycle('activate');
assert.ok(stores.has('unrelated-cache'));
assert.ok(stores.has(otherKey));
assert.equal(stores.get(fontKey).get('Standard.flf').used, true);
const key = `ascii-pwa-${encodeURIComponent('https://ascii.example/pwa/')}-v2`;
assert.ok(stores.has(key));
assert.equal(stores.has(key.replace(/v2$/, 'v1')), false);
assert.equal((await next.request('https://ascii.example/pwa/')).url, 'https://ascii.example/pwa/');
});
test('missing shell assets prevent installation without replacing the old shell', async () => {
const stores = new Map();
const old = worker({stores});
await old.lifecycle('install');
const next = worker({stores, version: 'v2', missing: 'renderer.js'});
await assert.rejects(next.lifecycle('install'), /Missing asset/);
assert.equal(next.claimed, false);
assert.equal(next.activated, false);
assert.equal((await old.request('https://ascii.example/pwa/')).url, 'https://ascii.example/pwa/');
});
test('manifest paths and PNG dimensions support root and subdirectory installs', async () => {
const manifest = JSON.parse(await readFile(new URL('../manifest.webmanifest', import.meta.url)));
for (const base of ['https://ascii.example/', 'https://ascii.example/pwa/']) {
for (const field of ['id', 'scope', 'start_url']) assert.equal(new URL(manifest[field], base).href, base);
}
assert.equal(manifest.display, 'standalone');
for (const icon of manifest.icons) {
const data = await readFile(new URL(`../${icon.src}`, import.meta.url));
if (icon.type === 'image/png') {
assert.equal(data.subarray(1, 4).toString(), 'PNG');
assert.equal(`${data.readUInt32BE(16)}x${data.readUInt32BE(20)}`, icon.sizes);
}
}
for (const purpose of ['any', 'maskable']) {
for (const size of ['192x192', '512x512']) {
assert.ok(manifest.icons.some(icon => icon.purpose === purpose && icon.sizes === size));
}
}
});