协作头脑风暴
brainstorming
在实现前澄清需求、探索设计选择,并形成清晰的开发方向。
modernize-test-starter【开发与工程】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(), o…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
name: modernize-test-starter description: | Modernize QUnit unit tests and OPA5 integration tests to the UI5 Test Starter concept. Use this skill when:
prefer-test-starter for *.qunit.html or *.qunit.js filesiStartMyUIComponent instead of iStartMyAppInAFrameThis 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:
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:
test*.html), NOT in the entry point HTML files. The host file stays unchanged.| 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.
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.
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)Scan the project to classify what exists before making changes.
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.
Phase 5b (bare-Component iframe migration) is gated on two signals, not one:
iStartMyAppInAFrame (iframe) vs iStartMyUIComponent (in-window).sap/ushell/bootstrap/sandbox.js (or older flpSandbox.js) or declares window["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.
Pattern A — "Single HTML + AllJourneys": The most common pattern.
webapp/test/integration/opaTests.qunit.html exists (single bootstrap file)AllJourneys.js orchestrates Opa5.extendConfig and dynamically loads journeysAllJourneys.json listing journey namesPattern B — "Many Individual HTML Files": Less common, larger projects.
*.qunit.html files under webapp/test/opa/ (one per test)Opa5.extendConfig and utility module importsDetection:
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.
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?, ... }opaConfig — extracted Opa5.extendConfig details and page object imports from AllJourneys.jsSave this output — it drives the rest of the modernization.
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 bare defineModuleSync(...)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.
The main testsuite lists all tests with a hybrid approach:
"unit/unitTests" entry (which loads all unit test modules through unitTests.qunit.js)"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:
page property MUST use the ui5:// protocol prefix — without it, Test Starter cannot resolve test pagesmodule property is needed — Test Starter appends .qunit to entry keys, resolving "integration/NavigationJourney" to integration/NavigationJourney.qunit.js.qunit.js suffix conventionsap.ui.define dependencies (utility modules, page objects, arrangement classes) keep their .js extensionMANDATORY 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:
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:
sap/suite/ui/generic/template/integration/testLibrary)test-resources mappingsIf 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.
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.
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.
Before converting any file, scan webapp/test/unit/ for redundant aggregators. A redundant aggregator is a JS file that:
sap.ui.require([...], function() { QUnit.start(); }) to load other test modules, ORsap.ui.define([...]) to list dependencies with no test body, ORQUnit.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.
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:
.qunit.js suffix (e.g., App.controller.js → App.controller.qunit.js)sap.ui.define format (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.jsBefore — 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");
});
});
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:
.qunit.js files in webapp/test/unit/ that contain actual testsDo 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:
./, not absolute namespace paths.qunit suffix to each dependency (without .js) because the actual files were renamed to .qunit.js in Step 4.1controller/App.controller.qunit.js, reference it as "./controller/App.controller.qunit"If the old testsuite.qunit.js used jsUnitTestSuite, it's already replaced in Phase 1. Delete the old content.
This phase differs based on the detected pattern. Read the full instructions in the corresponding reference file.
Read references/pattern-a-modernization.md for detailed instructions.
Summary:
Opa5.extendConfig and all page object/utility imports. OpaSetup.js must NOT import sap/ui/test/opaQunit — that module belongs in each individual journey file..qunit.js suffix so Test Starter can resolve them without a module override"./OpaSetup" (same directory). Do NOT use test-resources/ for same-directory imports.autoWait: false get a per-journey Opa5.extendConfig overridetestLibs settings move to OpaSetup.jsCorrect 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
});
Read references/pattern-b-modernization.md for detailed instructions.
Summary:
Opa5.createPageObjects (side-effect imports)Opa5.extendConfig from the HTML files.qunit.js suffix and add OpaSetup as a side-effect dependencyautoWaitFalseFiles list*.qunit.html loads more than one journey module in a single sap.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.js file under webapp/test/. See references/pattern-b-modernization.md Step 6.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:
webapp/test/integration/opaIframe.qunit.html + opaIframeBoot.js — bare-Component iframe entry. HTML loads sap/ushell sandbox.js for API stubs but defines no sap-ushell-config, so no FLP shell renderer is built. Bootstrap uses data-sap-ui-oninit="module:<NAMESPACE>/test/integration/opaIframeBoot" (no inline <script>, CSP-clean) — the boot module runs mockserver.init() then new ComponentContainer(...).placeAt("content").arrangements/Common.js — iStartMyApp now calls iStartMyAppInAFrame({ source: getFrameUrl(hash), … }). Drop the localService/mockserver import, drop any _clearSharedData helper that resets parent-frame ODataModel.mSharedData, drop the in-window componentConfig.Given.iStartMyUIComponent({...}) → Given.iStartMyApp() (forward hash / autoWait if originally passed).OpaSetup.js — mockserver now boots inside the iframe.waitFor callbacks must be resolved through the iframe's loader: Opa5.getWindow().sap.ui.require("sap/m/Token"). Drop those dependencies from the parent sap.ui.define. Iterate every file under webapp/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/* and sap/ui/core/routing/History. Run node <skill-dir>/scripts/detect-cross-window-imports.js <project-root> after the rewrite — non-zero exit halts Phase 5b. See references/pattern-u-iframe-migration.md §5b.6.2.$(...), jQuery(...), document.*, window.* references that target app-rendered DOM with Opa5.getJQuery()(...), Opa5.getWindow().document.*, Opa5.getWindow().*. Detection is folded into the same detect-cross-window-imports.js gate run for item 5 — bare DOM/jQuery lines that don't already route through Opa5.getJQuery() / Opa5.getWindow() are reported alongside the constructor / instanceof findings.#app-tile&/ prefix, no sap.ushell.Container.setDirtyFlag(false).sap-message envelopes — function-import POST handlers consumed by an app-side message collector that dereferences aErrorMsg[0] need a sap-message header so the collector array is non-empty.xmlDoc.getElementsByTagName("message")[0].firstChild.data against null nodes; fall back to the raw response text. Ship with the migration or flag in MODERNIZATION_ISSUES.md.<NAMESPACE>/test/integration/opaIframe in loader.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.
unitTests.qunit.html (or equivalent legacy bootstrap HTML)legacyTests.qunit.html (or any other per-test HTML bootstraps)legacyTests.qunit.js, allTests.js, AllTests.js). If any remain, delete them now.opaTests.qunit.htmlAllJourneys.js (replaced by OpaSetup.js; journeys now listed individually in testsuite.qunit.js)AllJourneys.json (journeys now listed in main testsuite)needsIframeMigration was true)webapp/test/integration/flpSandbox.qunit.html only if it exists from a prior intermediate attempt. Greenfield Pattern U projects do not have it. Phase 5b never authors this file.iStartMyUIComponent (verified again in Phase 7).*.qunit.html files under webapp/test/opa/summary.totalActivetestsuite.qunit.html (updated in Phase 3)Test.qunit.html (created in Phase 2)Count check: Confirm the number of OPA journey entries in the main testsuite.qunit.js matches 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.js must resolve to a real .qunit.js file under webapp/test/. Test Starter appends .qunit automatically, so an entry "integration/Foo" requires webapp/test/integration/Foo.qunit.js to exist. Run:
node <skill-dir>/scripts/check-dangling-entries.js webapp/test
Exit 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 *Combined name with no backing file). Fix any dangling entries before reporting done.
Run UI5 linter: npx @ui5/linter — check that no prefer-test-starter warnings remain for the modernized files.
Structural review:
Test.qunit.html exists with runTest.jstestsuite.qunit.html uses createSuite.jstestsuite.qunit.js has "unit/unitTests" delegate + all individual OPA journeyssap.ui.define (no Core.ready)OpaSetup.js exists and imports all utility/page-object modulesOpaSetupPattern U verification (only if needsIframeMigration was true):
webapp/test/integration/opaIframe.qunit.html exists and loads sap-ui-core.js + sap/ushell/bootstrap/sandbox.js, with no window["sap-ushell-config"] block and no inline <script> body (boot logic lives in opaIframeBoot.js, loaded via data-sap-ui-oninit="module:...").webapp/test/integration/opaIframeBoot.js exists and calls mockserver.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 inside opaIframeBoot.js (loaded by opaIframe.qunit.html via data-sap-ui-oninit), not in OpaSetup.js or arrangements/Common.js.loader.paths in testsuite.qunit.js does not alias <NAMESPACE>/test/integration/opaIframe or flpSandbox.node <skill-dir>/scripts/detect-cross-window-imports.js <project-root> exits 0. Non-zero halts. The gate detects by usage shape: new <Identifier>(...) and <x> instanceof <Identifier> where <Identifier> is a sap.ui.define dep param NOT on the OPA-safe allowlist (sap/ui/test/*, sap/ui/core/routing/History); plus bare $(, jQuery(, document., window. not routed through Opa5.getJQuery() / Opa5.getWindow(). Fix per finding: drop the dep from parent sap.ui.define and re-resolve at use site via Opa5.getWindow().sap.ui.require("<path>"), or rewrite the DOM access through Opa5.getJQuery() / Opa5.getWindow().MODERNIZATION_ISSUES.md): grep -rnE 'getElementsByTagName\("message"\)\[0\]\.firstChild' webapp returns no unguarded hits.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 into OpaSetup.js + individual journey entries in testsuite.qunit.jsAllJourneys.json, opaTests.qunit.html, unitTests.qunit.htmlProject 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 + common Opa5.extendConfigautoWait: false overrideunitTests.qunit.htmlsap.* globals the linter reports. The linter-blind-spots skill handles app-namespace globals the linter misses.runTest.js vs createSuite.js: createSuite.js is for the testsuite overview pages. runTest.js is for Test.qunit.html that runs individual tests. Do not mix them up..qunit.js suffix rule: Only files referenced by a testsuite entry key need .qunit.js — unit test files, OPA journey files, and aggregators. Files loaded as sap.ui.define dependencies (OPA utilities, page objects, OpaSetup.js) keep plain .js..qunit suffix in sap.ui.define dependency paths: When a .qunit.js file references another .qunit.js file via sap.ui.define, the dependency path must include the .qunit suffix (without .js). The UI5 module loader appends .js automatically, so "./FilterBar.qunit" resolves to FilterBar.qunit.js. Without the suffix, "./FilterBar" resolves to FilterBar.js (file not found). Exception: plain .js files like OpaSetup.js do NOT get the suffix. This applies to top-level aggregators (unitTests.qunit.js) AND individual test files that combine other .qunit.js files.test-resources/ prefix: Any sap.ui.define dependency pointing to a file under webapp/test/ must use test-resources/<NAMESPACE>/... instead of <NAMESPACE>/test/.... The test/ segment disappears because the test-resources resource root already maps to webapp/test/.<NAMESPACE>/test/ deps: After all journey file updates (Phase 5), scan ALL .js files under test directories for dependency paths using <NAMESPACE>/test/ and convert to test-resources/<NAMESPACE>/ (drop the test/ segment). This applies to both .qunit.js test files and plain .js utility/page-object files.test-resources/: Aggregators and same-directory imports use relative ./ paths. Cross-directory imports (e.g., journey → page object) use test-resources/<NAMESPACE>/.... Specifically: journey files import OpaSetup via "./OpaSetup" (same directory), NOT via "test-resources/<NAMESPACE>/integration/OpaSetup".sap/ui/test/opaQunit: The opaQunit module (which provides the opaTest function) belongs in each individual journey .qunit.js file, not in the shared setup. OpaSetup.js only contains Opa5.extendConfig and page object side-effect imports.{suite} and {name} placeholders are mandatory in the page property — without them, Test Starter cannot locate the right test to run.OpaSetup loaded for its Opa5.extendConfig side effect) must be appended at the END of the sap.ui.define dependency 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.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:]]*#x27; 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() |