xoong
Menu · Tooling and tests
DocsArchitectureTooling and tests

Tooling and tests

The tools exist so that a mistake is caught where it is made, with a message that contains the fix. One rule engine serves the command line, the editor and AI assistants, and the tests run the real framework with only the network replaced.

One check engine

One check engine, three front ends Three inputs feed the check engine: operations.json written by xoong sync, the list of xo-star names in syntax.js, and your HTML, JavaScript and TypeScript files. The engine, with its functions checkHtml and checkScript, produces findings with codes XO2xx. Three front ends show the same findings: the xoong check command for the terminal and CI, the xoong-lsp language server for editors, and the xoong-mcp server for AI assistants. At the bottom is the command that runs the first of them, in a terminal window. The code window shows Terminal: npx xoong check index.html. Steps: 1. You run xoong check, or your editor or your AI assistant does. The check engine takes three inputs: operations.json (from xoong sync), the list of xo-* names in syntax.js, and your HTML, JS and TS files. 2. The engine produces findings with codes XO2xx, each with a fix. The three front ends show the same findings: xoong check in the terminal and CI, xoong-lsp in your editor, xoong-mcp for AI assistants. When the animation plays, dots go from the three inputs into the engine together, and then out to the three front ends together, while the command is lit: one set of findings, shown three ways. operations.jsonfrom xoong sync syntax.jsevery xo-* name Your filesHTML, JS, TS check enginecheckHtml()checkScript()codes XO2xx xoong checkCLI and CI xoong-lspdiagnostics,completion, hover xoong-mcptools: check,explain_error 1 2

A new rule is written once and appears in every front end with the same message and the same fix. In motion, dots go from the three inputs into the engine together, and then out to the three front ends together, while the command is lit: one set of findings, shown three ways. Use the slider, or press a number, to look at one step.

xoong check <files or folders> lints screens against your API. The engine is checkHtml(source, table, file) for HTML and checkScript(source, table, file) for JavaScript and TypeScript. It parses HTML with parse5 and keeps exact line and column. In scripts it reads the HTML inside html`...` templates and the operation named in a string passed to client.load, query, mutate or stream, on something that looks like a client (client, apiClient, getClient(), xo); a name built at run time is not checked. It knows your operations from operations.json and the framework's names from syntax.js.

Every finding has a stable code, a place, a message and a fix. The output, and the process exit code (1 when there is an error), are the same in the terminal and in CI:

users.html:3:13  error XO201  unknown operation "POST /user/{id}/disable" (closest: "POST /users/{id}/disable")
    fix: Use "POST /users/{id}/disable"

1 files, 1 errors, 0 warnings
CodesWhat they catch
XO201 to XO204an unknown operation (with the closest name), xo-click on a GET, get on a non-GET, a read or write method on the wrong kind of operation in JavaScript
XO210, XO251 to XO253a missing p-* for a path parameter; <xo-table> outside a view, or sorting, paging and filters that the operation has no query parameters for
XO220, XO221, XO252, XO254, XO255xo-submit not on a form, an empty xo-each or xo-if, a <xo-form> with nothing it can draw or that can never satisfy the server, xo-selected or xo-select in the wrong place
XO230an xo-* element or attribute that xoong does not define (usually a typo, with the closest name)
XO240, XO241a button or link without an accessible name; xo-click on something a keyboard cannot reach
XO260 to XO263route paths, layouts, outlets and route parameters

xoong doctor openapi.json is the same idea for the document: XO1xx codes for what limits xoong, such as a missing operationId (XO110), no identity property (so records are not shared), an unresolved $ref (XO150), a union that cannot be told apart (XO160, XO161) or no documented errors (XO130). Warnings for one code are cut off after five unless you pass --all. xoong sync also reports the errors.

Editors: html-data and the language server

{ "html.customData": ["./.xoong/xoong.html-data.json"] }

Not yet: a VS Code extension, lint presets for ESLint and Stylelint, and SARIF output.

For AI assistants: llms.txt and the MCP server

llms.txt, at the root of the repository and on this site, is the whole public API in about 1,800 tokens, with the rules an assistant must follow. A test keeps it honest: it must stay under the budget, name only commands that exist, and mention exactly the xo-* names the code defines.

xoong-mcp is a Model Context Protocol server: JSON-RPC 2.0 over stdio with one message per line, written without an SDK. It has six tools, which make a loop: get_spec (the llms.txt), list_operations (the real names from operations.json), write the screen, check (HTML or script source in, findings out), fix from the message; explain_error says what a code means; explain_syntax says what an element or attribute does and which values it takes; search_docs searches the bundled docs and a project's own. A test makes sure explain_error covers every code that check can print. A scaffold tool is not built yet.

{ "mcpServers": { "xoong": { "command": "npx", "args": ["xoong-mcp"] } } }

@xoong/test

A screen test renders real markup, with the real data layer, against a fake network. That is the only seam: mock the network, never the data layer, because the cache, the store and invalidation are part of what you are testing.

import { mount, mockApi } from '@xoong/test';
import table from '../.xoong/operations.json' with { type: 'json' };

const api = mockApi(table, { listUsers: () => [{ id: 1, name: 'Ada' }] });   // keyed by operationId or "METHOD /path"
const screen = await mount('<xo-view get="/users"><li xo-each=".">{name}</li></xo-view>', { api });

screen.texts('li');            // ['Ada']
await screen.click('button');  // clicks, then waits until no request is pending
await screen.push({ type: 'patch', entity: 'User', id: 1, path: '/name', value: 'Ada L.', version: 2 });
api.callsOf('listUsers');      // what the app really asked for
api.unhandled;                 // [] means nothing unexpected was called

The test strategy

The layers of tests Four layers of tests, from the cheapest to the most realistic. Unit tests run in Node and cover signals, the store, the client, the form model, the CLI and the OpenAPI analysis. Screen tests run in happy-dom with mount and mockApi on a real DOM and a fake network. Real-browser tests drive Chromium for templates, the router, controls, the strict-CSP build and a two-tab end-to-end run. Documentation site tests build the site under a project path and check the CSP, axe, focus indicators and sideways scroll at 390 pixels. Unit tests (Node)signals, store, client, form model, CLI, OpenAPI analysis Screen tests (happy-dom)mount() and mockApi(): a real DOM, a fake network Real-browser tests (Chromium)templates, router, controls, strict-CSP build, two-tab end to end Documentation site testsbuilt under /xoong with the CSP: axe, focus indicators, 390 px

Four layers. Cheap and fast on top, realistic at the bottom; each catches what the one above cannot.

LayerHow it runsWhat it is for
Unit*.test.mjs, Nodepure logic: signals, the store and normalisation, the client (cache, invalidation, retry, errors, streams), the form model, the OpenAPI analysis, the CLI. The corpus tool also runs real-world OpenAPI documents through the analysis and checks that the generated types compile.
Screens*.spec.mjs, happy-dommount and mockApi flows: rendering, actions, errors, live updates, forms. Fast, and measured for coverage. Many cover the same behaviour as the real-browser tests, so each environment checks the other.
Real browser*.test.mjs that launch Chromium through Playwright (Firefox or WebKit with XOONG_BROWSER)what happy-dom cannot do: layout, focus, native <dialog> and popover details, templates against a fake backend over HTTP and server-sent events, the strict-CSP build, and an end-to-end run of xoong dev with two tabs seeing a live update.
Docs siteone test that builds site/ under /xoongevery page loads under the strict policy with no violation or error; axe (WCAG 2.0 and 2.1, A and AA) finds nothing on any page in four palette and light or dark combinations; a tab through five main pages measures every focus indicator against what surrounds it (3:1, WCAG 1.4.11), which axe cannot do; no page scrolls sideways at 390 px; menus and dialogs are checked in their open state.
Architecturetest/architecture.test.mjsthe promises of the design (see Overview and Build and delivery).

The runner is Vitest with two projects, logic (Node) and screens (happy-dom). There is also a first-attempt-correctness benchmark in tools/benchmark: four tasks, scored by a script, to check that an assistant with only llms.txt writes working screens. It has a first small measurement and is not a rate.

This site is generated

The pages of this documentation are JavaScript objects (file, group, title, body) in tools/site/. npm run site writes site/*.html from them with one layout function; the architecture test fails when a page on disk differs from what the generator writes. The generator makes the "On this page" list from the h2 headings, highlights code while it writes the page, and shows each live example's markup both as the running preview and as the code tab, so the two cannot drift apart. Some pages come from the design kit in design/brand-kit. The diagrams on these Architecture pages are inline SVG with a title and a text description, drawn with the page's own colour tokens so they follow the palette and dark mode. The built site is served by the same strict policy as any xoong app, and the site test above checks it.

Where to look in the code