Skip to main content

Component Testing

How to test individual UI5 components in isolation using Playwright's webServer option with ui5 serve. Covers setup, component bootstrapping, and isolated testing patterns.

What Is Component Testing?​

Component testing runs a single UI5 component (a view, fragment, or reusable control) outside the full Fiori Launchpad. This approach provides:

  • Speed: No FLP shell, no authentication, no full app bootstrap
  • Isolation: Test one component without side effects from others
  • Reliability: No backend dependency when combined with OData mocks
  • Parallelism: Multiple workers can test different components simultaneously

Prerequisites​

  • UI5 Tooling CLI: npm install --save-dev @ui5/cli
  • A UI5 app with ui5.yaml configuration
  • Playwright with Praman

Project Setup​

UI5 App Structure​

my-ui5-app/
webapp/
Component.js
manifest.json
view/
Main.view.xml
controller/
Main.controller.js
test/
component/
index.html # Component test harness
ui5.yaml
playwright.config.ts

Component Test Harness​

Create a minimal HTML page that bootstraps just the component under test:

<!-- webapp/test/component/index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Component Test</title>
<script
id="sap-ui-bootstrap"
src="https://sdk.openui5.org/resources/sap-ui-core.js"
data-sap-ui-theme="sap_horizon"
data-sap-ui-libs="sap.m, sap.ui.comp"
data-sap-ui-compatVersion="edge"
data-sap-ui-resourceroots='{ "my.app": "../../" }'
data-sap-ui-async="true"
></script>
<script>
sap.ui.require(['sap/ui/core/ComponentContainer'], function (ComponentContainer) {
new ComponentContainer({
name: 'my.app',
async: true,
settings: {},
height: '100%',
}).placeAt('content');
});
</script>
</head>
<body>
<div id="content"></div>
</body>
</html>

Playwright Config with ui5 serve​

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
testDir: './tests/component',
timeout: 30_000,

webServer: {
command: 'npx ui5 serve --port 8080',
port: 8080,
reuseExistingServer: !process.env.CI,
timeout: 60_000,
},

use: {
baseURL: 'http://localhost:8080',
},

projects: [
{
name: 'component-tests',
use: { ...devices['Desktop Chrome'] },
},
],
});

Writing Component Tests​

Basic Component Render Test​

import { test, expect } from 'playwright-praman';

test.describe('Main View Component', () => {
test.beforeEach(async ({ page }) => {
// Navigate to the component test harness
await page.goto('/test/component/index.html');

// Wait for UI5 to fully bootstrap
await page.waitForFunction(() => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
return window.sap?.ui?.getCore()?.isInitialized() === true;
});
});

test('main view renders correctly', async ({ ui5 }) => {
await test.step('page title is displayed', async () => {
const title = await ui5.control({
controlType: 'sap.m.Title',
properties: { text: 'Purchase Orders' },
});
expect(title).toBeTruthy();
});

await test.step('table is present', async () => {
const table = await ui5.control({
controlType: 'sap.m.Table',
});
expect(table).toBeTruthy();
});

await test.step('create button exists', async () => {
const btn = await ui5.control({
controlType: 'sap.m.Button',
properties: { text: 'Create' },
});
expect(btn).toBeTruthy();
});
});
});

Component with Mocked OData​

Combine component testing with OData mocking for fully offline tests:

import { test, expect } from 'playwright-praman';

test.describe('Purchase Order List Component (Offline)', () => {
test.beforeEach(async ({ page }) => {
// Mock OData before loading the component
await page.route('**/odata/**', async (route) => {
const url = route.request().url();

if (url.includes('$metadata')) {
await route.fulfill({
status: 200,
contentType: 'application/xml',
body: '<edmx:Edmx Version="1.0"></edmx:Edmx>',
});
} else if (url.includes('PurchaseOrders')) {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
d: {
results: [
{ PurchaseOrder: '4500000001', Supplier: '100001', Status: 'Approved' },
{ PurchaseOrder: '4500000002', Supplier: '100002', Status: 'Pending' },
],
},
}),
});
} else {
await route.continue();
}
});

await page.goto('/test/component/index.html');
});

test('displays purchase orders from mock data', async ({ ui5, ui5Matchers }) => {
const table = await ui5.control({ controlType: 'sap.m.Table' });
await ui5Matchers.toHaveRowCount(table, 2);
});
});

Testing Individual Views​

Test a single view without the full Component bootstrap:

<!-- webapp/test/component/view-test.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<script
id="sap-ui-bootstrap"
src="https://sdk.openui5.org/resources/sap-ui-core.js"
data-sap-ui-theme="sap_horizon"
data-sap-ui-libs="sap.m"
data-sap-ui-resourceroots='{ "my.app": "../../" }'
data-sap-ui-async="true"
></script>
<script>
sap.ui.require(['sap/ui/core/mvc/XMLView'], function (XMLView) {
XMLView.create({
viewName: 'my.app.view.Detail',
}).then(function (view) {
view.placeAt('content');
});
});
</script>
</head>
<body>
<div id="content"></div>
</body>
</html>
test.describe('Detail View (Isolated)', () => {
test('detail view renders form fields', async ({ page, ui5 }) => {
await page.goto('/test/component/view-test.html');

const inputs = await ui5.controls({
controlType: 'sap.m.Input',
});
expect(inputs.length).toBeGreaterThan(0);
});
});

Testing Fragments​

<!-- webapp/test/component/fragment-test.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<script
id="sap-ui-bootstrap"
src="https://sdk.openui5.org/resources/sap-ui-core.js"
data-sap-ui-theme="sap_horizon"
data-sap-ui-libs="sap.m"
data-sap-ui-resourceroots='{ "my.app": "../../" }'
data-sap-ui-async="true"
></script>
<script>
sap.ui.require(['sap/ui/core/Fragment'], function (Fragment) {
Fragment.load({
name: 'my.app.view.AddressForm',
type: 'XML',
}).then(function (fragment) {
fragment.placeAt('content');
});
});
</script>
</head>
<body>
<div id="content"></div>
</body>
</html>

Testing Controller Logic​

Use component tests to validate controller behavior in a real browser:

test('controller handles form validation', async ({ page, ui5 }) => {
await page.goto('/test/component/index.html');

await test.step('submit empty form triggers validation', async () => {
await ui5.click({
controlType: 'sap.m.Button',
properties: { text: 'Save' },
});

// Check for validation error state on required fields
const errorInputs = await ui5.controls({
controlType: 'sap.m.Input',
properties: { valueState: 'Error' },
});
expect(errorInputs.length).toBeGreaterThan(0);
});

await test.step('filling required fields clears errors', async () => {
await ui5.fill({ controlType: 'sap.m.Input', id: /nameInput/ }, 'Test Value');

const nameInput = await ui5.control({
controlType: 'sap.m.Input',
id: /nameInput/,
});
const valueState = await nameInput.getProperty('valueState');
expect(valueState).toBe('None');
});
});

CI Integration​

Component Test Workflow​

# .github/workflows/component-tests.yml
name: Component Tests
on: [push, pull_request]

jobs:
component-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx playwright install chromium
- run: npx playwright test --project=component-tests
- uses: actions/upload-artifact@v4
if: failure()
with:
name: component-test-report
path: playwright-report/

Component Testing vs E2E Testing​

AspectComponent TestE2E Test
ScopeSingle view/componentFull app + backend
SpeedFast (seconds)Slow (minutes)
AuthNone neededSetup project required
DataMocked (page.route)Real SAP backend
CIEvery pushNightly or pre-release
ParallelismHigh (4+ workers)Low (1 worker for stateful)
FailuresLogic/rendering bugsIntegration issues

Best Practices​

  • One harness per component: Keep test HTML files focused on a single component.
  • Mock at the OData level: Let the UI5 model parse mock data for realistic behavior.
  • Reuse mock factories: Share mock data creation across component and E2E tests.
  • Test interactions, not DOM: Use Praman selectors and matchers, not CSS selectors.
  • Run in CI on every push: Component tests are fast enough for PR gates.