A Native TypeScript QTI 3 Player
A strict TypeScript QTI 3 runtime and native Web Component player with typed models, response processing, scoring, saved state, fixtures, and Playwright-tested rendering.
As QFlowLearn added more QTI 3 question types, it needed a player that could support the full QTI 3 interaction set.
Once the authoring environment needed to support every defined item in the official spec, the player requirements became clear: native Web Components, zero runtime dependencies in the core package, good tests, and strict TypeScript. That became longsightgroup/qti3.
React and Vue are useful frameworks, but they are heavy for a reference item player, and they wrap implementation choices around their own component model. We wanted parsing, validation, response processing, rendering, scoring, and saved-state behavior to be readable without a framework runtime.
QTI 3 is a contract
QTI 3 gives assessment content a concrete contract. It covers item structure, response declarations, response processing, shared CSS vocabulary, accessibility metadata, Personal Needs and Preferences (PNP), package relationships, and results reporting expectations. When an implementation gets those details right, content can move between systems without being rewritten around one vendor’s player.
Where TypeScript helps
The XML import boundary is where TypeScript matters most. XML arrives as strings: element names, attributes, response identifiers, cardinalities, base types, coordinates. The parser still has to validate those at runtime; TypeScript is not a schema validator. But once the XML is translated into a QTI model, the rest of the code can stop passing anonymous nodes around. A response declaration has a known cardinality and base type. An interaction has a known QTI interaction type. Body content is text, an element, an interaction, feedback, or a printed variable. That gives the renderer, response processor, support matrix, and saved state code the same model to work from.
A small core API
This is the kind of core API we want:
import {
createItemSession,
parseQtiXml,
validateAssessmentItem,
} from "@longsightgroup/qti3-core";
const parsed = parseQtiXml(xml);
if (!parsed.ok || !parsed.document) {
throw new Error(parsed.diagnostics.map((item) => item.message).join("; "));
}
const validation = validateAssessmentItem(parsed.document);
const session = createItemSession(parsed.document);
session.respond("RESPONSE", "A");
const result = session.score();
console.log(validation.diagnostics);
console.log(result.outcomes);
console.log(result.state);
Learning from existing work
Amp-up’s Vue 3 QTI player has been a useful reference. We have learned from its rendering behavior, response processing, PNP/session control, catalog events, and the messy places where a clean XML spec turns into a real question on a screen.
Studying it helped clarify where longsightgroup/qti3 could take a different path:
framework-neutral packages, core logic that runs without browser DOM APIs, and a native Web
Component player. Imports, validation, response processing, scoring, fixtures, and
saved-state checks can run in CLIs, CI, Node, or Deno without rendering an item first.
PNP belongs in core
QTI accessibility is not only a rendering concern. A host may need to know that an item
contains catalog cards, companion materials, Data-SSML, text-to-speech metadata, or shared
vocabulary classes such as qti-keyword-emphasis before a Web Component is ever defined.
That is why the core package now carries more of the PNP-adjacent surface area. The parser preserves catalog and companion-material metadata. The shared vocabulary registry can tell authoring tools and delivery hosts which classes are understood, which are conditional, and which need host policy. The Data-SSML and text-to-speech helpers give the same model to server-side validation, authoring previews, and browser rendering.
Why now
It also lands at a good time for JavaScript. More packages are ESM first. TypeScript is the default for serious library APIs. Vitest makes fast unit tests normal. Playwright makes browser behavior testable without a separate Selenium-era apparatus. Tools like oxlint and oxfmt help for the same practical reason: fewer slow toolchains, fewer plugins, and fewer decisions that have to be rediscovered by every project.
The browser side should stay just as explicit:
import { defineQtiAssessmentItemPlayer } from "@longsightgroup/qti3-player";
defineQtiAssessmentItemPlayer();
const player = document.querySelector("qti-assessment-item-player");
await player?.loadXml(xml, {
status: "interacting",
sessionControl: {
validateResponses: true,
showFeedback: true,
},
});
The public manual harness is the fastest way to try the current player against fixture items without cloning the repository.
Keeping the stack small
As of @longsightgroup/qti3-core 0.9.1, the core package has zero third-party runtime
dependencies. XML parsing is handled inside the package by a small dependency-free parser
that records source ranges for redaction, does not resolve external entities, does not
process DTD entity declarations, and does not touch the network or filesystem.
That matters for the trust boundary. A QTI runtime should be easy to audit, easy to run in Node, Deno, browsers, and edge runtimes, and boring to install in locked-down environments. The core should not need a browser framework, an HTTP client, an alert library, a drag-and-drop package, or a general-purpose XML dependency. If a runtime dependency enters, it should make conformance, security, accessibility, or maintenance measurably better.
The development dependency list is still pragmatic: TypeScript, Vite for the manual harness, Vitest, Playwright, axe-core, oxfmt, and oxlint. axe-core is there to catch accessibility regressions early in CI. It is not the whole accessibility story, so the support matrix also needs keyboard, focus, forced-colors, and manual assistive-technology evidence.
What is next
Recent releases have focused on public fixtures, package metadata, release checks, dependency-free XML parsing, delivery-safe XML redaction, server-side scoring, shared-vocabulary authoring helpers, PNP-adjacent metadata, inline gap-match rendering, match rows, select-point and position-object behavior, point coordinate tolerances, and less brittle graphical interaction tests.
Certification is next. Before we submit, the support matrix, fixture suite, browser tests,
package inspection, and accessibility checks need to match the claims. We also need more
manual accessibility testing and better ergonomics for drag and graphical interactions.
The repo is public at
github.com/LongsightGroup/qti3.