Backend suite, profile ui5 (76 checks: 69 core, 7 UI5-profile; 62 MUST,
14 SHOULD), run 2026-10-03 with npm run conformance:<backend>. The traffic
of these runs is recorded in ../traffic/. Revision 0.3 added
error.no-reflection (MUST, open question 3);
both reference backends fail it - see below.
| Backend | Version | Apps | Pass | Fail | Warn | Skip | Verdict |
|---|---|---|---|---|---|---|---|
node-runtime |
@abap2ui5/node-runtime 1.146.0 (abap2UI5 1.146.0, transpiled) |
the ABAP apps, transpiled with @abaplint/transpiler-cli 2.13.93 |
75 | 1 | 0 | 0 | one MUST failed: error.no-reflection (fix in abap2UI5 core, pinned) |
cap2ui5 |
@cap2ui5/cds-plugin 0.4.0 on @abap2ui5/node-runtime 1.146.0, @sap/cds 10 |
the JavaScript apps | 74 | 1 | 1 | 0 | one MUST failed: error.no-reflection (inherited from the runtime, pinned) |
Per area (both backends identical except errors):
| Area | Checks | node-runtime | cap2ui5 |
|---|---|---|---|
| transport | 8 | 8 pass | 8 pass |
| response | 8 | 8 pass | 8 pass |
| event | 5 | 5 pass | 5 pass |
| model | 10 | 10 pass | 10 pass |
| message | 4 | 4 pass | 4 pass |
| slots | 7 | 7 pass | 7 pass |
| action | 3 | 3 pass | 3 pass |
| nav / route | 14 | 14 pass | 14 pass |
| session | 5 | 5 pass | 5 pass |
| error | 5 | 4 pass, 1 fail (error.no-reflection) |
3 pass, 1 fail (error.no-reflection), 1 warn (error.details) |
| ui5 | 7 | 7 pass | 7 pass |
The ABAP system itself (abap2UI5 on NetWeaver / ABAP Cloud) has not been run yet - it needs a system with the apps pulled in by abapGit; the transpiled runtime is the same framework source.
error.no-reflection (MUST since revision 0.3) starts an unknown app with
<script>, <img> and quotes in the request URL and requires the 500 body
not to contain them verbatim. Both reference backends fail it, identically:
Request failed, no event (initial rendering), app_start Z2UI5_CL_CONF_DOES_NOT_EXIST,
url /?app_start=Z2UI5_CL_CONF_DOES_NOT_EXIST&conformance=<script>alert("conformance-probe")</script><img src='conf...
The class name is stripped to name characters ([ACT] app_start_safe),
the URL is not (abap2UI5 1.146.0, [H] request_context_info). The
backend is wrong (the maintainer's decision on
open question 3):
the fix strips the URL like the class name and is made in abap2UI5 core
(ed8115b "Strip the request URL to safe characters in the 500 body", on
its working branch on 2026-10-03); cap2UI5 hosts the same framework and
gets it with the runtime. Until both
reference hosts run a release with the fix, the failure is pinned in
../test/lib/expected.mjs (the tests fail
when it starts to pass, so the pin is removed with the bump) and the
recorded traffic carries it (counts.fail 1 on both).
test/traffic.test.mjs compares the two recordings check by check - status,
media type and JSON body, draft ids normalized - and finds them
identical apart from the following. Both are the same framework
(cap2UI5 hosts @abap2ui5/node-runtime rather than porting it), so this
was the expected outcome; what differs is what the host adds around it.
- Error body of a JavaScript app's exception - filed difference,
within the spec. A
throwin a cap2UI5 JavaScript app is no ABAP exception: it passes the framework'sCATCH cx_rootand reaches the plugin's own catch, which answers500 text/plainwithroundtrip failed (<correlation id>)and logs the cause server-side (cap2UI5plugin/cds-plugin.js,roundtrip). Status, media type and the draft handling are the framework's (error.app-exception,session.failed-roundtrip-not-persistedpass); only the body text names nothing, soerror.details(SHOULD) warns. Spec decision: the content of an error body is not normative (../spec/errors.md); naming the failure is a SHOULD with "MAY hide details on purpose", which is exactly the reference's owncheck_hide_error_details. Filed for cap2UI5: the masking is deliberate (no reconnaissance for clients), but it differs from the framework's errors on the same route (an unknown draft shows the framework's full body) - a project setting, or the user exit'scheck_hide_error_details, could decide both the same way. Content-Typecharset spelling of that body -text/plain; charset=utf-8(cap2UI5, express) vstext/plain; charset=UTF-8(framework). Media types are case-insensitive in their parameters; not a difference the spec cares about.
Places where the implementations made a choice the specification now records - as a rule, or as an implementation note:
- Only bound attributes are the model. The called app's
MODELholdsINPUT/OUTPUT, not the unboundHAS_PREV- normative (../spec/response.md). - An app that binds nothing sends no
MODEL, even with a display - normative exception to "a display carries the model". - Draft ids repeat after
nav_app_leave. Leaving twice to the same caller answered the same id twice - implementation note; a frontend must not assume uniqueness (../spec/sessions.md). - An earlier draft id continues from its own snapshot - normative (retry safety, ../spec/sessions.md).
- Unknown draft on an event = error; on a route = fresh start + toast - normative, both behaviours of the reference.
- The request URL is reflected unescaped into the 500 body - recorded
as an implementation note in revision 0.1 (safe only as
text/plain+nosniff+ a frontend that renders text); since revision 0.3 a backend MUST NOT reflect unvalidated request data (error.no-reflection, above; ../spec/errors.md). DEFAULTrouting mode on every hop to an app without a mode - normative (../spec/navigation.md).- Event argument conversion (
true->X,null/false-> empty, objects -> compact JSON text) - normative, the same on both backends.
Frontend suite, 81 checks (66 MUST, 15 SHOULD; 66 core, 8 portable, 4 UI5,
3 semantic), run 2026-10-03 (the terminal renderer and the Adaptive Cards
renderer again 2026-10-04) with
abap2ui5-conformance frontend --adapter <name> (each frontend at its
widest profile). The scripted backend answers from
../traffic/node-runtime/ plus synthetic edge
cases. A skip is no verdict: outside the profile, a capability the
frontend does not have (a URL, a DOM, timers), or an interaction it cannot
perform.
| Frontend | Version | Profile | Pass | Fail | Warn | Skip | Verdict |
|---|---|---|---|---|---|---|---|
UI5 SPA (ui5) |
abap2UI5 main 1bbb9d4 app/webapp (which CI pins), OpenUI5 1.153.0 (npm), Chromium 141 |
ui5 | 77 | 0 | 0 | 4 | conformant (ui5) - portable.box-details fixed in abap2UI5 72c86cc |
agent client (agent) |
abap2UI5/mcp-server lib/appclient.mjs @ a4d9f07 (main, PR #44; vendored) |
semantic | 61 | 0 | 0 | 20 | conformant (semantic) - the five MUST deviations of ea4e9fa fixed |
Adaptive Cards renderer (adaptive-cards) |
renderers/adaptive-cards/ of this repository (prototype), Adaptive Cards 1.5 |
portable | 66 | 0 | 0 | 15 | every check it can be driven through holds |
terminal renderer (terminal) |
renderers/terminal/ of this repository, driven with keys through its state machine |
portable | 72 | 0 | 0 | 9 | every check it can be driven through holds, the router checks included |
Web Components (webcomponent) |
abap2UI5/frontend-webcomponent 0.1.0, dist/ built from main 410d607 (PR #2) |
portable | 68 | 0 | 0 | 13 | conformant (portable) in 3 of 4 runs; model.number-and-boolean intermittent (below) |
headless ABAP simulator (headless) |
- | - | - | - | - | - | not drivable: in-process, no HTTP seam (frontend/README.md) |
The UI5 SPA run is stable (two consecutive runs, identical results) and
takes about two minutes; test/frontend.test.mjs pins the UI5, the agent,
the Adaptive Cards and the terminal result check by check. Until 2026-10-10
CI pinned abap2UI5 5d7e91f (app/webapp as at b812079, 1.146.0), where
the UI5 SPA failed portable.box-details (finding 1 below; 76 pass, 1 fail,
4 skip, with OpenUI5 1.144.0 and 1.153.0 alike). abap2UI5 fixed it on main in
72c86cc (open question 7, decided: expanded); CI pins a main that carries
the fix, and the pin is gone - every MUST holds, 77 pass, 4 skip.
- Message box details are not shown on OpenUI5 >= 1.120 -
portable.box-details, MUST; the frontend is wrong (fixed in abap2UI572c86cc, after 1.146.0). AMESSAGE_BOXwithdetailsshows the text and an empty details area. Evidence (OpenUI5 1.144.0): the box's VBox holdsText(visible),Link "View Details"(hidden),MessageStrip "Details could not be loaded."(hidden),FormattedText(visible,htmlTextempty). abap2UI5app/webapp/core/actions/ControlCall.jsexpandBoxDetailsmakes theFormattedTextvisible and hides the link - butsap.m.MessageBoxsets the details text only in the link's press handler (showDetails, 1.144.0MessageBox.jsline 249; the same in 1.120.0), while 1.71.80 set it when creating the control (new FormattedText().setVisible(false).setHtmlText(mOptions.details)). So the expansion works on 1.71 and leaves the details unreachable on every release since the lazy fill - including the 1.144.0 the abap2UI5 e2e suite pins. Fix belongs inexpandBoxDetails(set the sanitized details ashtmlTextitself, or press the link). Spec: details are shown with the box (../spec/actions.md; alternative in open question 7). - An unknown control fails the view -
portable.unknown-control; the spec was wrong.<x:Gadget xmlns:x="com.example.conformance"/>ends inModuleError: failed to load 'com/example/conformance/Gadget.js'and the fatal overlay. The portable profile called the UI5 frontend "a portable frontend by construction", which would require a placeholder; but in the UI5 profile every UI5 element is in the vocabulary and one that does not load is a failed view (and the frontend's SDK hint depends on that). The profile now says the UI5 frontend renders every portable app and binds the tolerance rule to the other renderers; the check skips a UI5-profile frontend (open question 6).
Everything else holds in the browser, with real controls: the envelope,
ID continuation (also an id seen before), raw JSON event arguments (true
stays true), the delta rules (whole scalars and structures, __delta
rows, nested __delta, the whole value beating a delta, the popup's own
model, no delta for pushed values, edits typed during a roundtrip kept), the
CONFIG cadence, sap-contextid kept, the CSRF handshake, no retry of a
500, one roundtrip at a time, the queue-last wire, PROTOCOL 3 refused
naming both numbers, unknown keys ignored, every slot rule including the
teardown on an APP change, model pushes only into the answering app's
slots, follow-up actions in order after rendering, an unknown action
skipped and logged, the box close event with the pressed action, the hash
under KEEP, Back restoring the caller's route, the app-state hash, and an
error body shown verbatim as text.
Re-vendored at abap2UI5/mcp-server a4d9f07 (main, PR #44): every check
that applies passes - 61 pass, 0 fail, 0 warn, 20 skip, two runs
identical. The five MUST deviations and three warnings measured at
ea4e9fa (revision 0.2) are fixed in the client, as filed:
- PROTOCOL check - a
PROTOCOL: 3response is refused naming both numbers (response.protocol-mismatch,-message). sap-contextidread and sent back (transport.contextid-kept; open question 9, decided: MUST).- One roundtrip at a time - a second
acton a session in flight is queued (transport.one-at-a-time; open question 8, decided: the client queues). - POPUP/POPOVER torn down on an
APPchange (slots.app-change). - The error body verbatim, no markup stripped (
error.as-text).
and the CSRF token handshake (transport.csrf-token) and edits made during
a roundtrip kept (model.pending-survive-push). Skipped: the portable and
UI5 profiles (not claimed), the URL, DOM and focus checks (no browser), a
nested-table cell (the snapshot does not describe nested tables), a
programmatic model edit. traffic/*/agent-client.json is still the
recording of ea4e9fa; semantic.recorded-snapshots passes against it.
A renderer that is not a browser, run in process
(../renderers/adaptive-cards/):
the card host speaks the protocol to the scripted backend, each answer is
rendered into an Adaptive Card 1.5 and the suite reads the card; a "press"
submits what a card host submits - the action's data merged with every input
value, ids being binding paths - and the renderer turns that back into the
request. All 66 checks that apply pass, two runs identical: the envelope,
ID continuation, raw event arguments (model arguments re-read after the
edits of the same submit), every delta rule including nested __delta
rows (the edits are found by comparing the submitted values with the model
the card was rendered from), sap-contextid, the CSRF handshake, no retry
of a 500, one roundtrip at a time (a submit in flight queues the next, open
question 8), PROTOCOL 3 refused, the slot rules (popup modal, popover not:
a press in MAIN closes it, as UI5 does), the NEST placeholder, toasts,
boxes (their details shown expanded, as text) and their close event,
START_TIMER, an unknown and an excluded
follow-up action skipped and logged, the tolerance rule (an unknown control
becomes a placeholder and an unsupported entry), and an error body shown
verbatim (a TextRun, not markdown). Skipped: a URL (routing, Back, the
app-state hash), a DOM (the sanitizer probe), focus, a document title and
programmatic model edits; the UI5 and semantic profiles are not claimed.
(portable.box-details asked for a DOM until 2026-10-04 and was skipped;
it reads only the text on screen, which every adapter reports, and now
runs - see the terminal renderer below.)
Every golden card also validates with the Adaptive Cards JavaScript SDK
3.0.6 (AdaptiveCard.parse + validateProperties, no issue; run by hand,
not a dependency).
Found on the way, the spec held: nothing in the portable profile needed
a browser to be rendered; what a card cannot do (raise change events,
show a URL) the profile already lets a renderer drop or the edit travels
with the next action.
A renderer for a text terminal, run in process
(../renderers/terminal/): its session
speaks the protocol to the scripted backend, and the adapter drives its
state machine the way a user drives it - fill Tabs to the field bound to
the path and types the value (Ctrl+U, the characters, Tab to commit),
press Tabs to the action raising the event and presses Enter, back is
Alt+Left; the state is read from the screen (a layer per slot, the bound
fields' values). All 72 checks that apply pass, five runs identical: the
envelope, ID continuation, raw event arguments, every delta rule
including nested __delta rows (the edits are the committed fields' paths,
as in the UI5 frontend - an edit typed and typed back is none),
sap-contextid, the CSRF handshake, no retry of a 500, one roundtrip at a
time (queued), PROTOCOL 3 refused, the slot rules, the NEST placeholder,
toasts, boxes with their details expanded and their close event,
START_TIMER, SET_FOCUS (the focus moves to the widget with that id after
the views are built), SET_TITLE, an unknown and an excluded follow-up
action skipped and named in the status line, the tolerance rule, an error
body shown verbatim - and, unlike the card, the four router checks: the
renderer keeps the hash a browser would show and its history, synchronised
once per response like the UI5 router, sends it as HASH and restores the
caller's route on Back with an app-start-shaped request. Skipped: a DOM
(the sanitizer probe - the terminal's own sanitizing, every control
character a backend sends replaced before it reaches the terminal, is
tested in test/terminal.test.mjs), a programmatic model edit
(model.whole-beats-delta: a user cannot type a whole table); the UI5 and
semantic profiles are not claimed. test/backends.test.mjs also drives it
against the node-runtime host (BIND, ROUTE with Back, NAV, --print).
Found on the way:
portable.box-detailsasked for a DOM but reads only the text on screen - the suite was wrong. The check needs no capability: the text is part of every adapter's normalized state. It now runs for every portable frontend; the Adaptive Cards renderer and the terminal pass it (the UI5 SPA's result is unchanged - it has a DOM).- The spec held for a frontend with a hash but no browser: the router rules (spec/navigation.md) are written against the URL hash, and a history kept in memory satisfies all of them; nothing in the portable profile needed a browser. Where a terminal has no counterpart (a URL to open, a file to download) the profile's MAY-be-a-no-op already covers it - the terminal names the URL in its status line.
Run against dist/ built from its main 410d607 (PR #2: ROUTER and the
URL hash, verbatim error text, formatters, growing), in a separate worktree
(WC_FRONTEND_HOME): 68 pass, 0 fail, 0 warn, 13 skip in three of four full
runs - every portable-profile check that applies, now including the router
checks and error.as-text. In the other run, and in one of three runs of
the check alone, model.number-and-boolean failed: the edited
StepInput (/QTY, 42) was missing from MODEL - the press reached the
frontend before the ui5-step-input had committed the typed value.
Intermittent, not yet attributed (the adapter fills the inner input,
presses Enter and blurs, then waits 50 ms; a longer wait did not make it go
away); filed for abap2UI5/frontend-webcomponent. Skipped: the UI5 and
semantic profiles, message.box-close-event (the box close is not wired in
the adapter), the focus and model-edit checks, and three table-cell checks
(its DOM exposes control ids, not binding paths). Its vendored copy of the agent client is being
re-pinned to a4d9f07 there as well.