iconclip-demo / tests.html
NullSense's picture
Publish frontend-only IconClip search demo (transformers.js + parquet vectors)
d465270 verified
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>IconClip Static Demo — unit tests</title>
<style>
body { font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
padding: 24px; max-width: 880px; margin: 0 auto; line-height: 1.5; }
h1 { font-size: 18px; margin: 0 0 12px; }
.pass { color: #1a7f37; }
.fail { color: #c01717; font-weight: 600; }
.summary { padding: 10px 14px; border-radius: 8px; margin: 14px 0;
background: #f3f6fa; border: 1px solid #d6dae3; }
ol { padding-left: 22px; }
li { margin: 4px 0; }
code { background: #f3f6fa; padding: 1px 4px; border-radius: 4px; }
</style>
</head>
<body>
<h1>IconClip static-demo unit tests</h1>
<div id="summary" class="summary">Running…</div>
<ol id="log"></ol>
<script type="module">
// Note: imports are relative — works because both files ship together.
import { l2NormaliseInPlace, dot, scoreAll, topK } from './js/cosine.js';
import { buildMask, parseId } from './js/filters.js';
const log = document.getElementById('log');
const summary = document.getElementById('summary');
let pass = 0, fail = 0;
function t(name, fn) {
const li = document.createElement('li');
try {
fn();
li.innerHTML = `<span class="pass">PASS</span> — ${name}`;
pass++;
} catch (e) {
li.innerHTML = `<span class="fail">FAIL</span> — ${name}: <code>${escape(e.message)}</code>`;
fail++;
console.error(name, e);
}
log.appendChild(li);
}
function eq(a, b, msg) {
if (Math.abs(a - b) > 1e-5) throw new Error((msg || 'expected equality') + ` (got ${a}, expected ${b})`);
}
function ok(cond, msg) { if (!cond) throw new Error(msg || 'expected truthy'); }
function escape(s) { return String(s).replace(/[<>&]/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;'}[c])); }
// ----- cosine.js ----------------------------------------------------------
t('l2NormaliseInPlace makes the vector unit-length', () => {
const v = new Float32Array([3, 0, 4]);
l2NormaliseInPlace(v);
eq(v[0], 0.6); eq(v[1], 0); eq(v[2], 0.8);
// norm == 1
let s = 0; for (const x of v) s += x * x;
eq(Math.sqrt(s), 1);
});
t('l2NormaliseInPlace tolerates the zero vector', () => {
const v = new Float32Array([0, 0, 0]);
l2NormaliseInPlace(v);
eq(v[0], 0); eq(v[1], 0); eq(v[2], 0);
});
t('dot product matches naive implementation (length 4)', () => {
const a = new Float32Array([1, 2, 3, 4]);
const b = new Float32Array([4, 3, 2, 1]);
eq(dot(a, b), 20);
});
t('dot product handles non-multiple-of-4 lengths (length 7)', () => {
const a = new Float32Array([1, 1, 1, 1, 1, 1, 1]);
const b = new Float32Array([1, 2, 3, 4, 5, 6, 7]);
eq(dot(a, b), 28);
});
t('scoreAll without mask scores every row', () => {
// 3 rows × 2 dims, row-major
const M = new Float32Array([1, 0, 0, 1, 1, 1]);
const q = new Float32Array([1, 0]);
const s = scoreAll(M, q, 2, null);
eq(s[0], 1); eq(s[1], 0); eq(s[2], 1);
});
t('scoreAll with mask drops filtered rows to -Infinity', () => {
const M = new Float32Array([1, 0, 0, 1, 1, 1]);
const q = new Float32Array([1, 0]);
const mask = new Uint8Array([1, 0, 1]);
const s = scoreAll(M, q, 2, mask);
eq(s[0], 1);
ok(!Number.isFinite(s[1]) && s[1] < 0, 'masked row should be -Infinity');
eq(s[2], 1);
});
t('topK returns K highest scores in descending order', () => {
const scores = new Float32Array([0.1, 0.9, 0.5, 0.7, 0.2]);
const top = topK(scores, 3);
ok(top.length === 3, `expected 3 results, got ${top.length}`);
eq(top[0].score, 0.9); ok(top[0].index === 1);
eq(top[1].score, 0.7); ok(top[1].index === 3);
eq(top[2].score, 0.5); ok(top[2].index === 2);
});
t('topK skips -Infinity (masked) entries', () => {
const scores = new Float32Array([0.9, -Infinity, 0.5, -Infinity, 0.2]);
const top = topK(scores, 5);
ok(top.length === 3, `expected 3 finite results, got ${top.length}`);
eq(top[0].score, 0.9); eq(top[1].score, 0.5); eq(top[2].score, 0.2);
});
t('topK with K larger than N returns all sorted', () => {
const scores = new Float32Array([0.3, 0.1, 0.2]);
const top = topK(scores, 10);
ok(top.length === 3);
eq(top[0].score, 0.3); eq(top[1].score, 0.2); eq(top[2].score, 0.1);
});
t('topK handles ties stably enough (no crash, all returned)', () => {
const scores = new Float32Array([0.5, 0.5, 0.5, 0.5, 0.5]);
const top = topK(scores, 3);
ok(top.length === 3);
for (const r of top) eq(r.score, 0.5);
});
t('topK with K=0 returns empty', () => {
const top = topK(new Float32Array([1, 2, 3]), 0);
ok(top.length === 0);
});
// ----- filters.js ---------------------------------------------------------
const ALL = ['lucide', 'phosphor', 'tabler'];
const ids = ['lucide:bell', 'phosphor:bell', 'tabler:bell', 'lucide:user'];
t('buildMask returns null when every library is enabled', () => {
const m = buildMask(ids, new Set(ALL), ALL);
ok(m === null);
});
t('buildMask returns all-zero when nothing enabled', () => {
const m = buildMask(ids, new Set(), ALL);
ok(m instanceof Uint8Array && m.length === 4);
for (let i = 0; i < 4; i++) ok(m[i] === 0);
});
t('buildMask keeps exactly the enabled libraries', () => {
const m = buildMask(ids, new Set(['lucide']), ALL);
ok(m[0] === 1); ok(m[1] === 0); ok(m[2] === 0); ok(m[3] === 1);
});
t('buildMask handles ids with no colon by dropping them', () => {
const m = buildMask(['no-colon', 'lucide:bell'], new Set(['lucide']), ['lucide']);
// 'no-colon' has no slug → drop; 'lucide:bell' → keep.
// (Only one library "enabled" matches all-known, so we should NOT
// get the null short-circuit since the id-without-colon should still
// be dropped.)
ok(m === null || (m[0] === 0 && m[1] === 1),
'no-colon id should be excluded');
});
t('parseId splits slug:name', () => {
const p = parseId('lucide:bell');
ok(p.slug === 'lucide' && p.name === 'bell');
});
t('parseId returns empty slug for no-colon input', () => {
const p = parseId('bare');
ok(p.slug === '' && p.name === 'bare');
});
t('parseId handles names containing colons', () => {
const p = parseId('material-symbols:emoji:smile');
ok(p.slug === 'material-symbols' && p.name === 'emoji:smile');
});
// ----- summary -----------------------------------------------------------
summary.className = 'summary ' + (fail === 0 ? 'pass' : 'fail');
summary.textContent = fail === 0
? `All ${pass} tests passed.`
: `${fail} failed, ${pass} passed.`;
</script>
</body>
</html>