# Praman Migration Guides > Step-by-step migration from vanilla Playwright, Selenium WebDriver, wdi5, and Tosca to Praman. This file contains all documentation content in a single document following the llmstxt.org standard. ## Behavioral Equivalence # Behavioral Equivalence (BEHAV-EQ) Praman v1.0 is a ground-up rewrite. Behavioral equivalence testing ensures it produces the same observable results as the reference implementation (wdi5) for core SAP UI5 operations, while documenting intentional divergences. ## Golden Master Methodology Golden master testing (also called characterization testing or approval testing) captures the output of the reference implementation and compares it against the new implementation. ```text ┌──────────────────────┐ ┌──────────────────────┐ │ wdi5 (reference) │ │ Praman (new) │ │ │ │ │ │ Run scenario S │ │ Run scenario S │ │ Capture output O_r │ │ Capture output O_n │ │ │ │ │ └──────────┬───────────┘ └──────────┬────────────┘ │ │ ▼ ▼ ┌─────────────────────────────────────┐ │ Compare O_r vs O_n │ │ ● Match → behavioral equivalence │ │ ● Diff → classify as bug or │ │ intentional divergence │ └─────────────────────────────────────┘ ``` ### What Is Compared For each scenario, the comparison covers: | Aspect | How Compared | | -------------------- | ------------------------------------------------------ | | Control discovery | Same control found (by ID, type, properties) | | Property values | `getValue()`, `getText()`, `getProperty()` match | | Event side effects | Same UI5 events fired (liveChange, change, press) | | OData model state | Same model data after interaction | | Error behavior | Same error for invalid selectors / missing controls | | Timing (qualitative) | Both complete within reasonable time (not exact match) | ### What Is NOT Compared - Exact timing (Praman may be faster or slower) - Internal implementation details (how the bridge communicates) - Log output format (Praman uses pino structured logging, wdi5 uses console) - Error message text (Praman uses structured errors with codes, wdi5 uses plain strings) ## The 8 Parity Scenarios These scenarios cover the core operations that most SAP UI5 test suites rely on. ### Scenario 1: Control Discovery by ID Find a control using its stable ID. ```typescript // wdi5 reference const control = await browser.asControl({ selector: { id: 'container-app---main--saveBtn' } }); // Praman equivalent const control = await ui5.control({ id: 'saveBtn' }); ``` **Comparison**: Both return a proxy wrapping the same DOM element. Verified by comparing `control.getId()` output. **Known divergence**: Praman accepts short IDs (`saveBtn`) and resolves the full ID internally. wdi5 requires the full generated ID including view prefix. This is an intentional improvement. ### Scenario 2: Control Discovery by Type + Properties Find a control using its UI5 type and property values. ```typescript // wdi5 reference const input = await browser.asControl({ selector: { controlType: 'sap.m.Input', properties: { placeholder: 'Enter vendor' }, viewName: 'myApp.view.Main', }, }); // Praman equivalent const input = await ui5.control({ controlType: 'sap.m.Input', properties: { placeholder: 'Enter vendor' }, viewName: 'myApp.view.Main', }); ``` **Comparison**: Both discover the same control. Verified by comparing `getId()` and `getMetadata().getName()`. **Known divergence**: None -- selectors are semantically identical. ### Scenario 3: Property Read (getValue, getText) Read a property from a discovered control. ```typescript // wdi5 reference const value = await input.getValue(); const text = await button.getText(); // Praman equivalent const value = await input.getValue(); const text = await button.getText(); ``` **Comparison**: Return values are strictly equal (`===`). Tested with string, number, boolean, and null properties. **Known divergence**: None -- both call the same UI5 control API methods. ### Scenario 4: Input Fill (enterText + Events) Fill a value into an input control with proper event firing. ```typescript // wdi5 reference await input.enterText('100001'); // Praman equivalent await ui5.fill({ id: 'vendorInput' }, '100001'); ``` **Comparison**: After filling, both produce the same: - `getValue()` returns `'100001'` - `liveChange` event was fired (verified by model binding update) - `change` event was fired (verified by validation trigger) **Known divergence**: Praman fires events in the order `liveChange` then `change`, matching SAP's documented event sequence. wdi5's event ordering depends on the WebDriverIO interaction mode. This is an intentional alignment with SAP documentation. ### Scenario 5: Button Press (firePress / click) Trigger a press action on a button control. ```typescript // wdi5 reference await button.press(); // Praman equivalent await ui5.press({ id: 'submitBtn' }); ``` **Comparison**: Both trigger the button's `press` event, which fires any attached handlers. Verified by checking the UI state change caused by the handler (e.g., dialog opens, navigation occurs). **Known divergence**: Praman uses a three-tier strategy (UI5 firePress -> fireTap -> DOM click). wdi5 uses WebDriverIO's click action. Both produce the same observable outcome, but through different mechanisms. If `firePress` is not available on a control, Praman falls back to DOM click; wdi5 always uses DOM click. ### Scenario 6: Table Row Count and Cell Text Read table dimensions and cell contents. ```typescript // wdi5 reference const rows = await table.getAggregation('items'); const cellText = await rows[0].getCells()[2].getText(); // Praman equivalent await expect(table).toHaveUI5RowCount(5); await expect(table).toHaveUI5CellText(0, 2, 'Expected Value'); ``` **Comparison**: Same row count and cell text values. **Known divergence**: Praman provides dedicated matchers (`toHaveUI5RowCount`, `toHaveUI5CellText`) that are more ergonomic than manual aggregation traversal. The underlying data is identical, but the API surface differs intentionally for readability. ### Scenario 7: Select Dropdown Item Select an item in a `sap.m.Select` or `sap.m.ComboBox`. ```typescript // wdi5 reference await select.open(); const items = await select.getAggregation('items'); await items[2].press(); // Praman equivalent await ui5.select({ id: 'countrySelect' }, 'US'); ``` **Comparison**: After selection, `getSelectedKey()` returns the same value in both. **Known divergence**: Praman accepts the key or display text directly, without requiring manual open/press steps. This is an intentional API simplification. The underlying `setSelectedKey()` / `fireChange()` calls are equivalent. ### Scenario 8: Error on Missing Control Attempt to find a control that does not exist. ```typescript // wdi5 reference const control = await browser.asControl({ selector: { id: 'nonExistentControl' }, }); // wdi5: returns a falsy value or throws after timeout // Praman equivalent const control = await ui5.control({ id: 'nonExistentControl' }); // Praman: throws ControlError with code ERR_CONTROL_NOT_FOUND ``` **Comparison**: Both fail to find the control within the configured timeout. **Known divergence (intentional)**: Praman throws a structured `ControlError` with error code `ERR_CONTROL_NOT_FOUND`, `retryable: true`, and `suggestions[]` array. wdi5 returns a falsy proxy or throws a generic error. Praman's structured errors are an intentional improvement for debuggability. ## Running Parity Tests Parity tests live in a dedicated directory and run both implementations against the same SAP system. ```text tests/ parity/ scenarios/ 01-discovery-by-id.ts 02-discovery-by-type.ts 03-property-read.ts 04-input-fill.ts 05-button-press.ts 06-table-operations.ts 07-select-dropdown.ts 08-missing-control.ts golden-masters/ 01-discovery-by-id.json 02-discovery-by-type.json ... compare.ts ``` ### Generating Golden Masters (wdi5) ```typescript // tests/parity/generate-golden-master.ts interface GoldenMaster { scenario: string; timestamp: string; results: Record; } function saveGoldenMaster(scenario: string, results: Record): void { const master: GoldenMaster = { scenario, timestamp: new Date().toISOString(), results, }; writeFileSync( join(__dirname, 'golden-masters', `${scenario}.json`), JSON.stringify(master, null, 2), ); } // Example: capture discovery result const control = await browser.asControl({ selector: { id: 'container-app---main--saveBtn' }, }); saveGoldenMaster('01-discovery-by-id', { controlId: await control.getId(), controlType: await control.getMetadata().getName(), text: await control.getText(), enabled: await control.getEnabled(), }); ``` ### Comparing Against Golden Masters (Praman) ```typescript // tests/parity/scenarios/01-discovery-by-id.ts interface GoldenMaster { scenario: string; results: { controlId: string; controlType: string; text: string; enabled: boolean; }; } test('parity: discovery by ID matches golden master', async ({ ui5 }) => { const golden: GoldenMaster = JSON.parse( readFileSync(join(__dirname, '../golden-masters/01-discovery-by-id.json'), 'utf-8'), ); const control = await ui5.control({ id: 'saveBtn' }); await test.step('control type matches', async () => { const controlType = await control.getMetadata().getName(); expect(controlType).toBe(golden.results.controlType); }); await test.step('text matches', async () => { const text = await control.getText(); expect(text).toBe(golden.results.text); }); await test.step('enabled state matches', async () => { const enabled = await control.getEnabled(); expect(enabled).toBe(golden.results.enabled); }); // controlId may differ in prefix -- check suffix only (intentional divergence) await test.step('control ID resolves to same element', async () => { const id = await control.getId(); expect(id).toContain('saveBtn'); }); }); ``` ## Divergence Classification Every difference between Praman and wdi5 output must be classified: | Classification | Action Required | | --------------------------- | ----------------------------------------------- | | **Bug** | Fix in Praman to match wdi5 behavior | | **Intentional improvement** | Document in this page with rationale | | **Behavioral equivalent** | Same observable outcome via different mechanism | ### Documented Intentional Divergences | Scenario | Praman Behavior | wdi5 Behavior | Rationale | | ------------------- | ---------------------------------------- | --------------------------------- | ----------------------------------- | | Short ID resolution | Accepts `saveBtn`, resolves full ID | Requires full generated ID | Better DX, less brittle | | Event ordering | `liveChange` then `change` (SAP spec) | Varies by interaction mode | Aligns with SAP documentation | | Press mechanism | Three-tier: firePress -> fireTap -> DOM | DOM click via WebDriverIO | More reliable for UI5 controls | | Error structure | `ControlError` with code + suggestions | Generic error or falsy return | Better debuggability | | Table assertions | Dedicated matchers (`toHaveUI5RowCount`) | Manual aggregation traversal | Ergonomic API for common operations | | Select API | `ui5.select(selector, value)` one-liner | Manual open -> find item -> press | Simplified API, same result | ### How to Add a New Divergence When a parity test reveals a difference: 1. Determine if it is a **bug** or **intentional divergence** 2. If intentional, add a row to the divergence table above with rationale 3. If a bug, file an issue and fix Praman to match wdi5's observable behavior 4. Update the golden master if wdi5's behavior was also wrong (rare -- wdi5 is the reference) ## Running the Full Parity Suite ```bash # Generate golden masters (requires wdi5 + WebDriverIO setup) npx wdio run wdio.conf.ts --spec tests/parity/generate-golden-master.ts # Run Praman parity tests against golden masters npx playwright test tests/parity/scenarios/ # Compare results npx tsx tests/parity/compare.ts ``` The compare script exits with a non-zero code if any undocumented divergence is found. Documented divergences (listed in `divergences.json`) are excluded from the comparison. --- ## Migration from Vanilla Playwright Already using Playwright for web testing? This guide shows when and how to layer Praman on top of your existing Playwright tests for SAP UI5 applications. :::info[In this guide] - Keep all your existing Playwright tests running unchanged with a one-line import swap - Decide when to use `page.locator()` vs `ui5.control()` for each element - Add UI5-aware auto-waiting and stability checks to eliminate flaky tests - Adopt SAP navigation, authentication, and Fiori Elements fixtures incrementally - Run hybrid tests that mix Playwright locators and Praman selectors in the same file ::: ## When to Use `page.locator()` vs `ui5.control()` Praman does not replace Playwright -- it extends it. Use each where it makes sense. ### Use `page.locator()` When - Targeting **non-UI5 elements** (plain HTML, third-party widgets, SAP UI5 Web Components) - Working with **static DOM** that does not change between UI5 versions - Testing **login pages** before the UI5 runtime has loaded - Interacting with **iframes**, **file uploads**, or **browser dialogs** - Asserting on **CSS properties** or **visual layout** ### Use `ui5.control()` When - Targeting **SAP UI5 controls** (sap.m.Button, sap.m.Input, sap.ui.table.Table, etc.) - Control **DOM IDs are generated** and change across versions or deployments - You need **OData binding path** or **i18n text** matching - You need **UI5-level assertions** (value state, enabled, editable, binding) - Working with **SmartFields** that wrap inner controls dynamically - Navigating the **Fiori Launchpad** shell ### Decision Tree ```text Is the element a SAP UI5 control? YES -> Does its DOM structure change across UI5 versions/themes? YES -> Use ui5.control() (stable contract via UI5 registry) NO -> Either works, but ui5.control() is safer long-term NO -> Use page.locator() (standard Playwright) ``` ## Auto-Waiting Differences Both Playwright and Praman auto-wait, but they wait for different things. | Behavior | Playwright `page.locator()` | Praman `ui5.control()` | | ------------- | ----------------------------------------- | ------------------------------------------------------------------------------ | | DOM presence | Waits for element in DOM | Waits for control in UI5 registry | | Visibility | Waits for element visible (actionability) | Prefers visible controls (`preferVisibleControls`) | | UI5 stability | No awareness | Auto-waits for `waitForUI5Stable()` (pending requests, timeouts, promises) | | Retry | Built-in auto-retry on locators | Multi-strategy discovery chain (cache, direct-ID, RecordReplay, registry scan) | | Timeout | `expect.timeout` / `actionTimeout` | `controlDiscoveryTimeout` (default 10s) + `ui5WaitTimeout` (default 30s) | | Network idle | No built-in concept | Blocks WalkMe, analytics, overlay scripts automatically | ### What `waitForUI5Stable()` Does Before every `ui5.control()` call, Praman ensures: 1. The UI5 bootstrap has completed (core libraries loaded) 2. All pending `XMLHttpRequest` / `fetch` calls have settled 3. All JavaScript `setTimeout` / `setInterval` callbacks have fired 4. The OData model has no pending requests 5. A 500ms DOM settle period has elapsed This eliminates the need for `page.waitForTimeout()` (which is banned in Praman) and `page.waitForLoadState('networkidle')` (which is unreliable for SPAs). ## Hybrid Playwright + Praman Test The most practical migration approach is a **hybrid test** that uses both APIs in the same file. Praman's `test` and `expect` are extended versions of Playwright's -- your existing locators continue to work. ```typescript test('hybrid PW + Praman test', async ({ page, ui5, ui5Navigation, sapAuth }) => { // Step 1: Playwright handles login (before UI5 loads) await test.step('Login via SAP login page', async () => { await page.goto(process.env.SAP_CLOUD_BASE_URL!); // Plain Playwright locators for the login form await page.locator('#USERNAME_FIELD input').fill('TESTUSER'); await page.locator('#PASSWORD_FIELD input').fill('secret'); await page.locator('#LOGIN_LINK').click(); // Wait for redirect to FLP await page.waitForURL('**/FioriLaunchpad*'); }); // Step 2: Praman takes over once UI5 is loaded await test.step('Navigate to PO app', async () => { await ui5Navigation.navigateToApp('PurchaseOrder-manage'); }); // Step 3: Use ui5.control() for UI5 controls await test.step('Verify table loaded', async () => { const table = await ui5.control({ controlType: 'sap.m.Table', id: 'poTable', }); await expect(table).toHaveUI5RowCount({ min: 1 }); }); // Step 4: Mix both in the same step when needed await test.step('Check page title and create button', async () => { // Playwright for the browser title await expect(page).toHaveTitle(/Purchase Orders/); // Praman for the UI5 button const createBtn = await ui5.control({ controlType: 'sap.m.Button', properties: { text: 'Create' }, }); await expect(createBtn).toBeUI5Enabled(); }); // Step 5: Playwright for screenshots and traces await test.step('Capture evidence', async () => { await page.screenshot({ path: 'evidence/po-list.png', fullPage: true }); }); }); ``` ## Converting Existing Tests Incrementally You do not need to rewrite everything at once. The recommended approach: ### Phase 1: Drop-In Replacement (5 minutes) Change your import and keep everything else the same. ```typescript // Before // After -- Praman re-exports everything from @playwright/test // All your existing locator-based tests continue to work unchanged test('existing test', async ({ page }) => { await page.goto('/my-app'); await page.locator('#myButton').click(); await expect(page.locator('#result')).toHaveText('Done'); }); ``` ### Phase 2: Add UI5 Fixtures Gradually Start using `ui5` for new assertions alongside existing locators. ```typescript test('gradual adoption', async ({ page, ui5 }) => { await page.goto('/my-app'); // Old way -- still works await page.locator('#myButton').click(); // New way -- more resilient for UI5 controls const result = await ui5.control({ controlType: 'sap.m.Text', properties: { text: 'Done' }, }); await expect(result).toHaveUI5Text('Done'); }); ``` ### Phase 3: Replace Brittle Selectors Find tests that break on UI5 upgrades and convert their selectors. ```typescript // Before: Brittle CSS selector that breaks on theme/version changes await page.locator('.sapMBtnInner.sapMBtnEmphasized').click(); // After: Stable UI5 selector via control registry await ui5.click({ controlType: 'sap.m.Button', properties: { type: 'Emphasized' }, }); ``` :::warning[Common mistake] Do not replace `page.locator()` calls for non-UI5 elements (login forms, iframes, file uploads) with `ui5.control()`. Praman extends Playwright -- it does not replace it. Use `ui5.control()` only for SAP UI5 controls that are registered in the UI5 control registry. For everything else, keep using standard Playwright locators. ::: ### Phase 4: Adopt Navigation and Auth Fixtures Replace manual navigation and login scripts with built-in fixtures. ```typescript // Before: Manual hash navigation await page.goto( 'https://my-system.com/sap/bc/ui5_ui5/ui2/ushell/shells/abap/FioriLaunchpad.html#PurchaseOrder-manage', ); // After: Typed navigation await ui5Navigation.navigateToApp('PurchaseOrder-manage'); ``` ## Parallel Execution Guidance Playwright parallelism works the same with Praman. Each worker gets its own browser context with isolated cookies, storage, and UI5 state. ### Workers and SAP Sessions ```typescript // playwright.config.ts export default defineConfig({ // Each worker authenticates independently via storageState workers: process.env.CI ? 2 : 1, // SAP systems often struggle with high parallelism // Start with 1-2 workers and increase carefully fullyParallel: false, // Run tests within a file sequentially retries: 1, projects: [ { name: 'setup', testMatch: /auth-setup\.ts/, teardown: 'teardown', }, { name: 'teardown', testMatch: /auth-teardown\.ts/, }, { name: 'chromium', dependencies: ['setup'], use: { storageState: '.auth/sap-session.json' }, }, ], }); ``` ### Parallel Safety Tips 1. **Use unique test data**: The `testData` fixture generates UUIDs and timestamps to avoid conflicts 2. **Avoid shared state**: Do not rely on a specific PO number across parallel workers 3. **Lock management**: Use `flpLocks` to handle SM12 locks -- auto-cleanup prevents stale locks 4. **Session limits**: SAP systems may limit concurrent sessions per user -- use separate test users per worker if needed ```typescript test('parallel-safe test', async ({ ui5, testData }) => { // Generate unique test data per run const po = testData.generate({ documentNumber: '{{uuid}}', createdAt: '{{timestamp}}', vendor: '100001', }); await ui5.fill({ id: 'vendorInput' }, po.vendor); // Each worker gets its own unique data -- no conflicts }); ``` ## What You Gain Over Vanilla Playwright | Feature | Vanilla Playwright | With Praman | | --------------------------- | ------------------------- | ---------------------------------- | | UI5 control registry access | Manual `page.evaluate()` | Built-in `ui5.control()` | | UI5 stability waiting | Manual polling loops | Automatic `waitForUI5Stable()` | | OData model access | Manual `page.evaluate()` | `ui5.odata.getModelData()` | | SmartField handling | Fragile inner-control CSS | `controlType` + `properties` | | FLP navigation | Manual URL construction | 11 typed methods | | Authentication | Custom login scripts | 6 built-in strategies | | UI5 assertions | Generic `expect` only | 10 custom matchers | | Error recovery | Unstructured errors | Structured codes + suggestions | | Analytics blocking | Manual route interception | Automatic request interceptor | | Test data | Manual setup/teardown | Template generation + auto-cleanup | ## FAQ
Can I keep my existing Playwright tests? Yes. Praman's `test` and `expect` are extended versions of Playwright's. Change your import from `@playwright/test` to `playwright-praman` and every existing test continues to work unchanged. You do not need to modify a single locator or assertion.
Do I need to rewrite everything? No. The recommended approach is incremental adoption. Start by swapping the import (Phase 1), then gradually introduce `ui5.control()` for SAP UI5 controls that have brittle CSS selectors. You can mix `page.locator()` and `ui5.control()` in the same test indefinitely.
Does Praman slow down my tests? No. Praman adds UI5 stability checks (`waitForUI5Stable()`) that actually reduce flakiness and eliminate the need for manual `waitForTimeout()` calls. For non-UI5 elements, `page.locator()` runs at full Playwright speed with no overhead from Praman.
Can I use Playwright fixtures alongside Praman fixtures? Yes. Praman fixtures (`ui5`, `ui5Navigation`, `fe`, etc.) are added alongside Playwright's built-in fixtures (`page`, `context`, `browser`, `request`). Destructure whichever ones you need in each test.
:::tip[Next steps] - **[Getting Started](./getting-started.md)** -- Install Praman and run your first SAP UI5 test - **[Selectors](./selectors.md)** -- Learn the full `UI5Selector` syntax for targeting controls - **[Control Interactions](./control-interactions.md)** -- Click, fill, select, and assert on UI5 controls ::: --- ## From Selenium WebDriver Migrating from Selenium WebDriver (Java, Python, C#, or JavaScript) to Playwright + Praman for SAP Fiori and UI5 web application testing. This guide maps every core Selenium concept to its Praman equivalent, compares configuration, and provides a step-by-step migration path. :::info[In this guide] - Replace WebDriver protocol, driver management, and Selenium Grid with zero-infrastructure Playwright - Convert `By.id()`, `By.xpath()`, and `By.css()` selectors to UI5-aware `UI5Selector` objects - Eliminate explicit waits and `Thread.sleep()` with automatic `waitForUI5Stable()` - Migrate Page Object Model classes to Playwright fixtures with dependency injection - Adopt SAP-specific capabilities that have no Selenium equivalent (auth, FLP navigation, OData) ::: ## Quick Comparison | Aspect | Selenium WebDriver | Praman (Playwright) | | ------------------ | -------------------------------------------------- | ------------------------------------------------------- | | Protocol | WebDriver (W3C) over HTTP | Chrome DevTools Protocol (CDP) / BiDi | | Selector model | `By.id`, `By.xpath`, `By.css`, `By.className` | `UI5Selector` object (controlType, properties, binding) | | UI5 awareness | None (raw DOM only) | Native UI5 control registry access | | Auto-waiting | None (manual `WebDriverWait` + ExpectedConditions) | Built-in `waitForUI5Stable()` + auto-retry | | Parallel execution | Selenium Grid / TestNG / pytest-xdist | Native Playwright workers | | CI/CD integration | Requires Grid or cloud service | Zero infrastructure (`npx playwright test`) | | Browser install | Manual driver management (ChromeDriver) | `npx playwright install` (auto-managed) | | Language | Java, Python, C#, JavaScript, Ruby, Kotlin | TypeScript (first-class) | | Test runner | JUnit, TestNG, pytest, NUnit, Mocha | Playwright Test (built-in) | | Auth management | Manual login scripts | 6 built-in strategies + setup projects | | Reporting | Allure, ExtentReports (third-party) | Built-in HTML, JSON, JUnit reporters | | AI integration | None | Built-in capabilities, recipes, agentic handler | | OData helpers | None | Model-level + HTTP-level CRUD | | FLP navigation | Manual URL construction | 11 typed navigation methods | | Trace / debug | Screenshots only | Full trace (DOM snapshots, network, console, video) | | Page Object Model | Class-based POMs | Playwright fixtures (dependency injection) | ## Core API Mapping ### Navigation, Discovery, Interaction, and Assertions ```java // Selenium (Java) driver.get("https://sap-system.example.com/.../FioriLaunchpad.html#PurchaseOrder-manage"); WebElement button = driver.findElement(By.id("__xmlview0--saveBtn")); WebElement input = driver.findElement(By.xpath("//input[@aria-label='Vendor']")); button.click(); input.sendKeys("100001"); String text = driver.findElement(By.id("__xmlview0--statusText")).getText(); assertEquals("Active", text); assertTrue(driver.findElement(By.id("__xmlview0--saveBtn")).isDisplayed()); ``` ```typescript // Praman test('PO overview', async ({ ui5, ui5Navigation }) => { await ui5Navigation.navigateToApp('PurchaseOrder-manage'); await ui5.click({ controlType: 'sap.m.Button', properties: { text: 'Save' } }); await ui5.fill({ id: 'vendorInput' }, '100001'); const status = await ui5.control({ id: 'statusText' }); await expect(status).toHaveUI5Text('Active'); await expect(await ui5.control({ id: 'saveBtn' })).toBeUI5Visible(); }); ``` ### Waiting for Elements ```java // Selenium (Java) -- manual explicit waits WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(30)); wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("__xmlview0--poTable"))); Thread.sleep(5000); // common anti-pattern ``` ```typescript // Praman -- no explicit waits needed // ui5.control() automatically waits for UI5 bootstrap, pending OData requests, // JS timeouts/intervals, and DOM stabilization before returning const table = await ui5.control({ controlType: 'sap.m.Table', id: 'poTable' }); await expect(table).toHaveUI5RowCount({ min: 1 }); ``` :::warning[Common mistake] Do not carry over Selenium's `implicitlyWait` pattern by adding `page.waitForTimeout()` calls in Praman tests. Praman's `ui5.control()` automatically waits for the UI5 framework to stabilize (pending OData requests, JS timeouts, DOM settle) before returning. Adding manual waits defeats this mechanism and slows your tests. ::: ## Selector Mapping Selenium selectors target raw DOM elements. Praman selectors target the UI5 control registry, which survives UI5 version upgrades, theme changes, and DOM restructuring. | Selenium Selector | Praman `UI5Selector` Equivalent | Notes | | ------------------------------------ | ------------------------------------------------------------------------ | ---------------------------------------------- | | `By.id("__xmlview0--myBtn")` | `{ id: 'myBtn' }` | Praman strips view prefixes automatically | | `By.xpath("//button[@text='Save']")` | `{ controlType: 'sap.m.Button', properties: { text: 'Save' } }` | Property matching replaces XPath | | `By.cssSelector(".sapMBtn")` | `{ controlType: 'sap.m.Button' }` | Control type replaces CSS class matching | | `By.className("sapMInputBaseInner")` | `{ controlType: 'sap.m.Input', id: 'myInput' }` | Target the control, not its inner DOM | | `By.linkText("Purchase Orders")` | `{ controlType: 'sap.m.Link', properties: { text: 'Purchase Orders' } }` | Text property replaces link text | | `By.xpath("//div[@data-path]")` | `{ bindingPath: { value: '/Vendor/Name' } }` | OData binding path matching | | `By.xpath` with `contains()` | `{ id: /partialMatch/ }` | RegExp ID matching | | `By.xpath` with ancestor axis | `{ ancestor: { controlType: 'sap.m.Panel', id: 'headerPanel' } }` | Ancestor selector replaces XPath ancestor axis | | N/A | `{ searchOpenDialogs: true }` | Dialog-aware search (no Selenium equivalent) | :::tip Generated IDs Selenium tests for SAP UI5 apps commonly break because UI5 generates DOM IDs like `__xmlview0--__button3`. These IDs change across page reloads, UI5 versions, and theme switches. Praman's `UI5Selector` bypasses DOM IDs entirely by querying the UI5 control registry. ::: ## Config Mapping ### Selenium Capabilities (Java) ```java ChromeOptions options = new ChromeOptions(); options.addArguments("--headless", "--window-size=1920,1080"); WebDriver driver = new ChromeDriver(options); driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10)); driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(60)); ``` ### Praman Config (playwright.config.ts + praman.config.ts) ```typescript // playwright.config.ts export default defineConfig({ testDir: './tests', timeout: 120_000, retries: 1, workers: process.env.CI ? 2 : 1, use: { baseURL: process.env.SAP_CLOUD_BASE_URL, headless: true, // --headless viewport: { width: 1920, height: 1080 }, // --window-size trace: 'on-first-retry', screenshot: 'only-on-failure', }, projects: [ { name: 'setup', testMatch: /auth-setup\.ts/, teardown: 'teardown' }, { name: 'teardown', testMatch: /auth-teardown\.ts/ }, { name: 'chromium', dependencies: ['setup'], use: { ...devices['Desktop Chrome'], storageState: '.auth/sap-session.json' }, }, ], }); ``` ```typescript // praman.config.ts export default defineConfig({ logLevel: 'info', ui5WaitTimeout: 30_000, // Replaces implicitlyWait + WebDriverWait controlDiscoveryTimeout: 10_000, interactionStrategy: 'ui5-native', auth: { strategy: 'basic', baseUrl: process.env.SAP_CLOUD_BASE_URL!, username: process.env.SAP_CLOUD_USERNAME!, password: process.env.SAP_CLOUD_PASSWORD!, }, }); ``` ## Page Object Model to Fixtures Selenium teams almost universally use the Page Object Model. Praman replaces class-based POMs with Playwright's fixture system -- dependency injection with automatic teardown. ### Selenium Page Object (Java) ```java public class PurchaseOrderPage { @FindBy(id = "__xmlview0--vendorInput-inner") private WebElement vendorInput; @FindBy(id = "__xmlview0--saveBtn") private WebElement saveButton; @FindBy(css = ".sapMMsgStrip") private WebElement messageStrip; public PurchaseOrderPage(WebDriver d) { PageFactory.initElements(d, this); } public void fillVendor(String v) { vendorInput.clear(); vendorInput.sendKeys(v); } public void save() { saveButton.click(); } public String getMessage() { return messageStrip.getText(); } } // Test PurchaseOrderPage po = new PurchaseOrderPage(driver); po.fillVendor("100001"); po.save(); assertEquals("PO created", po.getMessage()); ``` ### Praman Fixture Equivalent ```typescript test('create purchase order', async ({ ui5, ui5Navigation, ui5Footer }) => { await test.step('Navigate', async () => { await ui5Navigation.navigateToApp('PurchaseOrder-create'); }); await test.step('Fill vendor', async () => { await ui5.fill({ id: 'vendorInput' }, '100001'); }); await test.step('Save', async () => { await ui5Footer.clickSave(); }); await test.step('Verify success', async () => { const message = await ui5.control({ controlType: 'sap.m.MessageStrip', properties: { type: 'Success' }, searchOpenDialogs: true, }); await expect(message).toBeUI5Visible(); }); }); ``` For reusable logic across tests, extract helper functions instead of POM classes. ```typescript // helpers/po-helpers.ts export async function fillPOHeader( ui5: ExtendedUI5Handler, data: { vendor: string; purchaseOrg: string; companyCode: string }, ): Promise { await ui5.fill({ id: 'vendorInput' }, data.vendor); await ui5.fill({ id: 'purchOrgInput' }, data.purchaseOrg); await ui5.fill({ id: 'compCodeInput' }, data.companyCode); } ``` ## Hybrid Approach During Migration You do not need to rewrite everything at once. Praman's `test` and `expect` are extended versions of Playwright's -- `page.locator()` works alongside `ui5.control()` in the same test. ```typescript test('hybrid test', async ({ page, ui5, ui5Navigation }) => { // Playwright handles login (before UI5 loads) await test.step('Login', async () => { await page.goto(process.env.SAP_CLOUD_BASE_URL!); await page.locator('#USERNAME_FIELD input').fill('TESTUSER'); await page.locator('#PASSWORD_FIELD input').fill('secret'); await page.locator('#LOGIN_LINK').click(); await page.waitForURL('**/FioriLaunchpad*'); }); // Praman takes over once UI5 is loaded await test.step('Navigate and verify', async () => { await ui5Navigation.navigateToApp('PurchaseOrder-manage'); await expect(page).toHaveTitle(/Purchase Orders/); const table = await ui5.control({ controlType: 'sap.m.Table', id: 'poTable' }); await expect(table).toHaveUI5RowCount({ min: 1 }); }); }); ``` ## New Features Only in Praman Praman includes capabilities that have no Selenium equivalent. These are worth adopting early. ### 6 Built-In Auth Strategies No more custom login scripts or cookie injection. Praman supports Basic, BTP SAML, Office 365, Custom, API, and Certificate authentication out of the box. ```typescript test('after auth', async ({ ui5Navigation }) => { await ui5Navigation.navigateToApp('PurchaseOrder-manage'); }); ``` ### 11 FLP Navigation Methods ```typescript test('navigation', async ({ ui5Navigation }) => { await ui5Navigation.navigateToApp('PurchaseOrder-manage'); await ui5Navigation.navigateToTile('Create Purchase Order'); await ui5Navigation.navigateToIntent( { semanticObject: 'PurchaseOrder', action: 'create' }, { plant: '1000' }, ); await ui5Navigation.navigateToHome(); await ui5Navigation.navigateBack(); }); ``` ### Fiori Elements Helpers Dedicated APIs for List Report and Object Page patterns -- no more fragile XPath chains. ```typescript test('FE list report', async ({ fe }) => { await fe.listReport.setFilter('Status', 'Active'); await fe.listReport.search(); await fe.listReport.navigateToItem(0); await fe.objectPage.clickEdit(); await fe.objectPage.clickSave(); }); ``` ### AI-Powered Test Generation ```typescript test('AI discovery', async ({ pramanAI }) => { const context = await pramanAI.discoverPage({ interactiveOnly: true }); const result = await pramanAI.agentic.generateTest( 'Create a purchase order for vendor 100001', page, ); }); ``` ### Custom UI5 Matchers 10 UI5-specific Playwright matchers -- replacing verbose Selenium assert chains. ```typescript const button = await ui5.control({ id: 'saveBtn' }); await expect(button).toBeUI5Enabled(); await expect(button).toHaveUI5Text('Save'); await expect(button).toBeUI5Visible(); ``` ### OData Model + HTTP Operations ```typescript test('OData', async ({ ui5 }) => { const data = await ui5.odata.getModelData('/PurchaseOrders'); // HTTP-level CRUD with automatic CSRF handling }); ``` ### SM12 Lock Management ```typescript test('locks', async ({ flpLocks }) => { await flpLocks.deleteAllLockEntries('TESTUSER'); // Auto-cleanup on teardown }); ``` ### Structured Error Codes Every Praman error includes a machine-readable `code`, `retryable` flag, and `suggestions[]` array for self-healing tests. Selenium errors are raw `WebDriverException` messages with stack traces but no recovery guidance. ## Step-by-Step Migration Checklist 1. **Install dependencies**: `npm install playwright-praman @playwright/test` 2. **Install browsers**: `npx playwright install` 3. **Create config files**: `praman.config.ts` and `playwright.config.ts` (see Config Mapping) 4. **Set up auth**: Create `tests/auth-setup.ts` using one of the 6 strategies 5. **Convert selectors**: Replace `By.id()` / `By.xpath()` / `By.css()` with `UI5Selector` objects 6. **Remove explicit waits**: Delete all `WebDriverWait`, `Thread.sleep()`, `implicitlyWait` -- Praman auto-waits 7. **Convert Page Objects**: Replace POM classes with fixture destructuring and helper functions 8. **Convert assertions**: Replace JUnit/TestNG `assertEquals` with Playwright `expect` + UI5 matchers 9. **Structure tests**: Convert `@Test` methods to `test()` + `test.step()` blocks 10. **Adopt navigation**: Replace `driver.get()` URL strings with `ui5Navigation` methods 11. **Remove driver management**: Delete ChromeDriver setup, Grid config, and teardown code 12. **Run and verify**: `npx playwright test` ## Team Workflow Transition ### From Java/Maven/Grid to TypeScript/npm/Playwright | Workflow | Selenium Stack | Praman Stack | | ------------------ | ------------------------------------- | --------------------------------------------------- | | Language | Java (+ Kotlin, Python, C#) | TypeScript | | Build tool | Maven / Gradle | npm | | Test runner | JUnit / TestNG | Playwright Test (built-in) | | Test structure | `@Test` annotated methods in classes | `test()` functions in `.test.ts` files | | Page Objects | Java classes with `@FindBy` | Playwright fixtures + helper functions | | Driver management | WebDriverManager / ChromeDriver | `npx playwright install` (fully automatic) | | Parallel execution | Selenium Grid + TestNG parallel suite | `workers: 4` in `playwright.config.ts` | | CI/CD | Grid server + nodes + Docker | `npx playwright test` (zero infrastructure) | | Reporting | Allure / ExtentReports plugin | Built-in HTML report (`npx playwright show-report`) | | Environment config | Maven profiles / `testng.xml` | `.env` files + `playwright.config.ts` projects | | Version control | Git (test code) + driver binaries | Git (everything, no binaries) | | IDE | IntelliJ / Eclipse | VS Code (recommended) / any IDE | ### Suggested Team Transition Plan #### Week 1-2: Foundation - Install Node.js, VS Code, and Playwright on developer machines - Complete the [Playwright Primer](./playwright-primer) (2-3 hours per person) - Set up a Git repository for the test project #### Week 3-4: First Tests - Convert 3-5 Selenium test classes from a single Fiori app to Praman tests - Set up authentication via setup projects (replaces Selenium login helpers) - Run tests locally and review the HTML report #### Week 5-6: CI Integration - Add `npx playwright test` to your CI/CD pipeline (replaces Grid + Maven Surefire) - Configure `screenshot: 'only-on-failure'` and `trace: 'on-first-retry'` - Set up environment variables for SAP credentials in CI secrets #### Week 7-8: Scale - Convert remaining Selenium test suites in priority order - Adopt Fiori Elements helpers (`fe.listReport`, `fe.objectPage`) for standard Fiori apps - Enable parallel execution with 2-4 workers (replaces Selenium Grid) - Decommission Selenium Grid infrastructure ### Role Mapping | Selenium Role | Praman Equivalent | | -------------------------- | ------------------------------------------------------- | | SDET / Automation Engineer | Test Engineer (writes `.test.ts` files) | | Grid Administrator | Not needed (no infrastructure to manage) | | Driver / Browser Manager | Not needed (`npx playwright install`) | | Test Architect (POMs) | Same role, designs fixtures and helper modules | | QA Lead / Manager | Reviews PRs, reads Playwright HTML reports, monitors CI | ## FAQ
How does Playwright's architecture differ from Selenium WebDriver? Selenium communicates with the browser over the W3C WebDriver protocol via HTTP. Each command is a round-trip HTTP request to the driver server (ChromeDriver, GeckoDriver). Playwright uses the Chrome DevTools Protocol (CDP) or WebDriver BiDi over a persistent WebSocket connection. This means Playwright commands are faster, support event-driven waiting, and do not require separate driver binaries.
Can I migrate incrementally or do I need to rewrite all tests at once? You can migrate incrementally. Start with a few high-value test classes, convert them to Praman tests, and run both Selenium and Playwright suites in parallel during the transition. Praman supports hybrid tests where `page.locator()` and `ui5.control()` coexist in the same file, so you can convert selectors one at a time.
Do I still need Selenium Grid for parallel execution? No. Playwright has native parallel execution via the `workers` setting in `playwright.config.ts`. Each worker runs in its own isolated browser context. There is no grid infrastructure to install, configure, or maintain. Run `npx playwright test` on any machine or CI runner.
What happens to my Page Object Model classes? Playwright uses fixtures instead of class-based Page Object Models. Fixtures provide dependency injection with automatic setup and teardown. You can extract reusable logic into helper functions (see the "Page Object Model to Fixtures" section above). The pattern is simpler and avoids the `@FindBy` annotation overhead.
:::tip[Next steps] - **[Getting Started](./getting-started.md)** -- Install Praman and run your first SAP UI5 test - **[Selectors](./selectors.md)** -- Learn the full `UI5Selector` syntax for targeting controls - **[Control Interactions](./control-interactions.md)** -- Click, fill, select, and assert on UI5 controls ::: --- ## From Tosca to Playwright-Praman A guide for teams migrating from Tricentis Tosca to Playwright + Praman for SAP Fiori and UI5 web application testing. Covers concept mapping, workflow changes, and practical code examples. :::info[In this guide] - Map 25 Tosca concepts (TestCase, Module, TCD, Recovery Scenario) to Playwright equivalents - Convert Tosca TestCases with Modules and ActionModes into TypeScript test files - Replace Tosca workspace versioning with Git-based version control and code review - Transition from per-user licensing to open-source tooling (Apache 2.0) - Keep Tosca for SAP GUI testing while using Praman for Fiori/UI5 web apps ::: ## Quick Comparison | Aspect | Tosca | Playwright + Praman | | ------------------ | ------------------------------ | -------------------------------------------- | | Licensing | Per-user commercial license | Open source (Apache 2.0) | | SAP Fiori support | Via SAP UI5 scan / engine | Native UI5 control registry | | CI/CD integration | Tosca CI adapter | First-class CLI (`npx playwright test`) | | Version control | Tosca workspace | Git (standard branching, PRs, diffs) | | Parallel execution | Tosca Distributed Execution | Native Playwright workers | | AI/ML | Tosca Vision AI (image-based) | LLM-powered test generation + recipe library | | Browser support | Chrome, Firefox, Edge | Chrome, Firefox, Safari, Edge | | Scripting language | Tosca TestStepValues / Modules | TypeScript (full programming language) | | Community | Tricentis community | Playwright 80k+ GitHub stars, npm ecosystem | :::note SAP GUI Testing Tosca excels at SAP GUI testing via its SAP engine. If your test scope includes SAP GUI transactions (not browser-based Fiori apps), keep Tosca for those tests and use Praman for Fiori/UI5 web apps. Both can coexist in a CI pipeline. ::: ## Concept Mapping Table The following table maps 25 core Tosca concepts to their Playwright/Praman equivalents. | # | Tosca Concept | Playwright/Praman Equivalent | Notes | | --- | ------------------------- | ------------------------------------------- | --------------------------------------------------------------- | | 1 | **Workspace** | Git repository | Tests are TypeScript files in a Git repo | | 2 | **TestCase** | `test('name', async () => {...})` | A single test function in a `.test.ts` file | | 3 | **TestStep** | `test.step('name', async () => {...})` | Collapsible step in HTML report | | 4 | **TestStepValue** | Variable / parameter | `const vendor = '100001';` | | 5 | **Module** | Playwright fixture | `async ({ ui5, fe }) => {...}` | | 6 | **ModuleAttribute** | Fixture property / method | `ui5.control()`, `fe.listReport.search()` | | 7 | **TestCaseDesign (TCD)** | Test data fixture | `testData.generate({ vendor: '{{uuid}}' })` | | 8 | **TestConfiguration** | `playwright.config.ts` + `praman.config.ts` | Two config files: runner + SAP-specific | | 9 | **ExecutionList** | Playwright project | `projects: [{ name: 'chromium', ... }]` | | 10 | **TestDataContainer** | `.env` file + `testData` fixture | Environment vars + template generation | | 11 | **ActionMode Verify** | `expect()` assertion | `await expect(btn).toBeUI5Enabled()` | | 12 | **ActionMode Input** | `ui5.fill()` / `ui5.select()` | `await ui5.fill({ id: 'vendorInput' }, '100001')` | | 13 | **ActionMode Buffer** | Variable assignment | `const value = await control.getValue()` | | 14 | **ActionMode WaitOn** | `ui5.control()` auto-wait | Built-in `waitForUI5Stable()` | | 15 | **Steering Parameter** | Config option / env var | `PRAMAN_LOG_LEVEL=debug npx playwright test` | | 16 | **Recovery Scenario** | `test.afterEach()` / fixture teardown | Auto-cleanup in fixtures (locks, test data) | | 17 | **Scan (UI5 Engine)** | `UI5Selector` object | `{ controlType: 'sap.m.Button', properties: { text: 'Save' } }` | | 18 | **XScan / TBox** | `page.locator()` (CSS/XPath) | For non-UI5 elements; `ui5.control()` for UI5 | | 19 | **Library** | npm package / utility module | Shared helpers via `import` | | 20 | **Requirements** | Test annotations / tags | `test('PO @smoke', ...)` + `--grep @smoke` | | 21 | **Execution log** | Playwright HTML report | `npx playwright show-report` | | 22 | **Screenshots** | Automatic screenshots | `screenshot: 'only-on-failure'` in config | | 23 | **Distributed Execution** | `workers` config | `workers: 4` in `playwright.config.ts` | | 24 | **Tosca Commander** | VS Code / terminal | IDE + `npx playwright test` | | 25 | **DEX Agent** | CI runner (GitHub Actions, Jenkins) | `npx playwright test` in pipeline YAML | ## Tosca TestCase to Playwright Test ### Tosca TestCase (Conceptual) ```text TestCase: Create Purchase Order TestStep: Login Module: SAP_Login Username = {TC_User} Password = {TC_Password} TestStep: Navigate to Create PO Module: FLP_Navigation Tile = "Create Purchase Order" TestStep: Fill Header Data Module: PO_Header Vendor = {TC_Vendor} PurchaseOrg = {TC_PurchOrg} CompanyCode = {TC_CompCode} TestStep: Add Item Module: PO_Item Material = {TC_Material} Quantity = {TC_Quantity} Plant = {TC_Plant} TestStep: Save Module: PO_Footer Action = Save TestStep: Verify Success Module: PO_Message MessageType = Success [Verify] ``` ### Praman Equivalent ```typescript // Test data replaces Tosca TestCaseDesign / TestDataContainer const testInput = { vendor: '100001', purchaseOrg: '1000', companyCode: '1000', material: 'MAT-001', quantity: '10', plant: '1000', }; test('create purchase order', async ({ ui5, ui5Navigation, fe, ui5Footer }) => { // TestStep: Navigate (login handled by setup project -- no code needed) await test.step('Navigate to Create PO', async () => { await ui5Navigation.navigateToTile('Create Purchase Order'); }); // TestStep: Fill Header Data await test.step('Fill header data', async () => { await ui5.fill({ id: 'vendorInput' }, testInput.vendor); await ui5.fill({ id: 'purchOrgInput' }, testInput.purchaseOrg); await ui5.fill({ id: 'compCodeInput' }, testInput.companyCode); }); // TestStep: Add Item await test.step('Add line item', async () => { await ui5.fill({ id: 'materialInput' }, testInput.material); await ui5.fill({ id: 'quantityInput' }, testInput.quantity); await ui5.fill({ id: 'plantInput' }, testInput.plant); }); // TestStep: Save (replaces Tosca PO_Footer module) await test.step('Save', async () => { await ui5Footer.clickSave(); }); // TestStep: Verify (replaces Tosca ActionMode Verify) await test.step('Verify success message', async () => { const message = await ui5.control({ controlType: 'sap.m.MessageStrip', properties: { type: 'Success' }, searchOpenDialogs: true, }); await expect(message).toBeUI5Visible(); }); }); ``` ## Intent API and SAP Transaction Codes Praman's Intent API provides high-level business operations that map to SAP transaction codes and Fiori apps. | SAP TCode | Fiori App | Praman Intent API | | --------- | ----------------------- | ---------------------------------------------- | | ME21N | Create Purchase Order | `intent.procurement.createPurchaseOrder()` | | ME23N | Display Purchase Order | `intent.procurement.displayPurchaseOrder()` | | VA01 | Create Sales Order | `intent.sales.createSalesOrder()` | | VA03 | Display Sales Order | `intent.sales.displaySalesOrder()` | | FB60 | Enter Vendor Invoice | `intent.finance.postVendorInvoice()` | | FBL1N | Vendor Line Items | `intent.finance.displayVendorLineItems()` | | CO01 | Create Production Order | `intent.manufacturing.createProductionOrder()` | | MM01 | Create Material Master | `intent.masterData.createMaterial()` | ### Using the Intent API ```typescript test('create PO via intent API', async ({ intent }) => { await test.step('Create purchase order', async () => { await intent.procurement.createPurchaseOrder({ vendor: '100001', material: 'MAT-001', quantity: 10, plant: '1000', }); }); await test.step('Post vendor invoice', async () => { await intent.finance.postVendorInvoice({ vendor: '100001', amount: 5000, currency: 'EUR', }); }); }); ``` ## Team Workflow Transition ### From Tosca Commander to Git + IDE | Workflow | Tosca | Praman | | ----------------- | ---------------------------- | --------------------------------------------------- | | Create test | Tosca Commander GUI | Create `.test.ts` file in VS Code | | Edit test | Tosca Module editor | Edit TypeScript in any IDE | | Version control | Tosca workspace versioning | Git branches + pull requests | | Code review | Not built-in | GitHub/GitLab PR reviews | | Merge conflicts | Workspace conflicts (binary) | Text-based Git merge (readable diffs) | | Share test assets | Export/import workspace | `npm install` shared packages | | Collaborate | Workspace locks | Git branching (multiple people edit simultaneously) | ### Suggested Team Transition Plan #### Week 1-2: Foundation - Install Node.js, VS Code, and Playwright on developer machines - Complete the [Playwright Primer](./playwright-primer) (2-3 hours per person) - Set up a Git repository for the test project #### Week 3-4: First Tests - Convert 3-5 Tosca TestCases from a single Fiori app to Praman tests - Set up authentication via setup projects - Run tests locally and review the HTML report #### Week 5-6: CI Integration - Add `npx playwright test` to your CI/CD pipeline - Configure `screenshot: 'only-on-failure'` and `trace: 'on-first-retry'` - Set up environment variables for SAP credentials in CI secrets #### Week 7-8: Scale - Convert remaining Tosca TestCases in priority order - Adopt Fiori Elements helpers (`fe.listReport`, `fe.objectPage`) for standard Fiori apps - Enable parallel execution with 2-4 workers ### Role Mapping | Tosca Role | Praman Equivalent | | ------------------- | ------------------------------------------------------- | | Test Designer | Test Engineer (writes `.test.ts` files) | | Test Data Manager | Uses `testData` fixture + `.env` files | | Automation Engineer | Same role, now using TypeScript + Playwright | | Test Manager | Reviews PRs, reads Playwright HTML reports, monitors CI | | Tosca Administrator | Not needed (config is in Git, CI handles execution) | ## Handling Tosca-Specific Patterns ### Recovery Scenarios Tosca Recovery Scenarios handle unexpected dialogs or errors. In Praman, use fixture teardown and `test.afterEach()`. ```typescript // Automatic recovery: fixtures clean up on teardown // - flpLocks: deletes stale SM12 locks // - testData: removes generated test data // - sapAuth: logs out // Manual recovery for edge cases test.afterEach(async ({ ui5 }) => { // Dismiss any open dialogs try { const okButton = await ui5.control({ controlType: 'sap.m.Button', properties: { text: 'OK' }, searchOpenDialogs: true, }); await okButton.press(); } catch { // No dialog open -- nothing to dismiss } }); ``` ### Test Data Parameterization Tosca TestCaseDesign provides data-driven testing. In Playwright, use arrays or CSV files. ```typescript // Data-driven test (replaces Tosca TCD) const vendors = [ { id: '100001', name: 'Vendor A', country: 'US' }, { id: '100002', name: 'Vendor B', country: 'DE' }, { id: '100003', name: 'Vendor C', country: 'JP' }, ]; for (const vendor of vendors) { test(`create PO for ${vendor.name}`, async ({ ui5, ui5Navigation }) => { await ui5Navigation.navigateToApp('PurchaseOrder-create'); await ui5.fill({ id: 'vendorInput' }, vendor.id); // ... rest of test }); } ``` ### Reusable Modules Tosca Modules are reusable test components. In Praman, extract shared logic into helper functions or custom fixtures. ```typescript // helpers/po-helpers.ts export async function fillPOHeader( ui5: ExtendedUI5Handler, data: { vendor: string; purchaseOrg: string; companyCode: string }, ): Promise { await ui5.fill({ id: 'vendorInput' }, data.vendor); await ui5.fill({ id: 'purchOrgInput' }, data.purchaseOrg); await ui5.fill({ id: 'compCodeInput' }, data.companyCode); } ``` ```typescript // tests/purchase-order.test.ts test('create PO', async ({ ui5, ui5Navigation }) => { await ui5Navigation.navigateToApp('PurchaseOrder-create'); await fillPOHeader(ui5, { vendor: '100001', purchaseOrg: '1000', companyCode: '1000' }); // ... }); ``` :::warning[Common mistake] Do not try to replicate Tosca's modular structure by creating one TypeScript file per Module. In Playwright, a test file contains complete test scenarios with `test.step()` blocks for structure. Shared logic goes into helper functions, not separate test files. This keeps tests self-contained and easier to debug. ::: ## Quick Reference Card | I want to... | Tosca | Praman | | ------------------- | --------------------------------- | -------------------------------------------------------- | | Find a UI5 control | Scan / Module with UI5 engine | `ui5.control({ controlType: '...', properties: {...} })` | | Click a button | ActionMode Input on Button module | `await ui5.click({ id: 'myBtn' })` | | Fill a text field | ActionMode Input on Input module | `await ui5.fill({ id: 'myInput' }, 'value')` | | Verify a value | ActionMode Verify | `await expect(control).toHaveUI5Text('value')` | | Buffer/read a value | ActionMode Buffer | `const val = await control.getValue()` | | Wait for page ready | WaitOn / Recovery | Automatic `waitForUI5Stable()` | | Navigate to app | Browser module + URL | `await ui5Navigation.navigateToApp('App-action')` | | Take screenshot | Screenshot step | `await page.screenshot({ path: 'file.png' })` | | Run in CI | Tosca CI adapter + DEX | `npx playwright test` in any CI | | Parameterize data | TestCaseDesign (TCD) | `for (const row of data) { test(...) }` | | Handle errors | Recovery Scenario | Fixture teardown + `test.afterEach()` | ## FAQ
Why move from a GUI-based tool to coded tests? Coded tests in Git provide benefits that GUI-based test management cannot: readable diffs on every change, pull request reviews before tests go live, branch-based parallel development, and seamless CI/CD integration without proprietary adapters. Your test logic becomes a first-class software artifact with full version history.
Can I keep Tosca for SAP GUI and use Praman for Fiori? Yes. Tosca's SAP GUI engine has no equivalent in Playwright. Many teams run Tosca for SAP GUI transactions (SE38, SM30, etc.) and Praman for browser-based Fiori/UI5 apps. Both test suites can run in the same CI pipeline and report results independently.
Do I need programming experience to use Praman? Basic TypeScript knowledge is needed to write and maintain tests. However, if your team includes a Test Engineer, Business Analysts can define scenarios in plain language and have the engineer translate them into code. Praman's AI agent can also generate tests from natural-language descriptions. See the [SAP Business Analyst Guide](./sap-business-analyst-guide.md) for details.
How does Tosca's TestCaseDesign (TCD) map to Praman? TCD provides data-driven testing with parameterized test data. In Playwright, you define test data as arrays or load from CSV/JSON files, then loop over them with `for (const row of data) { test(...) }`. Each data combination becomes a separate named test in the report. See the "Test Data Parameterization" section above for a complete example.
:::tip[Next steps] - **[Getting Started](./getting-started.md)** -- Install Praman and run your first SAP UI5 test - **[Selectors](./selectors.md)** -- Learn the full `UI5Selector` syntax for targeting controls - **[Control Interactions](./control-interactions.md)** -- Click, fill, select, and assert on UI5 controls ::: --- ## Migration from wdi5 Migrate your wdi5 test suite to Playwright-Praman with minimal friction. This guide maps every wdi5 API to its Praman equivalent and highlights features that are new in Praman. :::info[In this guide] - Convert `browser.asControl()` calls to `ui5.control()` with minimal selector changes - Map wdi5 config (`wdio.conf.ts`) to Praman config (`praman.config.ts` + `playwright.config.ts`) - Replace OPA5 journey patterns with `test.step()` for structured test reporting - Adopt built-in auth, FLP navigation, and Fiori Elements helpers that wdi5 does not offer - Handle the `labelFor` selector difference using Praman's `:labeled()` pseudo-class ::: ## Coming from WebdriverIO (without wdi5) wdi5 is built on top of WebdriverIO, so if you are using WebdriverIO for SAP testing without the wdi5 plugin, this guide is still the right starting point. The main difference is that without wdi5, you are targeting raw DOM elements via WebdriverIO selectors (`$()`, `$$()`) rather than UI5 controls via `browser.asControl()`. In that case, your migration involves replacing WebdriverIO DOM selectors with Praman's `ui5.control()` for UI5 elements and `page.locator()` for non-UI5 elements. The config mapping, test structure guidance, and new features sections below all apply equally to WebdriverIO users. ## Quick Comparison | Aspect | wdi5 | Praman | | ------------------ | ------------------------------------- | ----------------------------------------------- | | Test runner | WebdriverIO | Playwright | | UI5 bridge | `browser.asControl()` | `ui5.control()` | | Selector engine | wdi5 selector object | `UI5Selector` object + CSS locator syntax | | Async model | WDIO auto-sync (deprecated) / `async` | Always `async/await` | | Parallel execution | Limited (WDIO workers) | Native Playwright workers | | Auth management | Manual login scripts | 6 built-in strategies + setup projects | | AI integration | None | Built-in capabilities, recipes, agentic handler | | OData helpers | None | Model-level + HTTP-level CRUD | | FLP navigation | Manual hash navigation | 11 typed navigation methods | ## Core API Mapping ### Control Discovery ```typescript // wdi5 const button = await browser.asControl({ selector: { controlType: 'sap.m.Button', properties: { text: 'Save' }, }, }); // Praman test('find a button', async ({ ui5 }) => { const button = await ui5.control({ controlType: 'sap.m.Button', properties: { text: 'Save' }, }); }); ``` ### Multiple Controls ```typescript // wdi5 const buttons = await browser.allControls({ selector: { controlType: 'sap.m.Button' }, }); // Praman const buttons = await ui5.controls({ controlType: 'sap.m.Button' }); ``` ### Control Interaction ```typescript // wdi5 const input = await browser.asControl({ selector: { id: 'nameInput' }, }); await input.enterText('John Doe'); const value = await input.getValue(); // Praman const input = await ui5.control({ id: 'nameInput' }); await input.enterText('John Doe'); const value = await input.getValue(); // Or use the shorthand: await ui5.fill({ id: 'nameInput' }, 'John Doe'); ``` ## CSS-Style Locator Selectors Praman also supports a CSS-like syntax for finding controls with `page.locator('ui5=...')`. This has no wdi5 equivalent — it's an additional way to write selectors when you need structural queries, label matching, or positional selection: ```typescript // Find a button by text await page.locator("ui5=sap.m.Button[text='Save']").click(); // Find an input by its associated label await page.locator('ui5=sap.m.Input:labeled("Vendor")').fill('100001'); // Find the first input inside a form await page.locator('ui5=sap.ui.layout.form.SimpleForm sap.m.Input:first-child').click(); ``` This returns a standard Playwright `Locator`, so you can chain `.click()`, `.fill()`, and use it with `expect()`. See [Finding Controls with Locators](/docs/guides/locator-selector-syntax) for the full syntax reference. ## Selector Field Mapping The selector object structure is similar, with a few naming adjustments and additions. | wdi5 Selector Field | Praman `UI5Selector` Field | Type | Notes | | ------------------- | -------------------------- | ------------------------- | -------------------------------------------- | | `controlType` | `controlType` | `string` | Identical. Fully qualified UI5 type. | | `id` | `id` | `string \| RegExp` | Identical. Supports RegExp in Praman. | | `viewName` | `viewName` | `string` | Identical. | | `viewId` | `viewId` | `string` | Identical. | | `properties` | `properties` | `Record` | Identical. Key-value property matchers. | | `bindingPath` | `bindingPath` | `Record` | Identical. OData binding path matchers. | | `i18NText` | `i18NText` | `Record` | Identical. i18n translated value matchers. | | `ancestor` | `ancestor` | `UI5Selector` | Identical. Recursive parent matching. | | `descendant` | `descendant` | `UI5Selector` | Identical. Recursive child matching. | | `interaction` | `interaction` | `UI5Interaction` | Identical. `idSuffix` and `domChildWith`. | | `searchOpenDialogs` | `searchOpenDialogs` | `boolean` | Identical. Search inside open dialogs. | | `labelFor` | -- | -- | **Not available in Praman.** See note below. | :::tip labelFor Selector wdi5 supports a `labelFor` selector field that matches controls by their associated `sap.m.Label`. In Praman, use the `:labeled()` pseudo-class with [CSS locator syntax](/docs/guides/locator-selector-syntax): ```typescript // wdi5 const input = await browser.asControl({ selector: { controlType: 'sap.m.Input', labelFor: { text: 'Vendor Name' } }, }); // Praman — use :labeled() with page.locator() const input = page.locator('ui5=sap.m.Input:labeled("Vendor Name")'); await input.fill('100001'); ``` `:labeled()` checks both the label's `labelFor` association and the control's `ariaLabelledBy` association. You can also combine it with other selectors: ```typescript page.locator("ui5=sap.m.Input:labeled('Vendor Name')[required='true']"); ``` ::: ## Config Mapping ### wdi5 Config (wdio.conf.ts) ```typescript // wdio.conf.ts export const config = { wdi5: { waitForUI5Timeout: 30000, logLevel: 'verbose', url: 'https://sap-system.example.com', skipInjectUI5OnStart: false, }, specs: ['./test/specs/**/*.ts'], baseUrl: 'https://sap-system.example.com', }; ``` ### Praman Config (praman.config.ts + playwright.config.ts) ```typescript // praman.config.ts export default defineConfig({ logLevel: 'info', // wdi5 logLevel -> praman logLevel ui5WaitTimeout: 30_000, // wdi5 waitForUI5Timeout -> praman ui5WaitTimeout controlDiscoveryTimeout: 10_000, interactionStrategy: 'ui5-native', auth: { strategy: 'basic', baseUrl: process.env.SAP_CLOUD_BASE_URL!, username: process.env.SAP_CLOUD_USERNAME!, password: process.env.SAP_CLOUD_PASSWORD!, }, }); ``` ```typescript // playwright.config.ts export default defineConfig({ testDir: './tests', timeout: 120_000, use: { baseURL: process.env.SAP_CLOUD_BASE_URL, trace: 'on-first-retry', screenshot: 'only-on-failure', }, projects: [ { name: 'setup', testMatch: /auth-setup\.ts/, teardown: 'teardown' }, { name: 'teardown', testMatch: /auth-teardown\.ts/ }, { name: 'chromium', dependencies: ['setup'], use: { ...devices['Desktop Chrome'], storageState: '.auth/sap-session.json' }, }, ], }); ``` ## OPA5 Journey to test.step() wdi5 test files often follow the OPA5 journey pattern. In Praman, use `test.step()` for structured multi-step tests. ### wdi5 + OPA5 Journey ```typescript // wdi5 test describe('Purchase Order', () => { it('should navigate to the app', async () => { await browser.url('#/PurchaseOrder-manage'); await browser.asControl({ selector: { id: 'poTable' } }); }); it('should create a new PO', async () => { const createBtn = await browser.asControl({ selector: { controlType: 'sap.m.Button', properties: { text: 'Create' } }, }); await createBtn.press(); }); it('should fill vendor field', async () => { const vendorInput = await browser.asControl({ selector: { id: 'vendorInput' }, }); await vendorInput.enterText('100001'); }); }); ``` ### Praman + test.step() ```typescript test('create a purchase order', async ({ ui5, ui5Navigation }) => { await test.step('Navigate to PO app', async () => { await ui5Navigation.navigateToApp('PurchaseOrder-manage'); }); await test.step('Click Create', async () => { await ui5.click({ controlType: 'sap.m.Button', properties: { text: 'Create' }, }); }); await test.step('Fill vendor field', async () => { await ui5.fill({ id: 'vendorInput' }, '100001'); }); }); ``` :::warning[Common mistake] Do not wrap `ui5.control()` in a manual retry loop or `waitUntil()` call carried over from WebdriverIO. Praman's control discovery already includes multi-strategy retries with configurable timeouts (`controlDiscoveryTimeout`). Adding an outer retry loop causes duplicate waiting and makes tests slower and harder to debug. ::: ## New in Praman vs wdi5 Praman includes features that wdi5 does not offer. If you are migrating, these are worth adopting early. ### 6 Built-In Auth Strategies No more custom login scripts. Praman supports Basic, BTP SAML, Office 365, Custom, API, and Certificate authentication out of the box. ```typescript // Auth is handled by setup projects — no login code in tests test('after auth', async ({ ui5Navigation }) => { await ui5Navigation.navigateToApp('PurchaseOrder-manage'); }); ``` ### 11 FLP Navigation Methods ```typescript test('navigation', async ({ ui5Navigation }) => { await ui5Navigation.navigateToApp('PurchaseOrder-manage'); await ui5Navigation.navigateToTile('Create Purchase Order'); await ui5Navigation.navigateToIntent( { semanticObject: 'PurchaseOrder', action: 'create' }, { plant: '1000' }, ); await ui5Navigation.navigateToHome(); await ui5Navigation.navigateBack(); }); ``` ### Fiori Elements Helpers Dedicated APIs for List Report and Object Page patterns. ```typescript test('FE list report', async ({ fe }) => { await fe.listReport.setFilter('Status', 'Active'); await fe.listReport.search(); await fe.listReport.navigateToItem(0); await fe.objectPage.clickEdit(); await fe.objectPage.clickSave(); }); ``` ### AI-Powered Test Generation ```typescript test('AI discovery', async ({ pramanAI }) => { const context = await pramanAI.discoverPage({ interactiveOnly: true }); const result = await pramanAI.agentic.generateTest( 'Create a purchase order for vendor 100001', page, ); }); ``` ### Custom UI5 Matchers 10 UI5-specific Playwright matchers for expressive assertions. ```typescript const button = await ui5.control({ id: 'saveBtn' }); await expect(button).toBeUI5Enabled(); await expect(button).toHaveUI5Text('Save'); await expect(button).toBeUI5Visible(); ``` ### OData Model + HTTP Operations ```typescript test('OData', async ({ ui5 }) => { // Model-level (reads from the in-browser UI5 OData model) const data = await ui5.odata.getModelData('/PurchaseOrders'); // HTTP-level CRUD is also available with automatic CSRF handling }); ``` ### SM12 Lock Management ```typescript test('locks', async ({ flpLocks }) => { const count = await flpLocks.getNumberOfLockEntries('TESTUSER'); await flpLocks.deleteAllLockEntries('TESTUSER'); // Auto-cleanup on teardown }); ``` ### Structured Error Codes Every Praman error includes a machine-readable `code`, `retryable` flag, and `suggestions[]` array for self-healing tests. wdi5 errors are unstructured strings. ## Step-by-Step Migration Checklist 1. **Install dependencies**: `npm install playwright-praman @playwright/test` 2. **Install browsers**: `npx playwright install` 3. **Create config files**: `praman.config.ts` and `playwright.config.ts` 4. **Set up auth**: Create `tests/auth-setup.ts` using one of the 6 strategies 5. **Convert selectors**: Replace `browser.asControl({ selector: {...} })` with `ui5.control({...})` 6. **Convert assertions**: Replace WDIO `expect` with Playwright `expect` + UI5 matchers 7. **Structure tests**: Convert OPA5 journey `describe/it` blocks to `test` + `test.step()` 8. **Adopt navigation**: Replace `browser.url('#/hash')` with `ui5Navigation` methods 9. **Run and verify**: `npx playwright test` ## FAQ
How similar are wdi5 selectors and Praman selectors? Very similar. Both use an object with `controlType`, `properties`, `bindingPath`, `ancestor`, `descendant`, `searchOpenDialogs`, and other fields. The main difference is that wdi5 wraps the selector inside `{ selector: {...} }` while Praman passes it directly: `ui5.control({...})`. Most selector fields are identical and require no changes beyond removing the `selector` wrapper.
Does Praman support wdi5's labelFor selector? Not as a selector field. Instead, Praman offers the `:labeled()` pseudo-class in its CSS-style locator syntax: `page.locator('ui5=sap.m.Input:labeled("Vendor Name")')`. This checks both the label's `labelFor` association and the control's `ariaLabelledBy` association. See the "Selector Field Mapping" section above for the full conversion example.
What does Praman offer that wdi5 does not? Praman adds 6 built-in auth strategies, 11 FLP navigation methods, Fiori Elements helpers (`fe.listReport`, `fe.objectPage`), OData model and HTTP operations, SM12 lock management, AI-powered test generation, 10 UI5-specific Playwright matchers, and structured error codes with recovery suggestions. See the "New in Praman vs wdi5" section above for details on each.
:::tip[Next steps] - **[Getting Started](./getting-started.md)** -- Install Praman and run your first SAP UI5 test - **[Selectors](./selectors.md)** -- Learn the full `UI5Selector` syntax for targeting controls - **[Control Interactions](./control-interactions.md)** -- Click, fill, select, and assert on UI5 controls :::