A Native TypeScript QTI 3 Player
A strict TypeScript QTI 3 runtime and native Web Component player with XML import, typed item and interaction models, declaration validation, response processing, outcome scoring, saved-state serialization, fixture coverage, and Playwright-tested browser rendering.
As we added support for more QTI 3 question types in QFlowLearn, we realized that we needed a player that supports the full QTI 3 interaction set.
Once the current item interaction set was in scope, we also had to ask what we wanted from a player in the long run. For longsightgroup/qti3, the answer was fairly quick: native Web Components, low dependencies, and strict TypeScript.
React and Vue are wonderful frameworks, but they are heavy for a reference item player, and they wrap implementation choices around their own component model. We wanted the parsing, validation, response processing, rendering, scoring, and saved-state contract to be readable without pulling in 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, 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.document) throw new Error("Unable to parse item");
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.
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 has made fast unit tests feel normal. Playwright has made browser behavior testable without building 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,
},
});
Keeping the stack small
The dependency list is short: TypeScript, Vite for the manual harness, Vitest, Playwright,
axe-core, oxfmt, and oxlint. The core has one runtime dependency today:
stax-xml, a small ESM parser that works in
Node, Deno, browsers, and edge runtimes. We use it to read XML events, then keep those
parser events inside the package and expose QTI model types instead. The core should not
need a browser framework, an HTTP client, an alert library, or a drag-and-drop package. If a
dependency enters, it should make conformance, security, accessibility, or maintenance
measurably better. 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 commits have focused on public fixtures, package metadata, release checks, 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.