OData Operations
Praman provides two approaches to OData data access: model operations (browser-side, via
ui5.odata) and HTTP operations (server-side, via ui5.odata). This page covers both,
including V2 vs V4 differences and the ODataTraceReporter.
Two Approaches​
| Approach | Access Path | Use Case |
|---|---|---|
| Model operations | Browser-side UI5 model | Reading data the UI already loaded |
| HTTP operations | Direct HTTP to OData service | CRUD operations, test data setup/teardown |
Model Operations (Browser-Side)​
Model operations query the UI5 OData model that is already loaded in the browser. No additional HTTP requests are made — you are reading the same data the UI5 application is displaying.
getModelData​
Reads data at any model path. Returns the full object tree.
const orders = await ui5.odata.getModelData('/PurchaseOrders');
console.log(orders.length); // Number of loaded POs
const singlePO = await ui5.odata.getModelData("/PurchaseOrders('4500000001')");
console.log(singlePO.Vendor); // '100001'
getModelProperty​
Reads a single property value from the model.
const vendor = await ui5.odata.getModelProperty("/PurchaseOrders('4500000001')/Vendor");
const amount = await ui5.odata.getModelProperty("/PurchaseOrders('4500000001')/NetAmount");
getEntityCount​
Counts entities in a set. Reads from the model, not from $count.
const count = await ui5.odata.getEntityCount('/PurchaseOrders');
expect(count).toBeGreaterThan(0);
waitForODataLoad​
Polls the model until data is available at the given path. Useful after navigation when OData requests are still in flight.
await ui5.odata.waitForODataLoad('/PurchaseOrders');
// Data is now available
const orders = await ui5.odata.getModelData('/PurchaseOrders');
Default timeout: 15 seconds. Polling interval: 100ms.
hasPendingChanges​
Checks if the model has unsaved changes (dirty state).
await ui5.fill({ id: 'vendorInput' }, '100002');
const dirty = await ui5.odata.hasPendingChanges();
expect(dirty).toBe(true);
await ui5.click({ id: 'saveBtn' });
const clean = await ui5.odata.hasPendingChanges();
expect(clean).toBe(false);
fetchCSRFToken​
Fetches a CSRF token from an OData service URL. Needed for write operations via HTTP.
const token = await ui5.odata.fetchCSRFToken('/sap/opu/odata/sap/API_PO_SRV');
// Use token in custom HTTP requests
Named Model Support​
By default, model operations query the component's default model. To query a named model:
const data = await ui5.odata.getModelData('/Products', { modelName: 'productModel' });
HTTP Operations (Server-Side)​
HTTP operations perform direct OData CRUD operations against the service endpoint using Playwright's request context. These are independent of the browser — useful for test data setup, teardown, and backend verification.
queryEntities​
Performs an HTTP GET with OData query parameters.
const orders = await ui5.odata.queryEntities('/sap/opu/odata/sap/API_PO_SRV', 'PurchaseOrders', {
filter: "Status eq 'A'",
select: 'PONumber,Vendor,Amount',
expand: 'Items',
orderby: 'PONumber desc',
top: 10,
skip: 0,
});
createEntity​
Performs an HTTP POST with automatic CSRF token management.
const newPO = await ui5.odata.createEntity('/sap/opu/odata/sap/API_PO_SRV', 'PurchaseOrders', {
Vendor: '100001',
PurchOrg: '1000',
CompanyCode: '1000',
Items: [{ Material: 'MAT-001', Quantity: 10, Unit: 'EA' }],
});
console.log(newPO.PONumber); // Server-generated PO number
updateEntity​
Performs an HTTP PATCH with CSRF token and optional ETag for optimistic concurrency.
await ui5.odata.updateEntity('/sap/opu/odata/sap/API_PO_SRV', 'PurchaseOrders', "'4500000001'", {
Status: 'B',
Note: 'Updated by test',
});
deleteEntity​
Performs an HTTP DELETE with CSRF token.
await ui5.odata.deleteEntity('/sap/opu/odata/sap/API_PO_SRV', 'PurchaseOrders', "'4500000001'");
callFunctionImport​
Calls an OData function import (action).
const result = await ui5.odata.callFunctionImport('/sap/opu/odata/sap/API_PO_SRV', 'ApprovePO', {
PONumber: '4500000001',
});
OData V2 vs V4 Differences​
| Aspect | OData V2 | OData V4 |
|---|---|---|
| Model class | sap.ui.model.odata.v2.ODataModel | sap.ui.model.odata.v4.ODataModel |
| URL convention | /sap/opu/odata/sap/SERVICE_SRV | /sap/opu/odata4/sap/SERVICE/srvd_a2x/... |
| Batch | $batch with changesets | $batch with JSON:API format |
| CSRF token | Fetched via HEAD request with X-CSRF-Token: Fetch | Same mechanism |
| Deep insert | Supported via navigation properties | Supported natively |
| Filter syntax | $filter=Status eq 'A' | $filter=Status eq 'A' (same) |
Praman's model operations work with both V2 and V4 models transparently — the model class handles the protocol differences. HTTP operations use the URL conventions of your service.
OData Trace Reporter​
The ODataTraceReporter captures all OData HTTP requests during test runs and generates a
performance trace. See the Reporters guide for configuration.
The trace output includes per-entity-set statistics:
{
"entitySets": {
"PurchaseOrders": {
"GET": { "count": 12, "avgDuration": 340, "maxDuration": 1200, "errors": 0 },
"POST": { "count": 2, "avgDuration": 890, "maxDuration": 1100, "errors": 0 },
"PATCH": { "count": 1, "avgDuration": 450, "maxDuration": 450, "errors": 0 }
},
"Vendors": {
"GET": { "count": 3, "avgDuration": 120, "maxDuration": 200, "errors": 0 }
}
},
"totalRequests": 18,
"totalErrors": 0,
"totalDuration": 8400
}
Complete Example​
import { test, expect } from 'playwright-praman';
test('verify OData model state after form edit', async ({ ui5, ui5Navigation }) => {
await ui5Navigation.navigateToApp('PurchaseOrder-manage');
// Wait for initial data load
await ui5.odata.waitForODataLoad('/PurchaseOrders');
// Read model data
const orders = await ui5.odata.getModelData('/PurchaseOrders');
expect(orders.length).toBeGreaterThan(0);
// Navigate to first PO
await ui5.click({
controlType: 'sap.m.ColumnListItem',
ancestor: { id: 'poTable' },
});
// Edit a field
await ui5.click({ id: 'editBtn' });
await ui5.fill({ id: 'noteField' }, 'Test note');
// Verify model has pending changes
const dirty = await ui5.odata.hasPendingChanges();
expect(dirty).toBe(true);
// Save
await ui5.click({ id: 'saveBtn' });
// Verify model is clean
const clean = await ui5.odata.hasPendingChanges();
expect(clean).toBe(false);
});
test('seed and cleanup test data via HTTP', async ({ ui5 }) => {
// Setup: create test PO via direct HTTP
const po = await ui5.odata.createEntity('/sap/opu/odata/sap/API_PO_SRV', 'PurchaseOrders', {
Vendor: '100001',
PurchOrg: '1000',
CompanyCode: '1000',
});
// ... run test steps ...
// Cleanup: delete the test PO
await ui5.odata.deleteEntity(
'/sap/opu/odata/sap/API_PO_SRV',
'PurchaseOrders',
`'${po.PONumber}'`,
);
});