--- name: modernize-test-starter description: | Modernize QUnit unit tests and OPA5 integration tests to the UI5 Test Starter concept. Use this skill when: - The linter reports `prefer-test-starter` for *.qunit.html or *.qunit.js files - Test HTML files use manual sap-ui-core.js bootstrapping instead of Test Starter's runTest.js/createSuite.js - Test JS files use Core.ready(), Core.attachInit(), or jsUnitTestSuite instead of sap.ui.define - OPA test HTML files exist with per-test Opa5.extendConfig and manual bootstrapping - An AllJourneys.js orchestrator loads OPA journeys dynamically - OPA journeys call `iStartMyUIComponent` instead of `iStartMyAppInAFrame` - User asks to modernize tests, modernize test infrastructure, or adopt Test Starter Handles unit tests (Core.ready removal, sap.ui.define wrapping) and OPA challenges (page-object imports, Opa5 config, journey orchestration, QUnit 1.x assert modernization, in-window-to-iframe launcher migration). Trigger on: prefer-test-starter warnings, test modernization requests, iStartMyUIComponent. --- # Modernize to Test Starter This skill modernizes a UI5 project's entire test infrastructure — both unit tests and OPA5 integration tests — to the [Test Starter concept](https://ui5.sap.com/#/topic/032be2cb2e1d4115af20862673bedcdb). Reference: [Blog post: Simplify Your Test Setup](https://community.sap.com/t5/technology-blog-posts-by-sap/simplify-your-test-setup-introducing-the-test-starter-concept-for-your-ui5/ba-p/14303076) ## Why Modernize The Test Starter replaces per-test HTML bootstrapping with a single generic test page and a declarative `testsuite.qunit.js` configuration. Benefits: eliminates boilerplate HTML/JS, handles QUnit/Sinon/coverage setup automatically, ensures CSP compliance, provides consistent configuration, enables per-test isolation and parallel execution. ## NEVER Skip Test Starter Modernization OPA5 test HTML files are ALWAYS convertible — they follow a mechanical pattern. Invalid excuses: "complex mock server" (lives in host, not entry point), "sap.ui.define monkey-patching" (same), "FLP sandbox bootstrap" (same), "too many files" (batch-process), "too complex" (invalid). | Type | Example | Action | |------|---------|--------| | Test Driver | `opa//.qunit.html` | **CONVERT** to Test Starter | | Test Host | `test.html` | **KEEP AS-IS** (app-under-test in iframe) | | Test Suite | `testsuite.qunit.html` | **REPLACE** with testsuite.qunit.js | | Dev Sandbox | `flpSandbox.html` | **IGNORE** | The only real technical challenge: OPA5 entry points may use `jQuery.sap.require()` for QUnit bootstrap libs — replace with `sap.ui.require` or note that Test Starter loads them automatically. ## Pre-requisites Read `manifest.json` → `sap.app/id`. This is your namespace: - **``** — dots → slashes (e.g., `my.app` → `my/app`) - **``** — raw value with dots ## Phase 0: Detection ### 0.1 Check for unit tests Look for `webapp/test/unit/` containing `unitTests.qunit.html`, `unitTests.qunit.js`, `allTests.js`, `AllTests.js`, or `allTests.qunit.js`. Identify **all legacy aggregator files** — JS files whose only purpose is loading other test modules via `sap.ui.require`/`sap.ui.define` with no actual test logic (`QUnit.module`, `QUnit.test`, `assert.*`). Their contents will be inlined into `unitTests.qunit.js` and the files deleted. ### 0.2 Classify OPA launcher and FLP sandbox presence Phase 5b is gated on `needsIframeMigration` = (`launcher === "in-window"` AND `flpSandbox === true`). ```bash node /scripts/parse-testsuite.js --detect-launcher webapp/test > /tmp/launcher.json ``` Returns `{ "launcher": "iframe"|"in-window"|"mixed"|"none", "flpSandbox": true|false, "needsIframeMigration": true|false }`. | `launcher` | `flpSandbox` | Action | |---|---|---| | `iframe` | any | Pattern I — Phase 5 only, skip 5b | | `in-window` | `true` | Pattern U — Phase 5 + Phase 5b | | `in-window` | `false` | Plain in-window — skip 5b, leave `iStartMyUIComponent` | | `mixed` | any | **Halt.** Log to `MODERNIZATION_ISSUES.md`, ask developer to reconcile | | `none` | any | No OPA tests — skip OPA phases | **Pattern U + Pattern B is unsupported.** If `needsIframeMigration === true` and Phase 0.3 reports Pattern B, halt and surface to the developer. ### 0.3 Check for OPA tests — identify pattern **Pattern A — "Single HTML + AllJourneys"**: `opaTests.qunit.html` + `AllJourneys.js` orchestrator. **Pattern B — "Many Individual HTML Files"**: Multiple `*.qunit.html` under `webapp/test/opa/`. Detection: ```bash find webapp/test -name "AllJourneys.js" -o -name "AllJourneys.json" find webapp/test/opa -name "*.qunit.html" -type f 2>/dev/null | wc -l ``` ### 0.4 Run the parse script ```bash node /scripts/parse-testsuite.js ``` Outputs: `pattern` (A/B), `summary` (counts), `entries` (module→config mapping), `opaConfig` (for Pattern A). Save this — it drives the rest. ### 0.5 Report bootstrap overrides ```bash node /scripts/parse-testsuite.js --scan-bootstrap-overrides webapp/test > /tmp/bootstrap-overrides.json ``` Detects: `sap.ui.define = ...`, `sap.ui.require = ...`, `defineModuleSync(...)`. Append each finding to `MODERNIZATION_ISSUES.md`. Do NOT rewrite overrides — reporting is the deliverable. ## Phase 1: Create testsuite.qunit.js (Main) Lists all tests: unit tests delegated via `"unit/unitTests"`, OPA journeys listed individually. Build OPA entries from parse script output — key = relative path from `webapp/test/` without `.qunit.js`. ```javascript sap.ui.define(function() { "use strict"; return { name: "QUnit test suite for ", defaults: { page: "ui5://test-resources//Test.qunit.html?testsuite={suite}&test={name}", qunit: { version: 2 }, sinon: { version: 4 }, ui5: { theme: "sap_horizon" }, loader: { map: { "*": { "sap/ui/thirdparty/sinon": "sap/ui/thirdparty/sinon-4", "sap/ui/thirdparty/sinon-qunit": "sap/ui/qunit/sinon-qunit-bridge" }}, paths: { "": "../" } }, coverage: { only: [""], never: ["/test"] } }, tests: { // ----- Unit Tests ----- "unit/unitTests": { title: "Unit Tests" }, // ----- OPA Integration Tests ----- "integration/NavigationJourney": { title: "Navigation Journey" } } }; }); ``` Read `references/testsuite-configuration.md` for detailed option explanations. Key points: - `page` MUST use `ui5://` protocol prefix - No `module` property needed — Test Starter appends `.qunit` to entry keys - `{suite}` and `{name}` placeholders are mandatory ### Additional loader paths **MANDATORY.** Extract ALL resource root mappings from ALL test HTML files: ```bash grep -rh "data-sap-ui-resourceroots" webapp/test/ --include="*.html" ``` Convert dot-notation keys to slash-notation, add to `loader.paths`. All values resolve relative to `Test.qunit.html` (at `webapp/test/`) — recompute paths from subdirectory HTMLs accordingly. If two HTMLs define the same key with different values, prefer the main `testsuite.qunit.html`/`opaTests.qunit.html` value. Minority tests get per-test `loader.paths` overrides. ## Phase 2: Create Test.qunit.html ```html
``` ## Phase 3: Update testsuite.qunit.html ```html QUnit test suite for <NAMESPACE-WITH-DOTS> ``` ## Phase 4: Modernize Unit Test JS Files ### 4.0 FIRST — Delete redundant aggregators A redundant aggregator has NO actual test logic — only `sap.ui.require([deps], function() { QUnit.start(); })` or load-only `sap.ui.define`. `QUnit.config.autostart = false` + `QUnit.start()` are boot scaffolding, NOT test logic. **Action**: Note the test modules they load (for `unitTests.qunit.js`), then **DELETE** both `.js` and companion `.html` immediately. ### 4.1 Convert and rename real test files Files with actual test logic (`QUnit.module`, `QUnit.test`, `assert.*`) need: 1. **Rename** to `.qunit.js` suffix (e.g., `App.controller.js` → `App.controller.qunit.js`) 2. **Convert** to `sap.ui.define` (remove `QUnit.config.autostart`, `Core.ready` wrappers) ```javascript // Before (App.controller.js) QUnit.config.autostart = false; sap.ui.getCore().attachInit(function() { sap.ui.require(["my/app/model/formatter"], function(formatter) { QUnit.module("formatter"); QUnit.test("formatValue", function(assert) { ... }); }); }); // After (App.controller.qunit.js) sap.ui.define(["my/app/model/formatter"], function(formatter) { "use strict"; QUnit.module("formatter"); QUnit.test("formatValue", function(assert) { ... }); }); ``` ### 4.2 Create unitTests.qunit.js aggregator Directly lists all real unit test modules (from deleted aggregators + any additional `.qunit.js` files): ```javascript sap.ui.define([ "./controller/Main.qunit", "./model/formatter.qunit" ]); ``` Rules: relative `./` paths, include `.qunit` suffix (without `.js`). ### jsUnitTestSuite conversion If old `testsuite.qunit.js` used `jsUnitTestSuite`, it's already replaced in Phase 1. Delete old content. ## Phase 5: Modernize OPA Tests ### Pattern A — Single HTML + AllJourneys Read `references/pattern-a-modernization.md` for detailed instructions. 1. **Create OpaSetup.js** from AllJourneys.js — extract `Opa5.extendConfig` + page object imports. OpaSetup MUST NOT import `sap/ui/test/opaQunit`. 2. **Rename journey files** to `.qunit.js` 3. **Update journeys** — add `"./OpaSetup"` as side-effect dependency (relative path) 4. **Handle autoWait overrides** — per-journey `Opa5.extendConfig` 5. **Preserve testLibs config** in OpaSetup.js ```javascript // OpaSetup.js (no opaQunit!) sap.ui.define(["sap/ui/test/Opa5", "test-resources//integration/pages/App" ], function(Opa5) { "use strict"; Opa5.extendConfig({ viewNamespace: ".view.", autoWait: true }); }); // Journey file (opaQunit here, OpaSetup relative) sap.ui.define(["sap/ui/test/opaQunit", "sap/ui/test/Opa5", "./OpaSetup" ], function(opaTest, Opa5) { "use strict"; // opaTest(...) calls }); ``` ### Pattern B — Many Individual HTML Files Read `references/pattern-b-modernization.md` for detailed instructions. 1. **Inventory utility modules** calling `Opa5.createPageObjects` 2. **Create OpaSetup.js** — consolidate utility imports + `Opa5.extendConfig` 3. **Rename journeys** to `.qunit.js`, add OpaSetup dependency 4. **Handle autoWait overrides** from parse script's `autoWaitFalseFiles` 5. **Multi-module HTML files** — emit ONE testsuite entry per module (never invent `*Combined` names). See `references/pattern-b-modernization.md` Step 6. ## Phase 5b: Migrate in-window OPA to bare-Component iframe **Run only when Phase 0.2 reported `needsIframeMigration: true`.** Read `references/pattern-u-iframe-migration.md` for full instructions. Summary: 1. **Create `opaIframe.qunit.html` + `opaIframeBoot.js`** — bare-Component iframe. HTML loads `sap/ushell` sandbox.js for stubs, no `sap-ushell-config`. Boot module runs `mockserver.init()` → `new ComponentContainer(...).placeAt("content")`. CSP-clean: `data-sap-ui-oninit="module:..."`, no inline script. 2. **Rewrite `arrangements/Common.js`** — `iStartMyApp` → `iStartMyAppInAFrame(...)`. Drop mockserver import, drop `_clearSharedData`. 3. **Rewrite journeys** — `iStartMyUIComponent({...})` → `iStartMyApp()` (forward hash/autoWait). 4. **Strip parent-frame mockserver init from OpaSetup.js** — now boots in iframe. 5. **Cross-window control instantiation** — UI5 controls in `waitFor` callbacks must resolve via `Opa5.getWindow().sap.ui.require(...)`. OPA-safe deps kept in parent: `sap/ui/test/*`, `sap/ui/core/routing/History`. Gate: `node /scripts/detect-cross-window-imports.js `. 6. **Cross-window jQuery/DOM** — replace bare `$(...)`, `document.*`, `window.*` with `Opa5.getJQuery()(...)`, `Opa5.getWindow().*`. 7. **Routing helpers** — plain Component-router hash, no `#app-tile&/` prefix. 8. **Mockserver `sap-message` envelopes** — POST handlers need `sap-message` header for message collectors. 9. **ErrorHandler null-guard** — guard `getElementsByTagName("message")[0].firstChild.data` against null. 10. **Do NOT register `/test/integration/opaIframe` in `loader.paths`**. Items 5–9 are project-specific; apply mechanically per detection grep in reference file. ## Phase 6: Delete Old Files ### Unit test files - Delete `unitTests.qunit.html`, `legacyTests.qunit.html` (legacy bootstraps) - Verify all redundant aggregators from Phase 4.0 are deleted ### OPA files — Pattern A - Delete `opaTests.qunit.html`, `AllJourneys.js`, `AllJourneys.json` ### OPA files — Pattern U (only if `needsIframeMigration`) - Delete `flpSandbox.qunit.html` if it exists from prior attempt - Confirm no journey still calls `iStartMyUIComponent` ### OPA files — Pattern B - Delete all `*.qunit.html` under `webapp/test/opa/` (count must match `summary.totalActive`) ### Do NOT delete - `testsuite.qunit.html` (updated), `Test.qunit.html` (created) ## Phase 7: Verify 1. **Count check**: OPA entries in `testsuite.qunit.js` match parse script's OPA total. 2. **Dangling-entry check**: `node /scripts/check-dangling-entries.js webapp/test` — exit 0 = OK. 3. **Linter**: `npx @ui5/linter` — no `prefer-test-starter` warnings remain. 4. **Structural review**: `Test.qunit.html` → `runTest.js`; `testsuite.qunit.html` → `createSuite.js`; `testsuite.qunit.js` → unit delegate + OPA journeys; all unit files use `sap.ui.define`; `OpaSetup.js` exists; every journey imports it; no stale HTML remains. 5. **Pattern U verification** (if `needsIframeMigration`): - `opaIframe.qunit.html` + `opaIframeBoot.js` exist; no inline `