claude-skills/

Anthropic公式スキル・プラグインの日本語ディレクトリ

last sync 22h ago
スキルOfficialdevelopment

🧪modernize-test-starter

プラグイン
ui5-modernization

説明

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.appmy/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:

  1. The OPA app-launcher shape — iStartMyAppInAFrame (iframe) vs iStartMyUIComponent (in-window).
  2. Whether any legacy test HTML loads the FLP sandbox — either 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.

0.3 Check for OPA tests and identify the pattern

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 journeys
  • Often has AllJourneys.json listing journey names

Pattern B — "Many Individual HTML Files": Less common, larger projects.

  • Multiple *.qunit.html files under webapp/test/opa/ (one per test)
  • Each HTML has its own Opa5.extendConfig and 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 entries
  • entries — complete mapping from module path to { title, skip?, ... }
  • For Pattern A: opaConfig — extracted Opa5.extendConfig details 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 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.

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 through unitTests.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 page property MUST use the ui5:// protocol prefix — without it, Test Starter cannot resolve test pages
  • No module property is needed — Test Starter appends .qunit to entry keys, resolving "integration/NavigationJourney" to integration/NavigationJourney.qunit.js
  • Files referenced by testsuite entry keys must follow the .qunit.js suffix convention
  • Files loaded only as sap.ui.define dependencies (utility modules, page objects, arrangement classes) keep their .js extension

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:

  1. Determine what the original relative path resolves to from the source HTML's directory
  2. 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-resources mappings
  • 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:

  1. Rename the file to add .qunit.js suffix (e.g., App.controller.jsApp.controller.qunit.js)
  2. Convert the content to 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.jscontroller/App.controller.qunit.js
  • model/formatter.jsmodel/formatter.qunit.js
  • util/Helper.jsutil/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.js files in webapp/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 .qunit suffix to each dependency (without .js) because the actual files were renamed to .qunit.js in 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:

  1. Create OpaSetup.js from AllJourneys.js — extract 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.
  2. Rename journey files to .qunit.js suffix so Test Starter can resolve them without a module override
  3. Update journey files — add OpaSetup as a side-effect dependency using relative path "./OpaSetup" (same directory). Do NOT use test-resources/ for same-directory imports.
  4. Handle autoWait overrides — journeys needing autoWait: false get a per-journey Opa5.extendConfig override
  5. Preserve testLibs config — Fiori Elements testLibs settings 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:

  1. Inventory utility modules — find all modules that call Opa5.createPageObjects (side-effect imports)
  2. Create OpaSetup.js — consolidate all utility imports + Opa5.extendConfig from the HTML files
  3. Rename journey files to .qunit.js suffix and add OpaSetup as a side-effect dependency
  4. Handle autoWait overrides — use the parse script's autoWaitFalseFiles list
  5. Handle multi-module HTML files — when a legacy *.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.

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:

  1. Create 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").
  2. Rewrite arrangements/Common.jsiStartMyApp 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.
  3. Rewrite every journey fileGiven.iStartMyUIComponent({...})Given.iStartMyApp() (forward hash / autoWait if originally passed).
  4. Strip parent-frame mockserver init from OpaSetup.js — mockserver now boots inside the iframe.
  5. Cross-window control instantiation in page objects — UI5 controls instantiated in 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.
  6. Cross-window jQuery / DOM lookups — replace bare $(...), 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.
  7. Routing helpers — plain Component-router hash, no #app-tile&/ prefix, no sap.ushell.Container.setDirtyFlag(false).
  8. Mockserver 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.
  9. ErrorHandler null-guard for non-XML responses — guard 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.
  10. Do NOT register <NAMESPACE>/test/integration/opaIframe in loader.pathssap.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 in testsuite.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.html only 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.html files under webapp/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

  1. 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.

  2. 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.

  3. Run UI5 linter: npx @ui5/linter — check that no prefer-test-starter warnings remain for the modernized files.

  4. Structural review:

    • Test.qunit.html exists with runTest.js
    • testsuite.qunit.html uses createSuite.js
    • testsuite.qunit.js has "unit/unitTests" delegate + all individual OPA journeys
    • All unit test JS files use sap.ui.define (no Core.ready)
    • OPA: OpaSetup.js exists and imports all utility/page-object modules
    • OPA: every journey file imports OpaSetup
    • No stale individual test HTML files remain
  5. Pattern 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.
    • Cross-window misuse gate: 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().
    • ErrorHandler XML-parse null-guard applied (or flagged in MODERNIZATION_ISSUES.md): grep -rnE 'getElementsByTagName\("message"\)\[0\]\.firstChild' webapp returns 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.htmlcreateSuite.js, testsuite.qunit.js → 5 entries (1 unit delegate + 4 individual OPA journeys)
  • Test.qunit.htmlrunTest.js
  • AllJourneys.js → split into OpaSetup.js + individual journey entries in testsuite.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.htmlcreateSuite.js, testsuite.qunit.js → 46 entries (1 unit delegate + 45 individual OPA journeys)
  • Test.qunit.htmlrunTest.js
  • OpaSetup.js → union of all utility imports + common Opa5.extendConfig
  • All 45 journey files → OpaSetup added as dependency, 3 with autoWait: false override
  • 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.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/.
  • Convert existing <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.
  • Relative paths vs 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".
  • OpaSetup.js must NOT import 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.
  • Side-effect imports go at the END of the dependency array: Dependencies that don't map to a function parameter (e.g., 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.

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 による自動翻訳です。