@nan0web/test.app
v1.0.0
Published
Universal Test Engine for nan0web platform
Readme
🇺🇦 Українська
@nan0web/test.app
Universal Test Engine for the nan0web platform.
Zero dependencies. One Web Component. Plug-and-play scoring, classification, and result rendering.
Features
- Fisher-Yates shuffle for questions and options
- A-Z obfuscation of option values (DOM-safe)
- Early exit detection (configurable top/bottom thresholds)
- 5 built-in scoring strategies — sum, product, average, percentage, weighted
- Session persistence — save/restore test progress
- Standard result renderers — text, scale, fromConfig (YAML-driven)
- Timer — optional countdown per question with auto-advance
- Config validation — clear error messages for malformed configs
- OLMUI architecture —
ui-cli(terminal) +ui-lit(web component) - Dark premium UI — glassmorphism, gradient animations, progress bar
- Accessible —
role="radiogroup",aria-label, keyboard nav,prefers-reduced-motion
Installation
How to install with pnpm?
pnpm add @nan0web/test.appHow to install with npm?
npm install @nan0web/test.appHow to install with yarn?
yarn add @nan0web/test.appHow to use subpath exports?
import { scoring } from '@nan0web/test.app/scoring'
import { validateConfig } from '@nan0web/test.app/schema'
import { runTestCli } from '@nan0web/test.app/ui-cli'
import '@nan0web/test.app/ui-lit'Quick Start (Web Component)
<test-app></test-app>
<script type="module">
import '@nan0web/test.app/ui-lit'
const el = document.querySelector('test-app')
el.config = {
id: 'my-quiz',
title: 'Quick Quiz',
questions: [
{
title: 'What is 2 + 2?',
options: [
{ text: '3', value: 0 },
{ text: '4', value: 1 },
],
},
],
}
el.addEventListener('test-complete', (e) => {
console.log('Score:', e.detail.score)
})
</script>How to use the Web Component?
Quick Start (CLI)
import { runTestCli } from '@nan0web/test.app/ui-cli'
const result = await runTestCli({
id: 'demo',
title: 'Demo Test',
questions: [
{
title: 'Your question?',
options: [
{ text: 'Answer A', value: 3 },
{ text: 'Answer B', value: 1 },
],
},
],
})
console.log(result) // { score, level, reason, n }How to use the CLI adapter?
Config Validation
How to validate a test config?
import { validateConfig } from '@nan0web/test.app/schema'
const good = validateConfig({
id: 'test-1',
title: 'My Test',
questions: [
{
title: 'Question?',
options: [
{ text: 'A', value: 0 },
{ text: 'B', value: 1 },
],
},
],
})
console.info(good.valid) // true
console.info(good.errors) // []How to detect invalid config?
import { validateConfig } from '@nan0web/test.app/schema'
const bad = validateConfig({ id: 'broken' })
console.info(bad.valid) // false
console.info(bad.errors.length) // 2Scoring Strategies
5 built-in strategies:
How to use scoring strategies?
import { scoring } from '@nan0web/test.app/scoring'
console.info(scoring.sum([1, 2, 3])) // 6
console.info(scoring.product([1, 2, 3])) // 6
console.info(scoring.average([1, 2, 3])) // 2Resolve from YAML config string:
How to resolve scoring from config name?
import { resolveScoring } from '@nan0web/test.app/scoring'
const fn = resolveScoring('product')
console.info(fn([3, 3, 3])) // 27Session Persistence
How to save and restore test progress?
import { saveProgress, loadProgress, clearProgress } from '@nan0web/test.app/session'
const store = new Map()
const storage = {
getItem: (k) => store.get(k) ?? null,
setItem: (k, v) => store.set(k, String(v)),
removeItem: (k) => store.delete(k),
}
saveProgress('readme-test', { index: 2, answers: [3, 1] }, storage)
const saved = loadProgress('readme-test', storage)
console.info(saved.index) // 2
console.info(saved.answers) // [3, 1]Engine: prepareSession & processAnswer
How to use the engine directly?
import { prepareSession, processAnswer, checkEarlyExit } from '@nan0web/test.app/engine'
const config = {
id: 't',
title: 'T',
encode: false,
shuffle_questions: false,
shuffle_options: false,
questions: [
{
title: 'Q1',
options: [
{ text: 'A', value: 1 },
{ text: 'B', value: 2 },
],
},
],
}
const session = prepareSession(config)
console.info(session.questions.length) // 1
const val = processAnswer(1, 0, null, false)
console.info(val) // 1
const exit = checkEarlyExit([1], { bottom: 1, bottomValue: 0 })
console.info(exit.canStop) // falsePluggable Hooks
const el = document.querySelector('test-app')
// Custom scoring (default: sum)
el.onScore = (answers) => answers.reduce((a, b) => a * b, 1)
// Custom classification
el.onClassify = (score, n) => {
if (score === Math.pow(3, n)) return 'razumny'
if (score === 0) return 'bezumny'
return 'rozumny'
}
// Custom result renderer (return HTML string or null for default)
el.onRenderResult = (result) => `<div>Score: ${result.score}</div>`Events
| Event | Detail | Description |
| --------------- | -------------------------------------- | --------------------------------------- |
| test-complete | { score, n, level, reason, answers } | Fired when test finishes |
| navigate | { level } | Fired when "Continue" button is clicked |
CSS Custom Properties
Override theme variables without forking the component:
test-app {
--t-bg: #0a0a0f;
--t-card: rgba(255, 255, 255, 0.04);
--t-accent: #a78bfa;
--t-accent-glow: rgba(167, 139, 250, 0.3);
--t-text: #e2e8f0;
--t-text-muted: #94a3b8;
--t-success: #34d399;
--t-danger: #f87171;
--t-font: 'Inter', system-ui, -apple-system, sans-serif;
--t-radius: 16px;
}Accessibility
role="radiogroup"for answer options witharia-labelledbyrole="progressbar"witharia-label,aria-valuemin/max/now- Keyboard navigation:
Tabto focus,Arrow keysto move,Enter/Spaceto select - Focus management: auto-focus on first option on new question
prefers-reduced-motion: all animations reduced to near-zero duration- Touch targets: minimum 44px height (Apple HIG compliance)
Consumers
- Emanagramma (
willni/emanagramma) — Viral Resonance Test- onScore:
scoring.product - onClassify: 4-level custom classifier (bezumny → razumny)
- onRenderResult: custom UI with level icons
- onScore:
How to use hooks, events and CSS?
Testing
How to run tests?
pnpm test:allLicense
How to license? - ISC
