abap2UI5 headless frontend
Plays the frontend's side of the abap2UI5abap2UI5One ABAP class implementing z2ui5_if_app is the whole app: the browser loads a generic UI5 shell once, and every interaction after that is an HTTP roundtrip back into ABAP.community org, abap request/response contract in ABAP, so an app class is driven and asserted on from a plain ABAP Unit test.
An abap2UI5 app is a class whose whole conversation with the outside world is one JSON request in and one JSON response out, and this repository answers the other side of that contract in ABAP: z2ui5_cl_frontend_simulator builds the request the UI5 frontend builds, hands it to z2ui5_cl_ui5_handler and replays the response the way the frontend does, in the same session and the same call stack as the test. A test addresses the app by the names its author already wrote, MV_NAME for an attribute bound with client->_bind( ) and GREET for an event id, with no CSS selectors and no control ids, and reads the screen back with get_view( ), get_model( ), get_messages( ) and get_layers( ). The frontend's five view slots are kept the same way: MAIN, the nested views NEST and NEST2, and the standalone POPUP and POPOVER, each of which keeps its own copy of the model, so an edit travels with the next event of the layer it was made in. A draft-based session continues in a later request, where resume( id state ) posts the draft id the backend restores the app from and rebuilds each layer's view XML from the get_state( ) string of the request before; a sticky session keeps the app in the handler instead of a draft and cannot be resumed.
Facts
Where it sits
Who is behind it
In the atlas: maintains 18@abap2UI5maintaineroblomov-devContributes to this
In the atlas: maintains 4, contributes to 44@oblomov-dev1 contributor
Before choosing this
Conditions
PracticalFunctional boundary
There is no control tree: visibility and enablement are not evaluated, and set_value( ) is not checked against the bound paths. The frontend CONFIG (device, UI5 version, focus, scroll) is not sent at all, so an app that branches on device or theme sees initial values. It drives z2ui5_cl_ui5_handler and the draft service, which the README states are not part of abap2UI5's released API in src/02, so a core change there can require a change here.
What it declares
Every artifact
- Type
- Package
- Origin
- By a community org
As a package
- Where it runs
- does not say
- Distributed through
- abapGit
Connections
Testing application logic, with test doubles for its dependencies
ABAP-native MCP endpoint that drives opted-in abap2UI5 apps
UI5 apps written as one ABAP class, rendered by a generic shell
Git client written in ABAP that runs inside the system
Usage, error and response-time monitor for abap2UI5
Download
README
quoted from abap2UI5/headless-frontend under MIT, readabap2UI5 - headless frontend
Test abap2UI5 apps in ABAP. No browser, no UI5, no HTTP.
An abap2UI5 app is a class. Its whole conversation with the outside world is one JSON request in, one JSON response out — the frontend sends the model delta and an event id, the backend sends back view-lifecycle actions, a model and follow-up actions. That is the entire contract.
This repo takes that contract seriously and plays the other side of it in ABAP.
z2ui5_cl_frontend_simulator builds the same request the UI5 frontend builds,
hands it to z2ui5_cl_ui5_handler, and replays the response the way the
frontend does — all in the same session, in the same call stack, in a plain
ABAP Unit test.
DATA(sim) = z2ui5_cl_frontend_simulator=>start( `Z2UI5_CL_FRONTEND_SIM_EXAMPLE` ).
sim->set_value( name = `MV_NAME` value = `World` ).
sim->click( `GREET` ).
cl_abap_unit_assert=>assert_equals( exp = `Hello World!`
act = sim->get_message( ) ).
That is a full user session: typed a name, pressed a button, read the result back. It runs in milliseconds, needs no server, no browser driver and no network, and it fails with an ABAP stack trace pointing at your app.
Why
Testing abap2UI5 apps is currently all-or-nothing. You either unit-test the pieces around the app — which skips the part where the app actually talks to the framework — or you drive a real browser with Playwright/Selenium, which is slow, flaky, needs a deployed system, and breaks whenever the layout moves. There is nothing in between.
The simulator is the in-between. It exercises the real framework: the real
main( ) dispatch, the real event handling, the real model serialisation, the
real draft handling. Only the browser is missing — and the browser was never the
part with your bugs in it.
Apps are addressed by the names their author already wrote. MV_NAME is the
bound attribute (client->_bind( mv_name )), GREET is the event id
(client->_event( 'GREET' )). No CSS selectors, no control ids, no XPath. A
test does not break when someone swaps a VBox for a Grid, because a user
pressing "Greet" does not care either. The rendered XML is still there via
get_view( ) when you do want to assert on it.
The bigger idea
If the frontend contract can be served from ABAP, the browser is not the only possible frontend — it is just the first one.
A headless module that speaks this protocol is the seam where other frontends
attach: SAP GUI, a console renderer, a REST facade, a batch driver that replays
recorded sessions — or an AI agent. The
agent addon exposes abap2UI5 apps
over an ABAP-native MCP endpoint and uses this simulator as its engine: every
MCP call is one HTTP request, so it starts an app, resume( )s the session in
the next request, and builds its screen snapshot from get_layers( ),
get_messages( ), get_actions( ) and get_events( ).
See ROADMAP.md for where it goes from here.
API
Driving the app
| Method | Purpose |
|---|---|
start( app ) | Start an app — mirrors the browser's first POST with ?app_start=<class> |
resume( id state refresh ) | Continue a draft-based session from its draft id in a new instance — see below |
set_value( name value layer ) | Queue a text value (what an input sends) into the delta of the next roundtrip |
set_json( path json layer ) | Queue a raw JSON value at a model path — true, 42, ["A","C"], {"F":1} — see Typed edits |
set_bool( name value layer ) | Queue a boolean (CheckBox, Switch), sent as JSON true / false |
set_cell( table row column value layer ) | Queue one table cell as a row delta (TABLE.__delta.row.COLUMN), row 1-based |
set_row( table row json layer ) | Queue several cells of one row from a JSON object ({"QTY":3,"NAME":"Pen"}), one row delta |
select_row( table row column selected layer ) | Select / deselect a row — a boolean in the column its items bind selected to |
close_layer( layer ) | Close a layer in the browser only (_event_client( cs_event-popup_close )) — no roundtrip |
click( event t_arg layer ) | Fire an event, send the pending edits of the firing layer's model, replay the response |
set_check_events( val ) | Opt in: click( ) raises for an event that is not wired in the firing layer |
layer is optional everywhere: an edit goes into, and an event is fired from,
the topmost open layer unless another one is named.
The setters, close_layer( ), click( ) and set_check_events( ) return the
instance, so a whole session fits in one statement:
DATA(msg) = z2ui5_cl_frontend_simulator=>start( `Z2UI5_CL_FRONTEND_SIM_EXAMPLE`
)->set_value( name = `MV_NAME`
value = `World`
)->click( `GREET`
)->get_message( ).
Typed edits
The browser does not send text only: a CheckBox writes a boolean into its
model, a MultiComboBox an array of keys, a StepInput a number.
set_json( path json ) queues exactly such a value; set_value( ),
set_bool( ), set_cell( ), set_row( ) and select_row( ) are shorthands
for it, so every edit takes the same way:
sim->set_bool( `MV_ACTIVE`
)->set_json( path = `MT_KEY`
json = `["A","C"]`
)->set_value( name = `MS_ORDER/NAME`
value = `Max`
)->select_row( table = `MT_ROW`
row = 2
column = `SELKZ`
)->click( `SAVE` ).
- An edit goes into the model of its layer — the own copy of an open popup
or popover, else the MAIN model the nested views share — and travels with
the next event of that layer. An event of another layer leaves it pending;
closing its layer drops it. This is the frontend's rule
(
View1 _pickModelForRoundtrip). - The delta is the frontend's (
Lib.buildDeltaFromPaths), built from the layer's model with every edit applied: a table cell (/TAB/1/COL, nested/TAB/1/SUB/0/COL) goes out as a row deltaTAB.__delta.1.COL, every other path — a scalar, a structure field, an array element, a cell of a table inside a structure — as the whole top-level attribute. A whole value queued next to a delta of the same attribute wins, a later edit of a path replaces the earlier one. - Model paths count rows from 0, as the frontend records them
(
/MS_ORDER/T_POS/1/QTYis the second position).set_cell( ),set_row( )andselect_row( )take ABAP's 1-based row. - What went out stays in the layer's model, as in the browser — the
backend leaves an unchanged model out of its response, so
get_value( )reads the sent value back afterwards. A pending edit is not visible there. get_request_json( )shows the request as it went out, the delta included.
Reading the screen
| Method | Purpose |
|---|---|
get_view( layer ) | View XML of the topmost open layer (popover, else popup, else the main view) — or of the given one |
get_popup( ) / get_popover( ) | XML of the open popup / popover, empty when closed |
get_layer( ) | Name of the topmost open layer: POPOVER, POPUP or MAIN |
get_layers( ) | Every open layer — MAIN, NEST, NEST2, POPUP, POPOVER — with view XML, JSON model, owning app and anchor control |
get_model( layer ) | JSON model of the topmost (or given) layer |
get_value( name layer ) | A value from the model of the topmost (or given) layer, falling back to the last model the server sent |
get_events( layer ) | Events wired in the active (or given) layer |
check_event_exists( event layer ) | Whether an event is wired in the active (or given) layer |
get_ids( layer ) | Control ids (id="...") the app wrote into the active (or given) layer |
check_id_exists( id layer ) | Whether a control with this id is in the active (or given) layer - the assertion a recorded journey makes on the screen it reached |
get_message( ) | Text of the last toast / message box of the last roundtrip |
get_messages( only_last ) | All messages of the session: source (toast / box), type (success / info / warning / error), raw MessageBox method, text, title, details, roundtrip |
get_actions( ) | Follow-up actions of the last response (SET_FOCUS, CONTROL_BY_ID, ...) with their arguments |
get_nav( ) | Browser-history intent of the last response (app-state hash, push state, routing mode, nav-back) — read-only |
get_app( ) | App class that answered the last roundtrip — after nav_app_call( ) the called one |
get_id( ) | Current draft id |
is_sticky( ) | Whether the app runs in a held stateful session (no draft, cannot be resumed) |
get_state( ) | What the browser knows and the draft does not, as JSON — for resume( ) |
get_roundtrip( ) | Number of roundtrips of this instance |
get_response_json( ) | Raw response JSON of the last roundtrip |
get_request_json( ) | Raw request JSON of the last roundtrip, with the model delta as it went out |
Layers
The frontend has five view slots, and the simulator keeps them the same way:
MAIN— the page. A new main view is a new screen: it takes the nested views, the popup and the popover down with it.NEST,NEST2— nested views inserted into a control of the main view (anchor_id). They own no model;get_layers( )reports the main model they inherit.POPUP,POPOVER— standalone, on top of the page, each with its own copy of the model. A popover remembers the control it opens by (anchor_id). Both die when the response names another app.
A response's model is pushed into every open layer that belongs to the app that answered — exactly the frontend's rule, so a popup of a called app does not see the caller's model.
A layer the browser closes on its own — _event_client( cs_event-popup_close )
— is closed with close_layer( ): no roundtrip, the backend is not told, the
layer and the unsent edits of its model are gone, and get_state( ) no longer
carries it.
Event validation
check_event_exists( ) and get_events( ) read the event wires
(.eB(['NAME',...])) out of the active layer: an open popover, else an open
popup — dialogs are modal — else the main view with its nested views. With
set_check_events( ) on, click( ) refuses an event the user could not reach
and names the ones they could. A click( ) that names the MAIN layer while a
popup is open is refused as well — a dialog is modal. It is a check on the
view XML, not on a control tree: visibility and enablement are not evaluated,
and events raised by timers or keyboard shortcuts are not wired in a view.
get_ids( ) and check_id_exists( ) read the id="..." attributes of the
same layers the same way - only the ids the app wrote into its view, the
backend generates none. They are what the journey recorder of abap2UI5's
developer tools (Ctrl+F12, Roundtrips > Journey) asserts with: it records a
click-through in the browser and exports an ABAP Unit test that replays it on
this simulator, checking the ids and events the recorded screens showed.
Resuming a session
Every start( ) / click( ) is one roundtrip, and the draft id is the whole
backend state of a draft-based app — which is what lets a session continue in
another request, another work process:
" request 1
DATA(sim) = z2ui5_cl_frontend_simulator=>start( `Z2UI5_CL_FRONTEND_SIM_LAYERS` ).
sim->click( `POPUP_OPEN` ).
DATA(id) = sim->get_id( ).
DATA(state) = sim->get_state( ).
" request 2
sim = z2ui5_cl_frontend_simulator=>resume( id = id state = state ).
sim->set_value( name = `MV_POPUP_TEXT` value = `Bea` )->click( `POPUP_CONFIRM` ).
What resume( ) does and does not do:
- It sends what the browser sends. No roundtrip happens on
resume( ); the nextclick( )posts the draft id, the pending delta and the event, and the backend restores the app from the draft. Nothing else is needed on the backend side. - The view comes from
state. The backend does not keep view XML — the browser does. Pass theget_state( )string of the previous request and the layers (XML and model per layer) are back. A state that belongs to another draft id is refused. - Without
statethe instance knows the app class and the model from the draft, but no view and no layer:get_value( )works,get_view( )is empty, andset_check_events( )would refuse every event. refresh = abap_truere-requests the view with the request a browser reload sends for a routed app (#/app/<class>/<draft>): the app runscheck_on_navigated( )and, written the canonical way, displays its main view again. A popup is back only if the app re-opens it there. The restore mints a new draft id.- Sticky apps cannot be resumed. A stateful session
(
client->set_session_stateful( )) keeps the app in the handler, not in a draft — the id it reports has no draft behind it, andresume( )raises. Within one instance a sticky app works normally (is_sticky( )tells). - The draft must still exist and belong to the current user — the draft service binds drafts to their creator and expires them after a few hours.
Each start( ) / click( ) is exactly one roundtrip. Sticky apps keep their
handler between roundtrips, draft-based apps chain their state through the
persisted id — the same distinction z2ui5_cl_ui5_http_handler=>_http_post
makes, so what you test is what runs in production.
Contents
| Object | Description |
|---|---|
z2ui5_cl_frontend_simulator | The simulator |
z2ui5_cl_frontend_sim_example | Minimal app — one input, one text, two buttons — that exists to be driven by it |
z2ui5_cl_frontend_sim_layers | Example app with every layer: popup, popover, nested view, an editable table, follow-up actions, a message box, app-state hash, nav_app_call( ) and the switch to a sticky session |
z2ui5_cl_frontend_sim_form | Example app for typed edits: a CheckBox, a MultiComboBox, a structure that holds a table, a selectable table with a nested table in its rows |
The simulator's own unit tests live in
z2ui5_cl_frontend_simulator.clas.testclasses.abap and drive both example
apps and the form app. They double as the usage documentation:
ltcl_frontend_simulator(RISK LEVEL HARMLESS) installs an in-memory draft store through the core's store seam (z2ui5_cl_ui5_srv_draft=>set_instance), so even the draft path writes nothing to the database.ltcl_frontend_simulator_db(RISK LEVEL DANGEROUS) runs against the real store —Z2UI5_T_01and its commits — including the rollback bracket the framework puts aroundmain( )in the draft path, and deletes every draft it created inteardown.
Installation
Install with abapGit. Requires
abap2UI5 in the same system, at a
version that speaks wire protocol 2 (S_FRONT.S_ACTION) — a response that
names another protocol is refused with PROTOCOL_MISMATCH. Runs on ABAP
Standard, ABAP Cloud and — via the downport in CI — 7.02.
The simulator works on the core's engine (z2ui5_cl_ui5_handler and the draft
service), which is not part of abap2UI5's released API in src/02 — a core
change there can require a change here.
Current scope
Covered: the main view, nested views, popup and popover with their models, messages with severity, follow-up actions, the browser-history intent, typed edits (booleans, arrays, numbers, structures with tables) with the frontend's delta per layer model, table row deltas including nested tables, row selection, closing a layer in the browser, sticky and draft sessions, app-to-app navigation, resuming a session across requests, and an opt-in check that an event is wired in the active layer.
Not covered yet: an injectable frontend CONFIG (device, UI5 version, focus,
scroll), a control tree with visibility and enablement, and
set_value( ) validation against the bound paths. ROADMAP.md
has the plan and the order.
Contributing
See CONTRIBUTING.md. Every change must leave abaplint at
zero issues.
License
MIT — see LICENSE.
Others providing
Compare all 21 on the Unit testing page →- @abap2ui5/mcp-server@abap2ui5/mcp-serverAn MCP server that puts the abap2UI5 development loop on the machine an agent runs on: check the view the ABAP builds, photograph it, deploy the class into a sandbox, boot the app headless and read the errors.community org, abapMCP server for the abap2UI5 check, deploy and run loop
- assertassertA fluent assertion helper for ABAP Unit tests that wraps cl_abap_unit_assert in chained calls such as assert(x)->equals(y), also usable outside tests through ZCL_ASSERTABLE.independent, abapFluent assertion helper for ABAP Unit tests
- @abaplint/transpiler@abaplint/transpilerTranspiles ABAP 7.02 syntax to ES6 JavaScript, so ABAP code and its unit tests run on Node.js in CI without an ABAP system, with optional SQLite, PostgreSQL, Snowflake or SAP HANA drivers.community org, abapTranspiles ABAP to JavaScript to run on Node.js
- Modernization Agents for SAP on AWSModernization Agents for SAP on AWSAWS's suite of agents for SAP modernisation work: custom ABAP converted for S/4HANA, PI/PO interfaces moved to Integration Suite, BW objects moved to SAP Datasphere, deployed into a customer's own AWS account and reached only after registration.partner, abapAWS agents for ABAP, PI/PO and BW modernisation work
- abap_db_preparatorabap_db_preparatorA wrapper over ABAP's OpenSQL replacement service that exports chosen rows into a stored bundle and, in a unit test's setup, replays it as fake table content in two method calls.independent, abapReplays stored database snapshots in ABAP unit tests
- cacamber-BDD-for-ABAPcacamber-BDD-for-ABAPA BDD framework for ABAP that turns Gherkin-style Given/When/Then scenarios into ABAP Unit test steps, matching each step against a regular expression and calling the test class method it maps to.independent, abapBDD framework turning Given/When/Then into ABAP Unit steps
- abap-utesthelpabap-utesthelpTwo helper classes for ABAP Unit tests: one verifies a domain constant class against its domain's fixed values, the other creates test doubles without naming the interface.independent, abapHelper classes for ABAP Unit tests and test doubles
- regression_testregression_testA snapshot library for ABAP that records a value once and compares against it on every later run, from either the debugger or a unit test, storing snapshots in eCATT test data containers.independent, abapSnapshot testing library for ABAP, from debugger or unit test
- abapCovabapCovA report, ZABAPCOV, that uploads ABAP Unit test code coverage results to codecov.io, once the repository is installed through abapGit and codecov.io's certificates through STRUST.independent, abapUploads ABAP Unit code coverage to codecov.io
- ABAP remote filesystemABAP remote filesystemA Visual Studio Code extension that opens an ABAP system over ADT as an editable filesystem and exposes system tools to GitHub Copilot and external MCP clients.independent, abapVS Code extension editing ABAP systems over ADT as a filesystem
- open-steamgateopen-steamgateAn ABAP application server you clone and run on your own machine: transpiled ABAP serving classic /IWBEP/ OData v2 to Fiori over a local database, with no SAP system attached.independent, abapLocal ABAP server serving /IWBEP/ OData v2 without an SAP system
- express-icf-shimexpress-icf-shimA shim that runs an ABAP ICF handler written against if_http_extension behind an Express 4.x server, for ABAP transpiled to JavaScript and run on Node.js in testing and continuous integration.community org, abapShim running an ABAP ICF handler behind an Express 4.x server
- open-abap-coreopen-abap-coreReusable ABAP functionality for code transpiled by @abaplint/transpiler: the core ABAP artifacts that code runs against on Node.js.community org, abapCore ABAP artifacts for code transpiled by @abaplint/transpiler
- mockup_loadermockup_loaderA tool that loads ABAP unit test data prepared in Excel as TAB-delimited text into ABAP structures and tables, and generates interface stubs that return that mock data.independent, abapLoader of Excel-prepared ABAP unit test data, with stub generation
- mockAmockAA free mocking framework for ABAP, published under the Apache License 2.0.independent, abapA free mocking framework for ABAP, published under the Apache License 2.0.
- Vise_for_ABAPVise_for_ABAPA characterisation-test helper: ZCL_VISE=>GRIP records a field's value once and fails a later run if it has changed.independent, abapA characterisation-test helper: `ZCL_VISE=>GRIP` records a field's value once and fails a later run if it has changed.
- abap-senchaabap-senchaA BDD/TDD assertion library for ABAP Unit, styled after the JavaScript library Chai.js, that wraps cl_abap_unit_assert in chainable expect, should and assert styles.independent, abapBDD/TDD assertion library for ABAP Unit, styled after Chai.js
- abap-test-containerabap-test-containerA test data container for ABAP Cloud that loads unit test data in JSON, XML or raw formats from GitHub and maps it onto the target structure, filling the platform's missing native one.independent, abapGitHub-hosted test data container for ABAP Cloud
- abap-unit-runnerabap-unit-runnerAn application job that runs and schedules ABAP Unit tests in SAP's ABAP Environment through the SAP_COM_0735 API, where the on-premise test runner has no SAP GUI to run from.independent, abapApplication job that runs and schedules ABAP Unit tests in the cloud
- adash-servicesadash-servicesThe backend service behind ADASH Cli, exposing ABAP Unit test runs and coverage as REST and OData services for CI/CD pipelines, with a report scheduled to run tests per package.independent, abapBackend exposing ABAP Unit runs and coverage over REST and OData
Details
Sources
Sources are graph entities: the same page may back several entries, and an unreachable source flags every row that rests on it.
Compatibility and provenance
Flavors
“Runs on ABAP Standard, ABAP Cloud and — via the downport in CI — 7.02.” — abap2UI5/headless-frontend — README
“Runs on ABAP Standard, ABAP Cloud and — via the downport in CI — 7.02.” — abap2UI5/headless-frontend — README
Contributors
History in the atlas
- First seen.
For your agent
- Ref
artifact:abap2ui5-headless-frontend- JSON
- /artifacts/abap2ui5-headless-frontend.json
- MCP
- Connecting an agent