🧪modernize-test-starter
- プラグイン
- ui5-modernization
- ソース
- GitHub で見る ↗
説明
QUnitユニットテストおよびOPA5インテグレーションテストを、UI5 Test Starterコンセプトへ移行・モダナイズします。 次のような場合に使用: - リンターが `*.qunit.html` または `*.qunit.js` ファイルに対して `prefer-test-starter` を報告している - テスト用HTMLファイルが、Test Starterの `runTest.js` / `createSuite.js` ではなく、手動による `sap-ui-core.js` のブートストラップを使用している - テスト用JSファイルが、`sap.ui.define` ではなく `Core.ready()`、`Core.attachInit()`、または `jsUnitTestSuite` を使用している - OPAテスト用HTMLファイルが、テストごとの `Opa5.extendConfig` と手動ブートストラップを用いて存在している - `AllJourneys.js` オーケストレーターがOPAジャーニーを動的にロードしている - OPAジャーニーが `iStartMyAppInAFrame` ではなく `iStartMyUIComponent` を呼び出している - ユーザーがテストのモダナイズ、テストインフラのモダナイズ、またはTest Starterの導入を依頼している 対応範囲: - **ユニットテスト**: `Core.ready` の除去、`sap.ui.define` によるラッピング - **OPA固有の課題**: ページオブジェクトのインポート、`Opa5` の設定、ジャーニーのオーケストレーション、QUnit 1.x アサーションのモダナイズ、インウィンドウ方式からiframe方式へのランチャー移行 トリガー条件: `prefer-test-starter` 警告、テストモダナイズのリクエスト、`iStartMyUIComponent` の使用
原文を表示
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.
ユースケース
- ✓QUnitテストをTest Starterへ移行する
- ✓手動ブートストラップをTest Starterに置き換える
- ✓OPA5テストをモダナイズする
- ✓OPAジャーニーのオーケストレーションを改善する
- ✓インウィンドウ方式からiframe方式へ移行する
本文
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.
Reference: Blog post: Simplify Your Test Setup
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 code
- Handles QUnit, Sinon, and coverage setup automatically
- Ensures CSP compliance (no inline scripts)
- Provides consistent test configuration across all tests
- Enables per-test isolation and parallel execution
NEVER Skip Test Starter Modernization
OPA5 test HTML files are ALWAYS convertible. They follow a mechanical pattern: bootstrap → require utils → extendConfig → require test modules → QUnit.start().
Common "excuses" that are INVALID reasons to skip:
- "Complex mock server setup" → Mocks live in a SEPARATE host file (e.g.,
test*.html), NOT in the entry point HTML files. The host file stays unchanged. - "sap.ui.define monkey-patching" → Same: lives in the host, not the entry points.
- "FLP sandbox bootstrap" → Same: lives in the host, not the entry points.
- "125+ files is too many" → They're mechanical. Batch-process them.
- "Custom service implementations" → Loaded by the host, not by entry points. Entry points just declare a resource root pointing to the host.
- "Too complex" or "too many hurdles" → INVALID. This modernization has been done successfully on projects of this scale before.
Test Driver vs Test Host Distinction
| Type | Example | Contains | Action |
|---|---|---|---|
| Test Driver | opa/<Area>/<SubArea>/<TestName>.qunit.html |
Opa5 config + test module requires | CONVERT to Test Starter |
| Test Host | test<ServiceName>.html |
Mock servers, ushell config, monkey-patching | KEEP AS-IS |
| Test Suite | testsuite.qunit.html |
Manual addTestPage() list | REPLACE with testsuite.qunit.js |
| Dev Sandbox | flpSandbox.html |
Developer tool, not a test | IGNORE |
The test host HTML (e.g., test<ServiceName>.html) is NOT an entry point — it is the application-under-test started inside an iframe by the OPA initializer's iStartMyApp(). Do NOT attempt to convert the host file. Only convert the test DRIVER HTML files.
What Actually Blocks (vs What Doesn't)
The only real technical challenge: OPA5 entry points may use jQuery.sap.require() (deprecated) for QUnit bootstrap libs. The shared OpaSetup.js needs to replace these with sap.ui.require equivalents — or note that QUnit/qunit-css are loaded via Test Starter config automatically and can simply be removed.
Everything else is mechanical transformation. Do NOT refuse or defer this modernization.
Pre-requisites
Read the project's manifest.json to get sap.app/id. This is your namespace:
<NAMESPACE>— dots replaced by slashes (e.g.,my.app→my/app)<NAMESPACE-WITH-DOTS>— raw value with dots (e.g.,my.app)
Phase 0: Detection
Scan the project to classify what exists before making changes.
0.1 Check for unit tests
Look for a webapp/test/unit/ directory containing unitTests.qunit.html, unitTests.qunit.js, allTests.js, AllTests.js, or allTests.qunit.js. Identify all legacy aggregator files — these are JS files whose only purpose is to load other test modules via sap.ui.require or sap.ui.define dependencies, with no actual QUnit test logic (no QUnit.module, QUnit.test, or assert.* calls). Common names include allTests.js, AllTests.js, legacyTests.qunit.js, but ANY file matching this pattern is a legacy aggregator. Their contents will be inlined into unitTests.qunit.js and the files deleted.
0.2 Classify the OPA launcher (iframe vs in-window) and FLP sandbox presence
Phase 5b (bare-Component iframe migration) is gated on two signals, not one:
- The OPA app-launcher shape —
iStartMyAppInAFrame(iframe) vsiStartMyUIComponent(in-window). - Whether any legacy test HTML loads the FLP sandbox — either
sap/ushell/bootstrap/sandbox.js(or olderflpSandbox.js) or declareswindow["sap-ushell-config"].
The bare-Component iframe only buys something when the app actually depends on the FLP runtime. Plain in-window apps with no FLP coupling stay on iStartMyUIComponent — Phase 5b would force them into an iframe they don't need.
Run the combined scan:
node <skill-dir>/scripts/parse-testsuite.js --detect-launcher webapp/test \
> /tmp/launcher.json
The script returns:
{
"launcher": "iframe" | "in-window" | "mixed" | "none",
"flpSandbox": true | false,
"needsIframeMigration": true | false,
...
}
needsIframeMigration is true iff launcher === "in-window" AND flpSandbox === true. That single flag drives the decision:
launcher |
flpSandbox |
needsIframeMigration |
Action |
|---|---|---|---|
iframe |
any | false |
Pattern I. Proceed with Phase 5 only; skip Phase 5b. |
in-window |
true |
true |
Pattern U. Phase 5 (Pattern A wiring) plus Phase 5b (iframe migration). |
in-window |
false |
false |
Plain in-window app. Skip Phase 5b. Run Phase 5 for testsuite/journey wiring; leave iStartMyUIComponent calls untouched. |
mixed |
any | false |
Halt. Append a section to MODERNIZATION_ISSUES.md listing every iframe and in-window hit, ask the developer to reconcile to one shape, then re-run. |
none |
any | false |
Project has no OPA tests. Skip all OPA phases (5, 5b, OPA parts of 6/7). |
Pattern U + Pattern B is unsupported. This skill only handles needsIframeMigration === true projects whose Phase 0.3 classification is Pattern A (single AllJourneys.js aggregator). If needsIframeMigration === true and Phase 0.3 reports Pattern B, halt and surface to the developer — Phase 5b assumes a single shared Common.js / OpaSetup.js to rewrite.
Save the combined verdict; it gates Phase 5b and several Completion Checklist rows.
0.3 Check for OPA tests and identify the pattern
Pattern A — "Single HTML + AllJourneys": The most common pattern.
webapp/test/integration/opaTests.qunit.htmlexists (single bootstrap file)AllJourneys.jsorchestrates Opa5.extendConfig and dynamically loads journeys- Often has
AllJourneys.jsonlisting journey names
Pattern B — "Many Individual HTML Files": Less common, larger projects.
- Multiple
*.qunit.htmlfiles underwebapp/test/opa/(one per test) - Each HTML has its own
Opa5.extendConfigand utility module imports
Detection:
find webapp/test -name "AllJourneys.js" -o -name "AllJourneys.json"
find webapp/test/integration -name "opaTests.qunit.html"
find webapp/test/opa -name "*.qunit.html" -type f 2>/dev/null | wc -l
If AllJourneys.js exists → Pattern A. If many HTML files under opa/ → Pattern B.
0.4 Run the parse script
This skill bundles a script that extracts test entries from the legacy testsuite. It auto-detects the pattern:
node <skill-dir>/scripts/parse-testsuite.js <testsuite.qunit.html> <test-base-dir> <namespace>
The script outputs a JSON object with:
pattern—"A"or"B"(auto-detected)summary— counts of active, commented-out, autoWait:false, and multi-journey entriesentries— complete mapping from module path to{ title, skip?, ... }- For Pattern A:
opaConfig— extractedOpa5.extendConfigdetails and page object imports from AllJourneys.js
Save this output — it drives the rest of the modernization.
0.5 Report bootstrap overrides for manual review
Some test host HTML files (or the testsuite HTML itself) monkey-patch the UI5 module loader — typically to mock a module that is missing from the DIST layer (e.g. sap/ushell_abap/pbServices/ui2/Page). These patterns CANNOT be migrated mechanically because the right replacement (e.g. sap.ui.predefine, deletion, refactor) depends on what the original code was trying to achieve and which modules it must intercept. They must be reviewed by a human.
Run the bootstrap-override scan and append every finding to MODERNIZATION_ISSUES.md at the project root:
node <skill-dir>/scripts/parse-testsuite.js --scan-bootstrap-overrides webapp/test \
> /tmp/bootstrap-overrides.json
The scan reports any of:
sap.ui.define = ...(loader-define override)sap.ui.require = ...(loader-require override)sap.ui.loader._.defineModuleSync(...)or baredefineModuleSync(...)
For each finding, append a section to MODERNIZATION_ISSUES.md (create the file if it does not exist) using this template:
## Bootstrap override — manual review required
- File: `<path>` (line `<n>`)
- Pattern: `<patternId>`
- Snippet: `<trimmed line>`
- Note: <patternId-specific note from the scan output>
- Action: not auto-migrated. Review the original intent (usually mocking a missing-from-DIST module). Replace `defineModuleSync` / `sap.ui.define` overrides with `sap.ui.predefine` placed before any `sap.ui.require`, or remove the workaround if the missing module is now available.
Do NOT attempt to rewrite the override during this skill's run. Reporting it is the deliverable; the developer decides the correct fix afterwards.
If the scan finds zero overrides, skip writing to MODERNIZATION_ISSUES.md.
Phase 1: Create testsuite.qunit.js (Main)
The main testsuite lists all tests with a hybrid approach:
- Unit tests: Delegated via a single
"unit/unitTests"entry (which loads all unit test modules throughunitTests.qunit.js) - OPA/integration tests: Listed individually — one entry per journey file (e.g.,
"integration/NavigationJourney")
This gives full visibility of every OPA journey (which are typically the large, slow tests developers want to run selectively) while keeping unit tests bundled as a fast-running group.
Build the OPA entries from the parse script output. Each entry key is the relative path from webapp/test/ without the .qunit.js suffix. Test Starter appends .qunit automatically to resolve the module.
// Full example — unit delegated, OPA journeys listed individually:
sap.ui.define(function() {
"use strict";
return {
name: "QUnit test suite for <NAMESPACE-WITH-DOTS>",
defaults: {
page: "ui5://test-resources/<NAMESPACE>/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: {
"<NAMESPACE>": "../"
}
},
coverage: {
only: ["<NAMESPACE>"],
never: ["<NAMESPACE>/test"]
}
},
tests: {
// ----- Unit Tests -----
"unit/unitTests": {
title: "Unit Tests"
},
// ----- OPA Integration Tests -----
"integration/NavigationJourney": {
title: "Navigation Journey"
},
"integration/SearchJourney": {
title: "Search Journey"
}
}
};
});
If the project has NO integration/OPA tests, the test entries contain only "unit/unitTests". If unit tests don't exist (rare), only individual OPA entries appear.
Use section comments (// ----- Unit Tests -----, // ----- OPA Integration Tests -----) to visually group entries.
Read references/testsuite-configuration.md for detailed explanation of each configuration option.
Key points:
- The
pageproperty MUST use theui5://protocol prefix — without it, Test Starter cannot resolve test pages - No
moduleproperty is needed — Test Starter appends.qunitto entry keys, resolving"integration/NavigationJourney"tointegration/NavigationJourney.qunit.js - Files referenced by testsuite entry keys must follow the
.qunit.jssuffix convention - Files loaded only as
sap.ui.definedependencies (utility modules, page objects, arrangement classes) keep their.jsextension
Additional loader paths
MANDATORY step. Extract ALL resource root mappings from ALL test HTML files before creating testsuite.qunit.js:
grep -rh "data-sap-ui-resourceroots" webapp/test/ --include="*.html"
Parse every key-value pair from the JSON attributes. Convert dot-notation keys to slash-notation and add them to loader.paths.
Path adjustment: All loader.paths values resolve relative to Test.qunit.html (located at webapp/test/). When a resource root is extracted from an HTML file in a subdirectory (e.g., webapp/test/opa/Area/Test.qunit.html with path "../../flpSandboxMockServer"), you must recompute the path relative to webapp/test/. To do this:
- Determine what the original relative path resolves to from the source HTML's directory
- Re-express that target relative to
webapp/test/
Example: webapp/test/opa/SalesOrder/CreateSalesOrder.qunit.html has "flpSandboxMockServer": "../../flpSandboxMockServer". From test/opa/SalesOrder/, ../../flpSandboxMockServer resolves to test/flpSandboxMockServer. Relative to Test.qunit.html at test/, this becomes "./flpSandboxMockServer" (or simply "flpSandboxMockServer").
Example: webapp/test/integration/opaTests.qunit.html has "flpSandboxMockServer": "../flpSandboxMockServer". From test/integration/, ../flpSandboxMockServer resolves to test/flpSandboxMockServer. Relative to test/, this becomes "./flpSandboxMockServer".
The app's own paths ("<NAMESPACE>" and "<NAMESPACE>/app") are always needed but NOT sufficient. Common additional paths that must be carried over:
- Fiori Elements test libraries (
sap/suite/ui/generic/template/integration/testLibrary) - Generic
test-resourcesmappings - Reuse library test aliases
If two HTML files define the same resource root key with different values, compare which path the majority of tests use. Prefer the value from the main testsuite.qunit.html or opaTests.qunit.html over individual test HTMLs. If a minority of tests needs a different mapping, use a per-test loader.paths override in their testsuite entry rather than changing the default.
Phase 2: Create Test.qunit.html
Create webapp/test/Test.qunit.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script
src="../resources/sap/ui/test/starter/runTest.js"
data-sap-ui-resource-roots='{
"test-resources.<NAMESPACE-WITH-DOTS>": "./"
}'
></script>
</head>
<body class="sapUiBody">
<div id="qunit"></div>
<div id="qunit-fixture"></div>
</body>
</html>
This single file replaces ALL individual test HTML files. Test Starter uses URL query parameters to select which test to run.
Phase 3: Update testsuite.qunit.html
Replace the contents of webapp/test/testsuite.qunit.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>QUnit test suite for <NAMESPACE-WITH-DOTS></title>
<script
src="../resources/sap/ui/test/starter/createSuite.js"
data-sap-ui-testsuite="test-resources/<NAMESPACE>/testsuite.qunit"
data-sap-ui-resource-roots='{
"test-resources.<NAMESPACE-WITH-DOTS>": "./"
}'
></script>
</head>
<body>
</body>
</html>
This replaces qunit-redirect.js or sap-ui-core.js bootstrapping with createSuite.js.
Phase 4: Modernize Unit Test JS Files
4.0 FIRST — Identify and delete redundant aggregators
Before converting any file, scan webapp/test/unit/ for redundant aggregators. A redundant aggregator is a JS file that:
- Uses
sap.ui.require([...], function() { QUnit.start(); })to load other test modules, OR - Uses
sap.ui.define([...])to list dependencies with no test body, OR - Contains NO actual test logic (
QUnit.module,QUnit.test,assert.*calls)
IMPORTANT: QUnit.config.autostart = false and QUnit.start() are NOT test logic — they are boot scaffolding. A file that ONLY does sap.ui.require([deps], function() { QUnit.start(); }) is a redundant aggregator, even though it mentions QUnit.
Common filenames: allTests.js, AllTests.js, legacyTests.qunit.js, allTests.qunit.js — but ANY file matching this "load-only, no tests" pattern is a redundant aggregator.
Action: Note the test modules they load (these will go into unitTests.qunit.js), then DELETE the aggregator file immediately. Do NOT convert it to sap.ui.define format. Do NOT add QUnit.test stubs. Do NOT keep it as a test entry. Also delete its companion HTML file (e.g., legacyTests.qunit.html). DELETE BOTH FILES.
Example — this is a redundant aggregator (DELETE both .js and .html):
QUnit.config.autostart = false;
sap.ui.require([
"my/app/test/unit/controller/App.controller"
], function() {
"use strict";
QUnit.start();
});
It has no QUnit.module/QUnit.test of its own — it just loads another module and starts QUnit. Delete it.
4.1 Convert and rename real test files
Unit test JS files that contain actual test logic (QUnit.module, QUnit.test, assert.*) and use Core.ready(), Core.attachInit(), or sap.ui.require with QUnit.start() need TWO changes:
- Rename the file to add
.qunit.jssuffix (e.g.,App.controller.js→App.controller.qunit.js) - Convert the content to
sap.ui.defineformat (remove QUnit.config.autostart, Core.ready wrappers)
⚠️ CRITICAL — File Rename: Every real unit test file MUST be renamed to .qunit.js suffix. This is required because Test Starter resolves test entries by appending .qunit to the module path. Without the rename, the test cannot be found at runtime.
Examples:
controller/App.controller.js→controller/App.controller.qunit.jsmodel/formatter.js→model/formatter.qunit.jsutil/Helper.js→util/Helper.qunit.js
Before — old style with Core.ready (webapp/test/unit/controller/App.controller.js):
QUnit.config.autostart = false;
sap.ui.getCore().attachInit(function() {
"use strict";
sap.ui.require([
"my/app/model/formatter"
], function(formatter) {
QUnit.module("formatter");
QUnit.test("formatValue", function(assert) {
assert.equal(formatter.formatValue(1), "One");
});
});
});
After — Test Starter style (webapp/test/unit/controller/App.controller.qunit.js):
sap.ui.define([
"my/app/model/formatter"
], function(formatter) {
"use strict";
QUnit.module("formatter");
QUnit.test("formatValue", function(assert) {
assert.equal(formatter.formatValue(1), "One");
});
});
4.2 Create unitTests.qunit.js aggregator
The main testsuite entry "unit/unitTests" resolves to unit/unitTests.qunit.js. This file must directly list all real unit test modules (files with QUnit.module/QUnit.test).
Build the list from:
- Test modules extracted from deleted aggregators (Step 4.0)
- Any additional
.qunit.jsfiles inwebapp/test/unit/that contain actual tests
Do NOT include deleted aggregator files in this list.
After (unitTests.qunit.js — directly lists all tests):
sap.ui.define([
"./controller/Main.qunit",
"./model/formatter.qunit"
]);
Key rules for the aggregator:
- Use relative paths starting with
./, not absolute namespace paths - Add the
.qunitsuffix to each dependency (without.js) because the actual files were renamed to.qunit.jsin Step 4.1 - Example: if file was renamed to
controller/App.controller.qunit.js, reference it as"./controller/App.controller.qunit"
jsUnitTestSuite conversion
If the old testsuite.qunit.js used jsUnitTestSuite, it's already replaced in Phase 1. Delete the old content.
Phase 5: Modernize OPA Tests
This phase differs based on the detected pattern. Read the full instructions in the corresponding reference file.
Pattern A — Single HTML + AllJourneys
Read references/pattern-a-modernization.md for detailed instructions.
Summary:
- Create OpaSetup.js from AllJourneys.js — extract
Opa5.extendConfigand all page object/utility imports. OpaSetup.js must NOT importsap/ui/test/opaQunit— that module belongs in each individual journey file. - Rename journey files to
.qunit.jssuffix so Test Starter can resolve them without amoduleoverride - Update journey files — add OpaSetup as a side-effect dependency using relative path
"./OpaSetup"(same directory). Do NOT usetest-resources/for same-directory imports. - Handle autoWait overrides — journeys needing
autoWait: falseget a per-journeyOpa5.extendConfigoverride - Preserve testLibs config — Fiori Elements
testLibssettings move to OpaSetup.js
Correct OpaSetup.js structure (page objects use test-resources/, but opaQunit is absent):
sap.ui.define([
"sap/ui/test/Opa5",
"test-resources/<NAMESPACE>/integration/pages/App"
], function(Opa5) {
"use strict";
Opa5.extendConfig({
viewNamespace: "<APP-NAMESPACE>.view.",
autoWait: true
});
});
Correct journey file structure (opaQunit here, OpaSetup via relative path):
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.
Summary:
- Inventory utility modules — find all modules that call
Opa5.createPageObjects(side-effect imports) - Create OpaSetup.js — consolidate all utility imports +
Opa5.extendConfigfrom the HTML files - Rename journey files to
.qunit.jssuffix and add OpaSetup as a side-effect dependency - Handle autoWait overrides — use the parse script's
autoWaitFalseFileslist - Handle multi-module HTML files — when a legacy
*.qunit.htmlloads more than one journey module in a singlesap.ui.require, emit ONE testsuite entry per module. Never invent a synthetic combined name (e.g.<First>Combined) — the file does not exist and the resulting entry is dangling. The parse script does this automatically via_fromMultiModuleHtml. Halt if any of the loaded modules has no corresponding.qunit.jsfile underwebapp/test/. Seereferences/pattern-b-modernization.mdStep 6.
Phase 5b: Migrate in-window OPA launcher to bare-Component iframe
Run this phase only when Phase 0.2 reported needsIframeMigration: true (i.e. launcher === "in-window" AND flpSandbox === true). Skip entirely for any other combination — including plain in-window apps with no FLP sandbox load, where iStartMyUIComponent should stay as-is.
Phase 5b assumes Phase 5 has already produced OpaSetup.js, renamed journey files, and the main testsuite.qunit.js — it then rewrites the launcher path so journeys run inside a fresh same-origin iframe loading the Component directly (no FLP shell).
Read references/pattern-u-iframe-migration.md for the full step-by-step instructions. Summary:
- Create
webapp/test/integration/opaIframe.qunit.html+opaIframeBoot.js— bare-Component iframe entry. HTML loadssap/ushellsandbox.js for API stubs but defines nosap-ushell-config, so no FLP shell renderer is built. Bootstrap usesdata-sap-ui-oninit="module:<NAMESPACE>/test/integration/opaIframeBoot"(no inline<script>, CSP-clean) — the boot module runsmockserver.init()thennew ComponentContainer(...).placeAt("content"). - Rewrite
arrangements/Common.js—iStartMyAppnow callsiStartMyAppInAFrame({ source: getFrameUrl(hash), … }). Drop thelocalService/mockserverimport, drop any_clearSharedDatahelper that resets parent-frameODataModel.mSharedData, drop the in-windowcomponentConfig. - Rewrite every journey file —
Given.iStartMyUIComponent({...})→Given.iStartMyApp()(forwardhash/autoWaitif originally passed). - Strip parent-frame mockserver init from
OpaSetup.js— mockserver now boots inside the iframe. - Cross-window control instantiation in page objects — UI5 controls instantiated in
waitForcallbacks must be resolved through the iframe's loader:Opa5.getWindow().sap.ui.require("sap/m/Token"). Drop those dependencies from the parentsap.ui.define. Iterate every file underwebapp/test/integration/pages/(do not rely on which files you happened to edit for other reasons). The gate detects misuse by usage shape, not by module-path enumeration — UI5 has too many libraries (sap/m,sap/ui/core,sap/uxap,sap/suite,sap/viz,sap/ndc,sap/f,sap/ui/layout,sap/gantt, project libs …) to whitelist. OPA-safe dep paths kept in the parent:sap/ui/test/*andsap/ui/core/routing/History. Runnode <skill-dir>/scripts/detect-cross-window-imports.js <project-root>after the rewrite — non-zero exit halts Phase 5b. Seereferences/pattern-u-iframe-migration.md§5b.6.2. - Cross-window jQuery / DOM lookups — replace bare
$(...),jQuery(...),document.*,window.*references that target app-rendered DOM withOpa5.getJQuery()(...),Opa5.getWindow().document.*,Opa5.getWindow().*. Detection is folded into the samedetect-cross-window-imports.jsgate run for item 5 — bare DOM/jQuery lines that don't already route throughOpa5.getJQuery()/Opa5.getWindow()are reported alongside the constructor /instanceoffindings. - Routing helpers — plain Component-router hash, no
#app-tile&/prefix, nosap.ushell.Container.setDirtyFlag(false). - Mockserver
sap-messageenvelopes — function-import POST handlers consumed by an app-side message collector that dereferencesaErrorMsg[0]need asap-messageheader so the collector array is non-empty. - ErrorHandler null-guard for non-XML responses — guard
xmlDoc.getElementsByTagName("message")[0].firstChild.dataagainst null nodes; fall back to the raw response text. Ship with the migration or flag inMODERNIZATION_ISSUES.md. - Do NOT register
<NAMESPACE>/test/integration/opaIframeinloader.paths—sap.ui.require.toUrl("test-resources/<NAMESPACE>/integration/opaIframe")resolves through the existing resource root. A custom alias breaks the packaged-WAR path.
Items 5–9 are project-specific in scope: items 5 and 6 are gated by detect-cross-window-imports.js; items 7–9 use the detection greps in the reference file. Apply each match mechanically. The exact set of UI5 classes / endpoints / collectors varies per project — do not enumerate from training data.
Phase 6: Delete Old Files
Unit test files
- Delete
unitTests.qunit.html(or equivalent legacy bootstrap HTML) - Delete
legacyTests.qunit.html(or any other per-test HTML bootstraps) - Verify that all redundant aggregators identified in Phase 4.0 were already deleted (e.g.,
legacyTests.qunit.js,allTests.js,AllTests.js). If any remain, delete them now.
OPA test files — Pattern A
- Delete
opaTests.qunit.html - Delete
AllJourneys.js(replaced by OpaSetup.js; journeys now listed individually intestsuite.qunit.js) - Delete
AllJourneys.json(journeys now listed in main testsuite)
OPA test files — Pattern U (only if needsIframeMigration was true)
- Delete
webapp/test/integration/flpSandbox.qunit.htmlonly if it exists from a prior intermediate attempt. Greenfield Pattern U projects do not have it. Phase 5b never authors this file. - Confirm no journey or page object still calls
iStartMyUIComponent(verified again in Phase 7).
OPA test files — Pattern B
- Delete all individual
*.qunit.htmlfiles underwebapp/test/opa/ - Count files before deleting — must match the parse script's
summary.totalActive
Do NOT delete
testsuite.qunit.html(updated in Phase 3)Test.qunit.html(created in Phase 2)
Phase 7: Verify
-
Count check: Confirm the number of OPA journey entries in the main
testsuite.qunit.jsmatches the parse script's OPA total. The main testsuite should have 1 unit entry ("unit/unitTests") plus all individual OPA journeys. -
Dangling-entry check: Every entry key in
testsuite.qunit.jsmust resolve to a real.qunit.jsfile underwebapp/test/. Test Starter appends.qunitautomatically, so an entry"integration/Foo"requireswebapp/test/integration/Foo.qunit.jsto exist. Run:node <skill-dir>/scripts/check-dangling-entries.js webapp/testExit code 0 prints
OK: <n> entries all resolve. Exit code 1 prints the dangling entry list to stderr — this is the multi-module-HTML failure mode (synthetic*Combinedname with no backing file). Fix any dangling entries before reporting done. -
Run UI5 linter:
npx @ui5/linter— check that noprefer-test-starterwarnings remain for the modernized files. -
Structural review:
Test.qunit.htmlexists withrunTest.jstestsuite.qunit.htmlusescreateSuite.jstestsuite.qunit.jshas"unit/unitTests"delegate + all individual OPA journeys- All unit test JS files use
sap.ui.define(noCore.ready) - OPA:
OpaSetup.jsexists and imports all utility/page-object modules - OPA: every journey file imports
OpaSetup - No stale individual test HTML files remain
-
Pattern U verification (only if
needsIframeMigrationwas true):webapp/test/integration/opaIframe.qunit.htmlexists and loadssap-ui-core.js+sap/ushell/bootstrap/sandbox.js, with nowindow["sap-ushell-config"]block and no inline<script>body (boot logic lives inopaIframeBoot.js, loaded viadata-sap-ui-oninit="module:...").webapp/test/integration/opaIframeBoot.jsexists and callsmockserver.init()+new ComponentContainer(...).placeAt("content").grep -rn "iStartMyUIComponent\b" webapp/test→ zero hits.grep -rn "#app-tile&/" webapp/test→ zero hits (no FLP hash prefix in routing helpers).mockserver.init()appears only insideopaIframeBoot.js(loaded byopaIframe.qunit.htmlviadata-sap-ui-oninit), not inOpaSetup.jsorarrangements/Common.js.loader.pathsintestsuite.qunit.jsdoes not alias<NAMESPACE>/test/integration/opaIframeorflpSandbox.- Cross-window misuse gate:
node <skill-dir>/scripts/detect-cross-window-imports.js <project-root>exits0. Non-zero halts. The gate detects by usage shape:new <Identifier>(...)and<x> instanceof <Identifier>where<Identifier>is asap.ui.definedep param NOT on the OPA-safe allowlist (sap/ui/test/*,sap/ui/core/routing/History); plus bare$(,jQuery(,document.,window.not routed throughOpa5.getJQuery()/Opa5.getWindow(). Fix per finding: drop the dep from parentsap.ui.defineand re-resolve at use site viaOpa5.getWindow().sap.ui.require("<path>"), or rewrite the DOM access throughOpa5.getJQuery()/Opa5.getWindow(). - ErrorHandler XML-parse null-guard applied (or flagged in
MODERNIZATION_ISSUES.md):grep -rnE 'getElementsByTagName\("message"\)\[0\]\.firstChild' webappreturns no unguarded hits.
Worked Examples
Example A — Pattern A (Single HTML + AllJourneys)
Project namespace: com.mycompany.myapp, 4 OPA journeys + 2 unit tests.
After modernization:
testsuite.qunit.html→createSuite.js,testsuite.qunit.js→ 5 entries (1 unit delegate + 4 individual OPA journeys)Test.qunit.html→runTest.jsAllJourneys.js→ split intoOpaSetup.js+ individual journey entries intestsuite.qunit.js- Deleted:
AllJourneys.json,opaTests.qunit.html,unitTests.qunit.html
Example B — Pattern B (Many Individual HTML Files)
Project namespace: com.mycompany.myapp, 45 OPA journeys + 3 unit tests.
After modernization:
testsuite.qunit.html→createSuite.js,testsuite.qunit.js→ 46 entries (1 unit delegate + 45 individual OPA journeys)Test.qunit.html→runTest.jsOpaSetup.js→ union of all utility imports + commonOpa5.extendConfig- All 45 journey files → OpaSetup added as dependency, 3 with
autoWait: falseoverride - Deleted: all 46 individual HTML files,
unitTests.qunit.html
Related Skills
- fix-csp-compliance — the old HTML files contain inline scripts that violate CSP. Modernizing to Test Starter removes them.
- fix-linter-blind-spots — runs later in the modernization workflow (Phase 3, Step 3.2) to catch runtime-breaking patterns the linter misses (app-namespace globals in JS, QUnit assertions, sinon mocking).
- fix-js-globals (cases 1b and 1c) — handles the
sap.*globals the linter reports. The linter-blind-spots skill handles app-namespace globals the linter misses.
Important Notes
runTest.jsvscreateSuite.js:createSuite.jsis for the testsuite overview pages.runTest.jsis forTest.qunit.htmlthat runs individual tests. Do not mix them up..qunit.jssuffix rule: Only files referenced by a testsuite entry key need.qunit.js— unit test files, OPA journey files, and aggregators. Files loaded assap.ui.definedependencies (OPA utilities, page objects,OpaSetup.js) keep plain.js..qunitsuffix insap.ui.definedependency paths: When a.qunit.jsfile references another.qunit.jsfile viasap.ui.define, the dependency path must include the.qunitsuffix (without.js). The UI5 module loader appends.jsautomatically, so"./FilterBar.qunit"resolves toFilterBar.qunit.js. Without the suffix,"./FilterBar"resolves toFilterBar.js(file not found). Exception: plain.jsfiles likeOpaSetup.jsdo NOT get the suffix. This applies to top-level aggregators (unitTests.qunit.js) AND individual test files that combine other.qunit.jsfiles.test-resources/prefix: Anysap.ui.definedependency pointing to a file underwebapp/test/must usetest-resources/<NAMESPACE>/...instead of<NAMESPACE>/test/.... Thetest/segment disappears because thetest-resourcesresource root already maps towebapp/test/.- Convert existing
<NAMESPACE>/test/deps: After all journey file updates (Phase 5), scan ALL.jsfiles under test directories for dependency paths using<NAMESPACE>/test/and convert totest-resources/<NAMESPACE>/(drop thetest/segment). This applies to both.qunit.jstest files and plain.jsutility/page-object files. - Relative paths vs
test-resources/: Aggregators and same-directory imports use relative./paths. Cross-directory imports (e.g., journey → page object) usetest-resources/<NAMESPACE>/.... Specifically: journey files importOpaSetupvia"./OpaSetup"(same directory), NOT via"test-resources/<NAMESPACE>/integration/OpaSetup". - OpaSetup.js must NOT import
sap/ui/test/opaQunit: TheopaQunitmodule (which provides theopaTestfunction) belongs in each individual journey.qunit.jsfile, not in the shared setup. OpaSetup.js only containsOpa5.extendConfigand page object side-effect imports. {suite}and{name}placeholders are mandatory in thepageproperty — without them, Test Starter cannot locate the right test to run.- Side-effect imports go at the END of the dependency array: Dependencies that don't map to a function parameter (e.g.,
OpaSetuploaded for itsOpa5.extendConfigside effect) must be appended at the END of thesap.ui.definedependency array, after all named dependencies. Prepending them shifts function parameter positions, causing wrong modules to be passed to existing code. When appending, ensure no double-comma (,,) — check whether the preceding entry already has a trailing comma before inserting one.
Completion Checklist
Before reporting this skill as done, verify ALL of the following. If any item fails, go back and fix it.
| # | Check | How to verify |
|---|---|---|
| 1 | Test.qunit.html exists |
ls webapp/test/Test.qunit.html |
| 2 | testsuite.qunit.html uses createSuite.js |
Check <script src= in the file |
| 3 | testsuite.qunit.js has correct entries |
"unit/unitTests" delegate + all individual OPA journeys |
| 4 | No redundant aggregators remain | legacyTests.qunit.js, allTests.js, AllTests.js etc. must be deleted |
| 5 | No stale test HTML bootstraps remain | unitTests.qunit.html, legacyTests.qunit.html, opaTests.qunit.html must be deleted |
| 6 | unitTests.qunit.js only references real test files |
No references to deleted aggregators |
| 7 | Main testsuite OPA entry count matches parse script OPA total | Count OPA entries in testsuite.qunit.js against summary.totalActive minus unit count |
| 8 | Every testsuite entry resolves to a real .qunit.js file |
Run the Phase 7 dangling-entry check; output must be OK: <n> entries all resolve |
| 9 | Bootstrap overrides reported, not silently migrated | If --scan-bootstrap-overrides produced findings, MODERNIZATION_ISSUES.md contains one section per finding; if no findings, file may be absent |
| 10 | Launcher + FLP sandbox classified | Phase 0.2 --detect-launcher verdict recorded (launcher, flpSandbox, needsIframeMigration); mixed halted the skill |
| 11 | If needsIframeMigration: opaIframe.qunit.html + opaIframeBoot.js exist; HTML has no inline script body; no in-window launcher remains |
ls webapp/test/integration/opaIframe.qunit.html webapp/test/integration/opaIframeBoot.js; grep -nE '<script>[[:space:]]*$' webapp/test/integration/opaIframe.qunit.html returns nothing (only <script src=...> and <script ... data-sap-ui-oninit=...> allowed); grep -rn "iStartMyUIComponent\b" webapp/test (zero hits) |
| 12 | If needsIframeMigration: no FLP hash prefix, no loader.paths alias for the iframe |
grep -rn "#app-tile&/" webapp/test (zero hits); testsuite.qunit.js has no <NAMESPACE>/test/integration/opaIframe or flpSandbox alias |
| 13 | If needsIframeMigration: ErrorHandler XML parse guarded or flagged |
grep -rnE 'getElementsByTagName\("message"\)\[0\]\.firstChild' webapp returns no unguarded hits, or the issue is logged in MODERNIZATION_ISSUES.md |
| 14 | If needsIframeMigration: cross-window misuse gate clean |
node <skill-dir>/scripts/detect-cross-window-imports.js <project-root> exits 0; no page-object file uses a non-OPA-safe sap.ui.define dep param as a constructor / instanceof, and no bare $(, jQuery(, document., window. reaches app DOM without going through Opa5.getJQuery() / Opa5.getWindow() |
原文・著作権は Anthropic および各プラグイン作者に帰属します。日本語訳は Claude API による自動翻訳です。