Install
$ agentstack add skill-efeumutaslan-sap-skills-sap-fiori-testing ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
SAP Fiori Testing, Accessibility & Modern UI
Related Skills
sap-testing-quality— Cross-cutting SAP testing strategysap-devops-cicd— UI test automation in pipelinessap-build-apps— Alternative low-code UI approach
Quick Start
Choose your test framework:
| Framework | Level | Speed | Best For | |-----------|-------|-------|----------| | QUnit | Unit | Fast | Controller logic, formatters | | OPA5 | Integration | Medium | UI interaction flows, navigation | | wdi5 | E2E | Slow | Full app testing, cross-app | | UIVeri5 | Visual/E2E | Slow | Visual regression, screenshots |
Quick wdi5 setup:
npm init wdi5@latest
# Follow prompts: select UI5 app, choose test runner
# Generates: wdio.conf.js + test structure
Core Concepts
UI5 Test Pyramid
/\
/ \ E2E (wdi5/UIVeri5)
/ \ - Full app in browser
/------\ - Real backend or mock server
/ \
/ OPA5 \ Integration
/ Tests \ - UI interaction + navigation
/--------------\ - Mock server for data
/ \
/ QUnit Unit Tests \ Unit
/ \ - Pure logic, no DOM
/____________________\
wdi5 Architecture
Test Script ──► wdi5 Bridge ──► UI5 Control API
│ │ │
WebdriverIO Injects JS sap.ui.test.*
(Selenium/ into browser control selectors
Playwright)
OPA5 Key Concepts
| Concept | Role | Example | |---------|------|---------| | Page Object | Encapsulates page interactions | onTheMainPage.iClickCreate() | | Journey | Test scenario (sequence of actions) | opaTest("Create order", ...) | | Arrangement | Setup state | Given.iStartMyApp() | | Action | User interaction | When.onTheList.iSelectItem("001") | | Assertion | Verify outcome | Then.onTheDetail.iSeeTitle("Order 001") |
Common Patterns
Pattern 1: wdi5 Test with UI5 Selectors
// test/e2e/specs/order.test.js
const { wdi5 } = require('wdio-ui5-service');
describe('Order Management', () => {
before(async () => {
await browser.url('#/Orders');
await browser.asControl({
selector: { controlType: 'sap.m.Page', viewName: 'orders.List' }
});
});
it('should display order list', async () => {
const table = await browser.asControl({
selector: {
controlType: 'sap.m.Table',
viewName: 'orders.List',
id: 'orderTable'
}
});
const items = await table.getItems();
expect(items.length).toBeGreaterThan(0);
});
it('should create new order', async () => {
// Click Create button
const createBtn = await browser.asControl({
selector: {
controlType: 'sap.m.Button',
viewName: 'orders.List',
properties: { text: 'Create' }
}
});
await createBtn.press();
// Fill form
const customerInput = await browser.asControl({
selector: {
controlType: 'sap.m.Input',
viewName: 'orders.Create',
id: 'customerInput'
}
});
await customerInput.setValue('CUST001');
const amountInput = await browser.asControl({
selector: {
controlType: 'sap.m.Input',
viewName: 'orders.Create',
id: 'amountInput'
}
});
await amountInput.setValue('1500.00');
// Save
const saveBtn = await browser.asControl({
selector: {
controlType: 'sap.m.Button',
viewName: 'orders.Create',
properties: { type: 'Emphasized' }
}
});
await saveBtn.press();
// Verify success message
const msgStrip = await browser.asControl({
selector: {
controlType: 'sap.m.MessageStrip',
properties: { type: 'Success' }
}
});
const text = await msgStrip.getText();
expect(text).toContain('created successfully');
});
});
// wdio.conf.js
exports.config = {
specs: ['./test/e2e/specs/**/*.test.js'],
maxInstances: 1,
capabilities: [{
browserName: 'chrome',
'goog:chromeOptions': {
args: ['--headless', '--no-sandbox', '--disable-gpu']
}
}],
baseUrl: 'http://localhost:8080',
services: ['ui5'],
ui5: {
path: { webapp: 'webapp' },
url: 'http://localhost:8080/index.html'
},
framework: 'mocha',
mochaOpts: { timeout: 60000 }
};
Pattern 2: OPA5 Integration Test
// test/integration/pages/OrderList.js
sap.ui.define([
'sap/ui/test/Opa5',
'sap/ui/test/actions/Press',
'sap/ui/test/actions/EnterText',
'sap/ui/test/matchers/Properties',
'sap/ui/test/matchers/AggregationLengthEquals'
], function (Opa5, Press, EnterText, Properties, AggregationLengthEquals) {
'use strict';
Opa5.createPageObjects({
onTheOrderListPage: {
actions: {
iSearchFor: function (sQuery) {
return this.waitFor({
id: 'searchField',
viewName: 'orders.List',
actions: new EnterText({ text: sQuery }),
errorMessage: 'Search field not found'
});
},
iPressCreateButton: function () {
return this.waitFor({
controlType: 'sap.m.Button',
viewName: 'orders.List',
matchers: new Properties({ text: 'Create' }),
actions: new Press(),
errorMessage: 'Create button not found'
});
},
iSelectFirstItem: function () {
return this.waitFor({
controlType: 'sap.m.ColumnListItem',
viewName: 'orders.List',
actions: new Press(),
errorMessage: 'No list items found'
});
}
},
assertions: {
iShouldSeeTheTable: function () {
return this.waitFor({
id: 'orderTable',
viewName: 'orders.List',
success: function () { Opa5.assert.ok(true, 'Table is visible'); },
errorMessage: 'Table not found'
});
},
theTableShouldHaveEntries: function (iCount) {
return this.waitFor({
id: 'orderTable',
viewName: 'orders.List',
matchers: new AggregationLengthEquals({
name: 'items', length: iCount
}),
success: function () {
Opa5.assert.ok(true, 'Table has ' + iCount + ' entries');
},
errorMessage: 'Table does not have ' + iCount + ' entries'
});
}
}
}
});
});
// test/integration/OrderJourney.js
sap.ui.define([
'sap/ui/test/opaQunit',
'./pages/OrderList'
], function (opaTest) {
'use strict';
QUnit.module('Order Management');
opaTest('Should see order list on start', function (Given, When, Then) {
Given.iStartMyApp();
Then.onTheOrderListPage.iShouldSeeTheTable();
});
opaTest('Should filter orders by search', function (Given, When, Then) {
When.onTheOrderListPage.iSearchFor('CUST001');
Then.onTheOrderListPage.theTableShouldHaveEntries(3);
});
opaTest('Should navigate to create page', function (Given, When, Then) {
When.onTheOrderListPage.iPressCreateButton();
Then.onTheCreatePage.iShouldSeeTheForm();
Then.iTeardownMyApp();
});
});
Pattern 3: Fiori Elements OPA5 Test
// test/integration/FEListReportJourney.js
sap.ui.define([
'sap/ui/test/opaQunit',
'sap/fe/test/ListReport',
'sap/fe/test/ObjectPage'
], function (opaTest, ListReport, ObjectPage) {
'use strict';
var oListReport = new ListReport({ entitySet: 'Orders' });
var oObjectPage = new ObjectPage({ entitySet: 'Orders' });
QUnit.module('Fiori Elements - List Report');
opaTest('Table loads with data', function (Given, When, Then) {
Given.iStartMyFLPApp('orders-manage');
Then.onTheListReport.iSeeTheTable();
Then.onTheListReport.iCheckRows(10);
});
opaTest('Filter by status', function (Given, When, Then) {
When.onTheListReport.iOpenFilterBar();
When.onTheListReport.iFilterByField('Status', 'Open');
When.onTheListReport.iExecuteFilter();
Then.onTheListReport.iCheckRows(5);
});
opaTest('Navigate to detail', function (Given, When, Then) {
When.onTheListReport.iPressRow(0);
Then.onTheObjectPage.iSeeThisPage();
Then.onTheObjectPage.iSeeHeaderTitle('ORD-001');
Then.iTeardownMyApp();
});
});
Pattern 4: WCAG 2.1 Accessibility Checklist
// Accessibility patterns for Fiori apps
// 1. Labels for all inputs
// BAD:
new sap.m.Input({ placeholder: "Enter name" });
// GOOD:
new sap.m.Label({ text: "Customer Name", labelFor: "nameInput" });
new sap.m.Input({ id: "nameInput" });
// 2. ARIA for custom controls
new sap.m.GenericTile({
header: "Revenue",
ariaLabel: "Revenue tile showing 1.5 million euros",
press: function() { /* navigate */ }
});
// 3. High contrast support — use semantic colors
// BAD: custom CSS colors
// GOOD: use UI5 semantic classes
// .sapMObjStatusActive (standard active state)
// sap.ui.core.IconColor.Positive / Negative / Critical
// 4. Keyboard navigation
// Ensure all interactive elements are reachable via Tab
// Use sap.m.Table not custom HTML for data grids
// Test with screen reader (JAWS, NVDA, VoiceOver)
// 5. Color-independent information
// BAD: status shown only by color
// GOOD: status shown by color + icon + text
new sap.m.ObjectStatus({
text: "Approved",
state: "Success",
icon: "sap-icon://accept"
});
Pattern 5: UI5 TypeScript Migration
// webapp/controller/OrderList.controller.ts
import Controller from "sap/ui/core/mvc/Controller";
import JSONModel from "sap/ui/model/json/JSONModel";
import Filter from "sap/ui/model/Filter";
import FilterOperator from "sap/ui/model/FilterOperator";
import MessageToast from "sap/m/MessageToast";
import Event from "sap/ui/base/Event";
import Table from "sap/m/Table";
import ListBinding from "sap/ui/model/ListBinding";
/**
* @namespace orders.controller
*/
export default class OrderList extends Controller {
public onInit(): void {
const oViewModel = new JSONModel({
busy: false,
orderCount: 0
});
this.getView()?.setModel(oViewModel, "view");
}
public onSearch(oEvent: Event): void {
const sQuery = (oEvent.getParameter("query") as string) || "";
const aFilters: Filter[] = [];
if (sQuery) {
aFilters.push(new Filter("CustomerName", FilterOperator.Contains, sQuery));
}
const oTable = this.byId("orderTable") as Table;
const oBinding = oTable.getBinding("items") as ListBinding;
oBinding.filter(aFilters);
}
public onCreatePress(): void {
this.getOwnerComponent()?.getRouter().navTo("create");
}
public async onRefresh(): Promise {
const oTable = this.byId("orderTable") as Table;
const oBinding = oTable.getBinding("items") as ListBinding;
try {
await oBinding.requestRefresh();
MessageToast.show("Data refreshed");
} catch (error) {
MessageToast.show("Refresh failed");
}
}
}
// tsconfig.json for UI5 TypeScript
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "node",
"skipLibCheck": true,
"allowJs": true,
"strict": true,
"strictNullChecks": true,
"baseUrl": ".",
"paths": {
"orders/*": ["webapp/*"]
},
"types": ["@openui5/types"]
},
"include": ["webapp/**/*.ts"]
}
Pattern 6: UI5 Web Components Integration
import "@ui5/webcomponents/dist/Button.js";
import "@ui5/webcomponents/dist/Table.js";
import "@ui5/webcomponents/dist/Input.js";
import "@ui5/webcomponents-fiori/dist/ShellBar.js";
import "@ui5/webcomponents-fiori/dist/SideNavigation.js";
Order ID
Customer
ORD-001
Acme Corp
// React + UI5 Web Components
import { ShellBar, Table, TableColumn, TableRow, TableCell } from '@ui5/webcomponents-react';
function OrderList({ orders }) {
return (
<>
Order ID
Customer
Amount
}>
{orders.map(order => (
{order.id}
{order.customer}
{order.amount}
))}
);
}
Error Catalog
| Error | Context | Root Cause | Fix | |-------|---------|------------|-----| | wdi5: Control not found | wdi5 selector | Wrong controlType or viewName | Use UI5 Inspector to get correct selector | | OPA5: Timeout | waitFor expired | Element not rendered in time | Increase timeout; check if element is conditional | | OPA5: Ambiguous match | Multiple controls match | Selector too broad | Add id, viewName, or more specific matchers | | TS2307: Cannot find module | TypeScript import | Missing @openui5/types or wrong path | Install types: npm i -D @openui5/types | | wdi5: Session not created | Browser start failed | ChromeDriver version mismatch | Update chromedriver or use services: ['chromedriver'] | | FE test: Page not found | Fiori Elements OPA | Wrong entitySet or app ID | Check manifest.json routing config | | a11y: Missing label | Accessibility audit | Input without associated label | Add sap.m.Label with labelFor | | a11y: Low contrast | Color contrast check | Custom theme colors too similar | Use SAP theme parameters; test with High Contrast theme |
Performance Tips
- OPA5 over wdi5 for UI logic — OPA5 runs in-browser (no Selenium overhead); use wdi5 only for cross-app/E2E
- Mock server for OPA5 — Use
sap/ui/core/util/MockServerto eliminate backend dependency - Headless browser — Run wdi5/UIVeri5 headless in CI/CD (Chrome
--headless) - Parallel OPA5 suites — Split OPA test files by module; run in parallel with Karma
- Selective test runs — Use
QUnit.modulefiltering to run only changed test suites - UI5 TypeScript — Compiler catches errors at build time; reduces runtime debugging
Gotchas
- wdi5 vs UIVeri5: wdi5 is community-maintained (active), UIVeri5 is SAP-maintained (slower updates)
- OPA5 auto-wait: OPA5 waits for UI5 rendering automatically; don't add manual
setTimeout - Fiori Elements tests: Use
sap/fe/test/*page objects, not custom selectors for FE apps - TypeScript types:
@openui5/types(open-source) vs@sapui5/types(requires SAP license) - Web Components ≠ UI5:
@ui5/webcomponentsis framework-agnostic; different API fromsap.m.*controls - Accessibility testing: Automated tools catch ~30% of issues; manual screen reader testing is essential
MCP Server Integration
{
"mcpServers": {
"ui5-mcp": {
"command": "npx", "args": ["-y", "@ui5/mcp-server"]
},
"fiori-mcp": {
"command": "npx", "args": ["-y", "@sap-ux/fiori-mcp-server"]
}
}
}
- UI5 MCP (official, 10+ tools): API reference, linting, TS migration guidelines, app scaffolding
- Fiori MCP (official): App generation, page management, annotation editing
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: efeumutaslan
- Source: efeumutaslan/SAP-SKILLS
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.