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
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| Codes | What they catch |
|---|---|
| XO201 to XO204 | an 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 XO253 | a 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, XO255 | xo-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 |
| XO230 | an xo-* element or attribute that xoong does not define (usually a typo, with the closest name) |
| XO240, XO241 | a button or link without an accessible name; xo-click on something a keyboard cannot reach |
| XO260 to XO263 | route 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
xoong.html-data.json.xoong syncwrites it next tooperations.json. Point VS Code'shtml.customDataat it once and you get completion and hover for everyxo-*element and attribute, and your real operation names insidexo-click,xo-submit,getandop. It needs no extension.xoong-lsp. A language server over stdio. It finds.xoong/operations.jsonfrom the workspace folder upwards and re-reads it when it changes. It offers diagnostics (the findings above, as you type), completion, hover, and a code action "use the closest operation". The message handling is a pure function of a message (createLanguageServer), so it is tested without a process.
{ "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.
mockApi(table, handlers)builds afetchreplacement fromoperations.json. It matches the method and the path, and calls your handler with{ params, query, body }. Return data (answered 200) orrespond(status, body). An operation without a handler answers 404 and is listed inapi.unhandled.api.calls,callsOf(id)andon(id, handler)are there too.mount(html, { api })configures the real client with thatfetch(no retries, queries kept for ever) and a fakeEventSource, renders the markup into a div, and waits until everything has settled. Thescreenhas$,$$,text,texts,click,submit,push(event)for live events,settle,errors(thexo-errorevents seen) andunmount.- "Settled" means no request in flight, checked twice a tick apart, which is why there is no
awaitroulette.
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 calledThe test strategy
Four layers. Cheap and fast on top, realistic at the bottom; each catches what the one above cannot.
| Layer | How it runs | What it is for |
|---|---|---|
| Unit | *.test.mjs, Node | pure 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-dom | mount 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 site | one test that builds site/ under /xoong | every 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. |
| Architecture | test/architecture.test.mjs | the 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
packages/cli/src/check.js: the rules and their codespackages/cli/src/openapi.js: the analysis and thedoctorproblemspackages/cli/src/html-data.js: the editor data, written bysyncpackages/lsp/src/server.js, cli.js: the language server and its stdio framingpackages/mcp/src/server.js: the six tools andEXPLANATIONSpackages/test/src/index.js:mockApiandmountvitest.config.mjs: the two projects and the coverage thresholdstools/axe.mjs, tools/focus.mjs, tools/browser.mjs: the accessibility and browser helperstools/site.mjs, tools/site/: the site generatordocs/testing.md, docs/ai-integration.md, docs/editor-integration.md: the longer notes