UI5 Test Starterの概念に合わせて、QUnitユニットテストとOPA5統合テストを最新化する 次のような場合に使用: - リンターが *.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 のアサーション現代化、ウィンドウ内フレーム起動ツールの移行)に対応します。 トリガー条件: 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.
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
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.
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/<Area>/<TestName>.qunit.html |
CONVERT to Test Starter |
| Test Host | test<ServiceName>.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.
Read manifest.json → sap.app/id. This is your namespace:
<NAMESPACE> — dots → slashes (e.g., my.app → my/app)<NAMESPACE-WITH-DOTS> — raw value with dotsLook 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.
Phase 5b is gated on needsIframeMigration = (launcher === "in-window" AND flpSandbox === true).
node <skill-dir>/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.
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:
find webapp/test -name "AllJourneys.js" -o -name "AllJourneys.json"
find webapp/test/opa -name "*.qunit.html" -type f 2>/dev/null | wc -l
node <skill-dir>/scripts/parse-testsuite.js <testsuite.qunit.html> <test-base-dir> <namespace>
Outputs: pattern (A/B), summary (counts), entries (module→config mapping), opaConfig (for Pattern A). Save this — it drives the rest.
node <skill-dir>/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.
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.
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" }
}
};
});
Read references/testsuite-configuration.md for detailed option explanations.
Key points:
page MUST use ui5:// protocol prefixmodule property needed — Test Starter appends .qunit to entry keys{suite} and {name} placeholders are mandatoryMANDATORY. Extract ALL resource root mappings from ALL test HTML files:
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.
<!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>
<!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>
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.
Files with actual test logic (QUnit.module, QUnit.test, assert.*) need:
.qunit.js suffix (e.g., App.controller.js → App.controller.qunit.js)sap.ui.define (remove QUnit.config.autostart, Core.ready wrappers)// 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) { ... });
});
Directly lists all real unit test modules (from deleted aggregators + any additional .qunit.js files):
sap.ui.define([
"./controller/Main.qunit",
"./model/formatter.qunit"
]);
Rules: relative ./ paths, include .qunit suffix (without .js).
If old testsuite.qunit.js used jsUnitTestSuite, it's already replaced in Phase 1. Delete old content.
Read references/pattern-a-modernization.md for detailed instructions.
Opa5.extendConfig + page object imports. OpaSetup MUST NOT import sap/ui/test/opaQunit..qunit.js"./OpaSetup" as side-effect dependency (relative path)Opa5.extendConfig// OpaSetup.js (no opaQunit!)
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 });
});
// 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
});
Read references/pattern-b-modernization.md for detailed instructions.
Opa5.createPageObjectsOpa5.extendConfig.qunit.js, add OpaSetup dependencyautoWaitFalseFiles*Combined names). See references/pattern-b-modernization.md Step 6.Run only when Phase 0.2 reported needsIframeMigration: true.
Read references/pattern-u-iframe-migration.md for full instructions. Summary:
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.arrangements/Common.js — iStartMyApp → iStartMyAppInAFrame(...). Drop mockserver import, drop _clearSharedData.iStartMyUIComponent({...}) → iStartMyApp() (forward hash/autoWait).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 <skill-dir>/scripts/detect-cross-window-imports.js <project-root>.$(...), document.*, window.* with Opa5.getJQuery()(...), Opa5.getWindow().*.#app-tile&/ prefix.sap-message envelopes — POST handlers need sap-message header for message collectors.getElementsByTagName("message")[0].firstChild.data against null.<NAMESPACE>/test/integration/opaIframe in loader.paths.Items 5–9 are project-specific; apply mechanically per detection grep in reference file.
unitTests.qunit.html, legacyTests.qunit.html (legacy bootstraps)opaTests.qunit.html, AllJourneys.js, AllJourneys.jsonneedsIframeMigration)flpSandbox.qunit.html if it exists from prior attemptiStartMyUIComponent*.qunit.html under webapp/test/opa/ (count must match summary.totalActive)testsuite.qunit.html (updated), Test.qunit.html (created)testsuite.qunit.js match parse script's OPA total.node <skill-dir>/scripts/check-dangling-entries.js webapp/test — exit 0 = OK.npx @ui5/linter — no prefer-test-starter warnings remain.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.needsIframeMigration):
opaIframe.qunit.html + opaIframeBoot.js exist; no inline <script> body; no window["sap-ushell-config"]grep -rn "iStartMyUIComponent\b" webapp/test → zero hitsgrep -rn "#app-tile&/" webapp/test → zero hitsmockserver.init() only in opaIframeBoot.jsloader.paths alias for opaIframe/flpSandboxdetect-cross-window-imports.js exits 0Namespace: com.mycompany.myapp, 4 OPA journeys + 2 unit tests.
After: testsuite.qunit.js → 5 entries (1 unit + 4 OPA). AllJourneys.js → OpaSetup.js + individual entries. Deleted: AllJourneys.json, opaTests.qunit.html, unitTests.qunit.html.
Namespace: com.mycompany.myapp, 45 OPA journeys + 3 unit tests.
After: testsuite.qunit.js → 46 entries (1 unit + 45 OPA). OpaSetup.js = union of utility imports. 3 journeys with autoWait: false override. Deleted: 46 HTML files + unitTests.qunit.html.
runTest.js vs createSuite.js: createSuite.js = testsuite overview. runTest.js = Test.qunit.html for individual tests..qunit.js suffix rule: Only testsuite entry keys need .qunit.js. Dependencies loaded via sap.ui.define (page objects, OpaSetup.js) keep plain .js..qunit in dependency paths: "./FilterBar.qunit" resolves to FilterBar.qunit.js. Without suffix → file not found.test-resources/ prefix: Deps under webapp/test/ use test-resources/<NAMESPACE>/... not <NAMESPACE>/test/....<NAMESPACE>/test/ deps: After Phase 5, scan ALL test .js files and convert to test-resources/<NAMESPACE>/.test-resources/: Same-directory → ./. Cross-directory → test-resources/<NAMESPACE>/....sap/ui/test/opaQunit — belongs in each journey file.| # | Check |
|---|---|
| 1 | Test.qunit.html exists with runTest.js |
| 2 | testsuite.qunit.html uses createSuite.js |
| 3 | testsuite.qunit.js has "unit/unitTests" + all OPA journeys |
| 4 | No redundant aggregators remain |
| 5 | No stale test HTML bootstraps remain |
| 6 | unitTests.qunit.js references only real test files |
| 7 | OPA entry count matches parse script total |
| 8 | Dangling-entry check passes (exit 0) |
| 9 | Bootstrap overrides reported in MODERNIZATION_ISSUES.md (if any) |
| 10 | Launcher + FLP classified; mixed halted |
| 11 | If needsIframeMigration: iframe files exist, no inline script, no iStartMyUIComponent |
| 12 | If needsIframeMigration: no FLP hash prefix, no loader alias |
| 13 | If needsIframeMigration: ErrorHandler guarded or flagged |
| 14 | If needsIframeMigration: detect-cross-window-imports.js exits 0 |
sap.* globals the linter reports.原文・著作権は Anthropic および各プラグイン作者に帰属します。日本語訳は Claude API による自動翻訳です。