PackagePackagetypeA library you install and call yourself.See every one →What this type is →by a community orgby a community orgoriginPublished by a community organisation, so it outlives any one maintainer.See every one →

abap2UI5 headless frontend

Plays the frontend's side of the abap2UI5 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

Last pushLicenceMIT

Where it sits

Lives in
Provides
Unit testingABAPUnit testingcapabilityTesting application logic without a running business process — and the test doubles that make dependencies on the database and on framework services tractable.
Add-on to
abap2UI5abap2UI5artifactOne 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.
Requires
abap2UI5abap2UI5artifactThe framework the simulator drives, installed in the same system, at a version that speaks wire protocol 2 — a response naming another protocol is refused with PROTOCOL_MISMATCH.abapGitabapGitartifactInstalling the simulator and its example apps into the system.

Who is behind it

abap2UI5abap2UI5OrganisationMaintains this
In the atlas: maintains 18
@abap2UI5
maintaineroblomov-devPersonContributes to this
In the atlas: maintains 4, contributes to 44
@oblomov-dev
1 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

Connections5

abap2UI5 headless frontend connectionsabap2UI5 headlessfrontendPROVIDES→ provides1provides stated Runs a whole abap2UI5 user session inside an ABAP Unit test — start the app, queue model edits, fire an event, read the view, model, messages and actions back — against the framework's own dispatch, event handling, model serialisation and draft handling, with no server, browser driver or network. Its `HARMLESS` test class installs an in-memory draft store through the core's store seam, so the draft path writes nothing to the database. abap2UI5/headless-frontend — README: https://github.com/abap2ui5/headless-frontend abap2UI5/headless-frontend — README: https://github.com/abap2ui5/headless-frontend abap2UI5/headless-frontend — README: https://github.com/abap2ui5/headless-frontendUnit testingUSED BY← needed by1needed by derived Plays the browser side of the abap2UI5 protocol inside ABAP; every snapshot and every act runs through it. Through: inverted from its requires edge abap-agent-runtime — README, badges: https://github.com/abap2ui5-addons/abap-agent-runtime abap-agent-runtime — README, Architecture: https://github.com/abap2ui5-addons/abap-agent-runtimeabap-agent-runtime← optionally used by1optionally used by derived If: the headless frontend is installed re-running a failed roundtrip's app logic for the Reproduce feature Through: inverted from its requires edge admin-cockpit README: https://github.com/abap2ui5-addons/admin-cockpitadmin-cockpitifNEEDS→ needs2needs stated The framework the simulator drives, installed in the same system, at a version that speaks wire protocol 2 — a response naming another protocol is refused with `PROTOCOL_MISMATCH`. abap2UI5/headless-frontend — README: https://github.com/abap2ui5/headless-frontendabap2UI5needs stated Installing the simulator and its example apps into the system. abap2UI5/headless-frontend — README: https://github.com/abap2ui5/headless-frontendabapGit

README

quoted from abap2UI5/headless-frontend under MIT, read
abap2UI5/headless-frontend/README.mdOn GitHub

abap2UI5 - 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
MethodPurpose
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 delta TAB.__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/QTY is the second position). set_cell( ), set_row( ) and select_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
MethodPurpose
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 next click( ) 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 the get_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 state the instance knows the app class and the model from the draft, but no view and no layer: get_value( ) works, get_view( ) is empty, and set_check_events( ) would refuse every event.
  • refresh = abap_true re-requests the view with the request a browser reload sends for a routed app (#/app/<class>/<draft>): the app runs check_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, and resume( ) 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

ObjectDescription
z2ui5_cl_frontend_simulatorThe simulator
z2ui5_cl_frontend_sim_exampleMinimal app — one input, one text, two buttons — that exists to be driven by it
z2ui5_cl_frontend_sim_layersExample 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_formExample 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_01 and its commits — including the rollback bracket the framework puts around main( ) in the draft path, and deletes every draft it created in teardown.

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.

MIT, read
Unit testing
  1. @abap2ui5/mcp-serverMCP server for the abap2UI5 check, deploy and run loopfrom ABAP
  2. assertFluent assertion helper for ABAP Unit testsfrom ABAP
  3. @abaplint/transpilerTranspiles ABAP to JavaScript to run on Node.jsfrom ABAP
  4. Modernization Agents for SAP on AWSAWS agents for ABAP, PI/PO and BW modernisation workfrom ABAP
  5. abap_db_preparatorReplays stored database snapshots in ABAP unit testsfrom ABAP
  6. cacamber-BDD-for-ABAPBDD framework turning Given/When/Then into ABAP Unit stepsfrom ABAP
  7. abap-utesthelpHelper classes for ABAP Unit tests and test doublesfrom ABAP
  8. regression_testSnapshot testing library for ABAP, from debugger or unit testfrom ABAP
  9. abapCovUploads ABAP Unit code coverage to codecov.iofrom ABAP
  10. ABAP remote filesystemVS Code extension editing ABAP systems over ADT as a filesystemfrom ABAP
  11. open-steamgateLocal ABAP server serving /IWBEP/ OData v2 without an SAP systemfrom ABAP
  12. express-icf-shimShim running an ABAP ICF handler behind an Express 4.x serverfrom ABAP
  13. open-abap-coreCore ABAP artifacts for code transpiled by @abaplint/transpilerfrom ABAP
  14. mockup_loaderLoader of Excel-prepared ABAP unit test data, with stub generationfrom ABAP
  15. mockAA free mocking framework for ABAP, published under the Apache License 2.0.from ABAP
  16. Vise_for_ABAPA characterisation-test helper: `ZCL_VISE=>GRIP` records a field's value once and fails a later run if it has changed.from ABAP
  17. abap-senchaBDD/TDD assertion library for ABAP Unit, styled after Chai.jsfrom ABAP
  18. abap-test-containerGitHub-hosted test data container for ABAP Cloudfrom ABAP
  19. abap-unit-runnerApplication job that runs and schedules ABAP Unit tests in the cloudfrom ABAP
  20. adash-servicesBackend exposing ABAP Unit runs and coverage over REST and ODatafrom ABAP

Details

Sources3
GitHub APIdiscovered herechecked
github.com/abap2UI5/headless-frontendtree, pushes, contributors, CIchecked
README.mdquoted, and what the entry declareschecked

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
discovered viaGitHub API
first seen in the atlas
ecosystem and domainabap, developer-experience
Flavors2
Standard ABAP
“Runs on ABAP Standard, ABAP Cloud and — via the downport in CI — 7.02.” — abap2UI5/headless-frontend — READMEchecked
ABAP Cloud
“Runs on ABAP Standard, ABAP Cloud and — via the downport in CI — 7.02.” — abap2UI5/headless-frontend — READMEchecked
Contributors1

From the repository’s contributor list, .

History in the atlas1
  1. First seen.
For your agent
Ref
artifact:abap2ui5-headless-frontend
JSON
/artifacts/abap2ui5-headless-frontend.json
MCP
Connecting an agent