@aerokit/test
Publishing note: every release publishes this folder as
@aerokit/test, versioned with the platform. Consumers depend on@aerokit/testand import@aerokit/test/@aerokit/test/fixtures. A harness written against the earlier@aerokit/sdk/testsubpath keeps resolving while@aerokit/sdkstill ships it, but@aerokit/sdkis being deprecated: switch the import and the dependency to@aerokit/test.
Generic Playwright runner that executes an intent module's generated
<name>.test manifest against a running Eclipse Dirigible instance. The manifest (emitted by the
AppTestIntentGenerator in engine-intent, alongside .edm/.model/.bpmn/...) says WHAT the
app promises — entities, layouts, fields, seeds, languages; this package knows HOW to verify a
promise against the generated Harmonia UI and the reused REST controllers. So one runner, versioned
alongside the templates whose markup it drives, checks every generated app the same way.
This is the app-integration-test surface (drives a browser + the REST layer from outside the
instance). It is distinct from @aerokit/sdk/junit, the in-instance server-side unit-test facade.
Use from a module
A module keeps a tiny test/ harness at its repo root (outside the published project folder, so
it never enters the registry or the npm package):
// test/app.spec.js
import { fileURLToPath } from 'node:url';
import { runTest } from '@aerokit/test';
runTest(fileURLToPath(new URL('../<project>/<name>.test', import.meta.url)));
// test/package.json
{
"type": "module",
"scripts": { "test": "playwright test" },
"devDependencies": { "@aerokit/test": "*", "@playwright/test": "^1.45" }
}
BASE_URL=http://localhost:8080 npm test # defaults: admin/admin, installed Chrome
APPTEST_SHELL=1 npm test # also assert the shared-shell menu item
APPTEST_A11Y=strict npm test # fail on serious/critical accessibility violations
Env: BASE_URL (default http://localhost:8080), APPTEST_USERNAME/APPTEST_PASSWORD
(default admin/admin), APPTEST_SHELL (opt-in shared-shell flow), APPTEST_A11Y (report
default, strict, off - the accessibility flow).
Flows per entity
- list — the shell breadcrumb naming the list by its plural label, the declared columns, and
(when
expectSeedData) a row. An entity whose manifest carrieslistrenders exactly those columns, in order, and a field left off it must have no header at all; withoutlistthe columns are the fields whosemajoris notfalse. - crud — UI create → filter → row appears; edit via the row menu → save (a
documentsaves in place, so the walk leaves through Cancel); delete via the row menu → confirm → gone. A walk that fails halfway still removes its test row over REST. An entity without aroute(a composition child with no page of its own) has no list, crud, multilingual or shell flow. - rest — the same CRUD over the generated Java controllers via
APIRequestContext(isolates backend vs UI failures), asserting the manifest's field names bind and delete yields 404 — or, for an entity awhenDeleted: refuseprocess guards, that the refused record is still served. - multilingual — switch the shared language key, reload, a seeded row shows its translated name.
The assertion waits for the list's own data GET and allows 15 s; a failure names the
Accept-Languagethat GET sent, whether its body carried the expected word, and the table's first row, so a slow render reads differently from a missing translation. - my — the personal (my) surface WIRE contract, when the manifest marks an entity
personal: create through the scoped<Entity>MyController(owner FK forced server-side), everysensitivefield null on the personal wire, the own row in the personal list, foreign rows 404 (when the owner relation is optional), own-row delete. Skips with a pointer when the test user has no identity mapping (seed the dev identity row). Personal UI parity (resolved labels, chat, calendar on the Personal shell) is deliberately not asserted yet — it tracks the personal-template parity fixes. - shell (opt-in) — the shared application shell's nav item opens the module SPA in its iframe.
- a11y — axe-core scans the list page and the create form against WCAG 2.1 A/AA and attaches
each page's result (
axe-<Entity>-<page>.json) to the test report.APPTEST_A11Y=report(default) only reports - a module's first release;strictalso fails on aseriousorcriticalviolation;offskips it. Needs@axe-core/playwrightin the harnessdevDependencies(the flow skips itself without it). Contrast against Harmonia's own--primaryis not counted in strict mode: the default palette misses 4.5:1 there, which the component library fixes; every other contrast failure is.
Test records carry an APPTEST- prefix and are removed in teardown; seed data is never mutated.
A field the app owns rather than the user — a number: series, a calculated or readOnly: value —
is never written or flipped by a flow (the manifest marks it readOnly). A relation the form renders
read-only — a calculatedActionOnUpdate, a platform-owned FK — carries the same flag and is never
picked in the UI. A relation fixed after create (fixedAfterCreate: true) carries readOnlyOnEdit: it is
picked on create and never changed by an edit, which the generated app answers with a 409. A field declaring a
pattern: gets a value of that shape rather than the marker string. An optional pattern: field
whose shape none of the sample candidates has (an IBAN, say) is left empty; a required one keeps the
marker, so the controller's 400 names the field. A dependsOn relation is sampled to match its
trigger, as the form narrows it: with filterBy alone the trigger is the row the dependent's filterBy
names; with valueFrom the dependent is the row whose filterBy (or key) equals the trigger row's
valueFrom. Relations an agree check names are picked so
their targets agree; a composite uniqueKeys entry is given a relation combination no live row
carries, and the rest and crud flows skip with the key named when every combination tried is taken.
A slots entity's start (the manifest's slots window) is a random grid start inside the hours, on
an open weekday of a far-future day, so the server's slot rules (a 400 off the window, a 409 for a
booked slot) never refuse it; the REST requests name UTC as their X-Time-Zone, the zone it is
computed in.
A relation carrying pickable is sampled only from the rows its rule admits — the chooser offers no
other, so a record built from one would never save — and one carrying forbiddenTarget only from
rows the record's own forbidWhen does not refuse, which is what the REST flow needs, having no
picker to read. When no row is left, a required relation fails naming the rule's own message rather
than "no rows". A relation whose target is labelled by a stampOn: issue number carries
labelDocumentNumber: the picker shows every row not issued yet as the same "(draft)", so the sample
is a stamped row, picked by its number; with none stamped yet an optional relation is left unset and
a required one picks "(draft)".
Custom UI hooks
runTest(manifest, { extend }) (runAppTest remains a deprecated alias):
widgets: { '<widget>': async (page, field, value) => ... }— custom widget fillers.entities: { <Entity>: { skip: ['delete'], beforeCreate, afterCreate } }— per-entity hooks.
Hand-written specs live in test/custom/*.spec.js and import @aerokit/test/fixtures
(pre-logged-in test, an api client, expect) so login/baseURL/cleanup come free.