From d1357933939be5bc86db42865072a791b96ebbcd Mon Sep 17 00:00:00 2001 From: Matthias Osswald Date: Tue, 23 Jun 2026 11:29:44 +0200 Subject: [PATCH] feat!: Remove testRunner middleware The QUnit TestRunner (`testrunner.html`, `testrunner.css`, `TestRunner.js`) is now provided by the UI5 framework (`sap.ui.core`). All supported UI5 releases ship the relevant testrunner resources, so the dedicated middleware is no longer needed. JIRA: CPOUI5FOUNDATION-1158 --- internal/documentation/docs/pages/Server.md | 8 +- .../documentation/docs/updates/migrate-v5.md | 11 +- packages/server/REUSE.toml | 5 - packages/server/eslint.config.js | 7 - .../lib/middleware/MiddlewareManager.js | 94 ++- .../lib/middleware/middlewareRepository.js | 1 - packages/server/lib/middleware/testRunner.js | 62 -- .../lib/middleware/testRunner/TestRunner.js | 674 ------------------ .../lib/middleware/testRunner/testrunner.css | 81 --- .../lib/middleware/testRunner/testrunner.html | 310 -------- packages/server/nyc.config.js | 3 - packages/server/package.json | 1 - .../scripts/update-testRunner-resources.sh | 28 - packages/server/test/lib/server/main.js | 9 +- .../server/middleware/MiddlewareManager.js | 214 +++++- .../test/lib/server/middleware/testRunner.js | 122 ---- 16 files changed, 261 insertions(+), 1369 deletions(-) delete mode 100644 packages/server/lib/middleware/testRunner.js delete mode 100644 packages/server/lib/middleware/testRunner/TestRunner.js delete mode 100644 packages/server/lib/middleware/testRunner/testrunner.css delete mode 100644 packages/server/lib/middleware/testRunner/testrunner.html delete mode 100755 packages/server/scripts/update-testRunner-resources.sh delete mode 100644 packages/server/test/lib/server/middleware/testRunner.js diff --git a/internal/documentation/docs/pages/Server.md b/internal/documentation/docs/pages/Server.md index 3900de4ee72..5382121f102 100644 --- a/internal/documentation/docs/pages/Server.md +++ b/internal/documentation/docs/pages/Server.md @@ -28,7 +28,9 @@ Please be aware of the following risks when using the server: ::: info Removed Middleware The `serveThemes` middleware has been removed in UI5 CLI v5. Theme compilation is now handled by the `buildThemes` build task, which pre-compiles all theme CSS files. The resulting CSS files (including `library.css`, `library-RTL.css`, `library-parameters.json`, and CSS Variables resources) are served via the `serveResources` middleware, providing the same functionality with better performance through build-time compilation and caching. -Custom middleware previously referencing `serveThemes` via `beforeMiddleware` or `afterMiddleware` will continue to work with automatic remapping and a deprecation warning. See the [v5 migration guide](../updates/migrate-v5.md) for details. +The `testRunner` middleware has also been removed in UI5 CLI v5. The QUnit TestRunner (`testrunner.html`, `testrunner.css`, `TestRunner.js`) is now provided by the UI5 framework (`sap.ui.core`) and is served via the `serveResources` middleware. + +Custom middleware previously referencing `serveThemes` or `testRunner` via `beforeMiddleware` or `afterMiddleware` will continue to work with automatic remapping and a deprecation warning. See the [v5 migration guide](../updates/migrate-v5.md) for details. ::: All available standard middleware are listed below in the order of their execution. @@ -43,7 +45,6 @@ A project can also add custom middleware to the server by using the [Custom Serv | `liveReloadClient` | See chapter [liveReload](#livereload) | | `discovery` | See chapter [discovery](#discovery) | | `serveResources` | See chapter [serveResources](#serveresources) | -| `testRunner` | See chapter [testRunner](#testrunner) | | `versionInfo` | See chapter [versionInfo](#versioninfo) | | `nonReadRequests` | See chapter [nonReadRequests](#nonreadrequests) | | `serveIndex` | See chapter [serveIndex](#serveindex) | @@ -105,9 +106,6 @@ The following file content transformations are executed: - Escaping non-ASCII characters in `.properties` translation files based on a project's [configuration](./Configuration.md#encoding-of-properties-files) - Enhancing the `manifest.json` with supported locales determined by available `.properties` [translation files](./Builder.md#generation-of-supported-locales) -### testRunner -Serves a static version of the UI5 QUnit TestRunner at `/test-resources/sap/ui/qunit/testrunner.html`. - ### versionInfo Generates and serves the version info file `/resources/sap-ui-version.json`, which is required for several framework functionalities. diff --git a/internal/documentation/docs/updates/migrate-v5.md b/internal/documentation/docs/updates/migrate-v5.md index 0b735e36608..21a97e1ff62 100644 --- a/internal/documentation/docs/updates/migrate-v5.md +++ b/internal/documentation/docs/updates/migrate-v5.md @@ -19,6 +19,7 @@ Or update your global install via: `npm i --global @ui5/cli@next` - **@ui5/server: Live Reload is enabled by default for `ui5 serve`** +- **@ui5/server: Standard middleware `serveThemes` and `testRunner` have been removed** ## Node.js and npm Version Support @@ -268,21 +269,23 @@ If your project uses a custom middleware that provides live reload functionality The following middleware has been removed from the [standard middlewares list](../pages/Server.md#standard-middleware): * `serveThemes` — The `buildThemes` build task now handles theme compilation (LESS to CSS). Because server sessions now also perform builds, this task runs during a server start instead of on demand during runtime. The resulting CSS files are served by the `serveResources` middleware. This change improves performance through build-time compilation and caching while maintaining the same functionality. +* `testRunner` — The UI5 QUnit TestRunner resources (`testrunner.html`, `testrunner.css`, `TestRunner.js`) are now provided by the UI5 framework (`sap.ui.core`) and served via the `serveResources` middleware. All supported UI5 releases ship the relevant testrunner resources, so the dedicated middleware is no longer needed. **Backward Compatibility:** -If your project or any custom middleware references a removed middleware via `beforeMiddleware` or `afterMiddleware`, UI5 CLI will automatically remap the reference to the nearest remaining middleware and log a deprecation warning. Your custom middleware will still be executed in the expected order. +If your project or any custom middleware references a removed middleware via `beforeMiddleware` or `afterMiddleware`, UI5 CLI keeps a no-op placeholder in the middleware execution order at the original slot. The custom middleware is executed in the same position as before and a deprecation warning is logged. **What Changed:** - Theme CSS files (`library.css`, `library-RTL.css`, etc.) are now **pre-built** -- Files are served via `serveResources` instead of being compiled on demand -- The same CSS files are available at the same URLs as before +- Theme files are served via `serveResources` instead of being compiled on demand +- TestRunner resources are served via `serveResources` from the UI5 framework instead of being shipped with UI5 CLI **Recommended Action:** Update your `ui5.yaml` configuration to reference an existing middleware instead. | Removed Middleware | Replacement Behavior | Recommended `afterMiddleware` | | ------------------ | -------------------- | ----------------------------- | -| `serveThemes` | CSS files pre-built by `buildThemes` task and served via `serveResources` | `testRunner` | +| `serveThemes` | CSS files pre-built by `buildThemes` task and served via `serveResources` | `serveResources` | +| `testRunner` | TestRunner resources served via `serveResources` from the UI5 framework | `serveResources` | ## Learn More diff --git a/packages/server/REUSE.toml b/packages/server/REUSE.toml index b4233a3d79d..43e6c6253ca 100644 --- a/packages/server/REUSE.toml +++ b/packages/server/REUSE.toml @@ -16,8 +16,3 @@ precedence = "aggregate" SPDX-FileCopyrightText = ["2010 Sencha Inc.", "2011 LearnBoost", "2011 TJ Holowaychuk", "2014-2015 Douglas Christopher Wilson"] SPDX-License-Identifier = "MIT" -[[annotations]] -path = "lib/middleware/testRunner/**" -precedence = "aggregate" -SPDX-FileCopyrightText = "2026 SAP SE or an SAP affiliate company and OpenUI5 contributors" -SPDX-License-Identifier = "Apache-2.0" diff --git a/packages/server/eslint.config.js b/packages/server/eslint.config.js index 436d653b035..d4a5c181426 100644 --- a/packages/server/eslint.config.js +++ b/packages/server/eslint.config.js @@ -3,13 +3,6 @@ import eslintCommonConfig from "../../eslint.common.config.js"; export default [ ...eslintCommonConfig, // Load common ESLint config - { - // Add project-specific ESLint config rules here - // in order to override common config - ignores: [ - "lib/middleware/testRunner/", - ] - }, { // Live reload client script runs in the browser, not Node.js files: ["lib/liveReload/client.js"], diff --git a/packages/server/lib/middleware/MiddlewareManager.js b/packages/server/lib/middleware/MiddlewareManager.js index 55f315f546e..f4c651a8f22 100644 --- a/packages/server/lib/middleware/MiddlewareManager.js +++ b/packages/server/lib/middleware/MiddlewareManager.js @@ -4,15 +4,6 @@ import {getLogger} from "@ui5/logger"; const hasOwn = Function.prototype.call.bind(Object.prototype.hasOwnProperty); const log = getLogger("server:MiddlewareManager"); -/** - * Mapping of removed standard middleware names to their predecessor and successor - * in the original execution order. Used to remap custom middleware references - * to the nearest remaining middleware when a removed middleware is referenced. - */ -const LEGACY_MIDDLEWARE_MAPPING = { - serveThemes: {before: "testRunner", after: "versionInfo"} -}; - /** * @private * @typedef {object} MiddlewareResources @@ -49,6 +40,7 @@ class MiddlewareManager { this.middleware = Object.create(null); this.middlewareExecutionOrder = []; + this.legacyMiddlewarePlaceholders = new Set(); this.middlewareUtil = new MiddlewareUtil({graph, project: rootProject}); } @@ -64,10 +56,15 @@ class MiddlewareManager { await this.addStandardMiddleware(); await this.addCustomMiddleware(); - return this.middlewareExecutionOrder.map((name) => { - const m = this.middleware[name]; - app.use(m.mountPath, m.middleware); - }); + // Strip legacy placeholders for removed standard middlewares — they only existed + // in middlewareExecutionOrder to preserve slots for custom beforeMiddleware / + // afterMiddleware references and are not backed by an actual middleware. + return this.middlewareExecutionOrder + .filter((name) => !this.legacyMiddlewarePlaceholders.has(name)) + .map((name) => { + const m = this.middleware[name]; + app.use(m.mountPath, m.middleware); + }); } /** @@ -86,7 +83,7 @@ class MiddlewareManager { customMiddleware, wrapperCallback, mountPath = "/", beforeMiddleware, afterMiddleware } = {}) { - if (this.middleware[middlewareName]) { + if (this.#isMiddlewareNameKnown(middlewareName)) { throw new Error(`A middleware with the name ${middlewareName} has already been added`); } @@ -107,8 +104,7 @@ class MiddlewareManager { } if (beforeMiddleware || afterMiddleware) { - let refMiddlewareName = beforeMiddleware || afterMiddleware; - const originalRefMiddlewareName = refMiddlewareName; // Store original before any remapping + const refMiddlewareName = beforeMiddleware || afterMiddleware; let refMiddlewareIdx = this.middlewareExecutionOrder.indexOf(refMiddlewareName); if (refMiddlewareName === "connectUi5Proxy") { @@ -119,33 +115,23 @@ class MiddlewareManager { `Please see the migration guide at https://ui5.github.io/cli/updates/migrate-v3/`); } - // Handle legacy middleware with graceful fallback - const legacyMapping = LEGACY_MIDDLEWARE_MAPPING[refMiddlewareName]; - if (legacyMapping) { - // Replace with the appropriate fallback based on reference type - refMiddlewareName = afterMiddleware ? legacyMapping.before : legacyMapping.after; - + // Warn when a removed standard middleware is referenced. The reference still resolves + // because the removed middleware is kept as a placeholder in middlewareExecutionOrder + // (see #addLegacyMiddlewarePlaceholder), preserving the original execution slot. + if (this.legacyMiddlewarePlaceholders.has(refMiddlewareName)) { log.warn( - `Standard middleware "${originalRefMiddlewareName}" has been removed. ` + + `Standard middleware "${refMiddlewareName}" has been removed. ` + `Custom middleware "${middlewareName}" defined in project ` + - `"${this.middlewareUtil.getProject()}" references it and ` + - `is now placed ${afterMiddleware ? "after" : "before"} ` + - `"${refMiddlewareName}" instead. ` + + `"${this.middlewareUtil.getProject()}" still references it. ` + + `The custom middleware will be executed in the slot the removed middleware ` + + `originally occupied, but the reference should be updated. ` + `For details, see the migration guide at ` + `https://ui5.github.io/cli/next/updates/migrate-v5`); } - refMiddlewareIdx = this.middlewareExecutionOrder.indexOf(refMiddlewareName); - if (refMiddlewareIdx === -1) { - // Provide clear error message, including remapping context if applicable - const errorMsg = legacyMapping ? - `Could not find fallback middleware "${refMiddlewareName}" ` + - `(mapped from removed middleware "${originalRefMiddlewareName}"), ` + - `referenced by custom middleware "${middlewareName}"` : - `Could not find middleware ${refMiddlewareName}, referenced by custom ` + - `middleware ${middlewareName}`; - throw new Error(errorMsg); + throw new Error(`Could not find middleware ${refMiddlewareName}, referenced by custom ` + + `middleware ${middlewareName}`); } if (afterMiddleware) { // Insert after index of referenced middleware @@ -272,7 +258,8 @@ class MiddlewareManager { }); } }); - await this.addMiddleware("testRunner"); + this.#addLegacyMiddlewarePlaceholder("testRunner"); + this.#addLegacyMiddlewarePlaceholder("serveThemes"); await this.addMiddleware("versionInfo", { mountPath: "/resources/sap-ui-version.json" }); @@ -327,11 +314,11 @@ class MiddlewareManager { } let middlewareName = middlewareDef.name; - if (this.middleware[middlewareName]) { + if (this.#isMiddlewareNameKnown(middlewareName)) { // Middleware is already known // => add a suffix to allow for multiple configurations of the same middleware let suffixCounter = 0; - while (this.middleware[middlewareName]) { + while (this.#isMiddlewareNameKnown(middlewareName)) { suffixCounter++; // Start at 1 middlewareName = `${middlewareDef.name}--${suffixCounter}`; } @@ -363,6 +350,35 @@ class MiddlewareManager { }); } } + + /** + * Inserts a no-op placeholder for a removed standard middleware into the execution order. + * Placeholders preserve the original slot so custom middlewares referencing the removed + * middleware via beforeMiddleware / afterMiddleware keep their intended position. + * They are stripped in applyMiddleware() before mounting on the express app. + * + * @private + * @param {string} middlewareName Name of the removed standard middleware + */ + #addLegacyMiddlewarePlaceholder(middlewareName) { + this.legacyMiddlewarePlaceholders.add(middlewareName); + this.middlewareExecutionOrder.push(middlewareName); + } + + /** + * Returns whether the given middleware name is already known — either registered + * in this.middleware or occupying a legacy placeholder slot. + * Placeholder names are treated as known to avoid collisions in the execution order: + * a custom middleware named e.g. "testRunner" would otherwise clash with the + * placeholder inserted to preserve its original slot. + * + * @private + * @param {string} middlewareName Middleware name to check + * @returns {boolean} + */ + #isMiddlewareNameKnown(middlewareName) { + return !!(this.middleware[middlewareName] || this.legacyMiddlewarePlaceholders.has(middlewareName)); + } } export default MiddlewareManager; diff --git a/packages/server/lib/middleware/middlewareRepository.js b/packages/server/lib/middleware/middlewareRepository.js index 613af7a738e..a4a28fac5d0 100644 --- a/packages/server/lib/middleware/middlewareRepository.js +++ b/packages/server/lib/middleware/middlewareRepository.js @@ -7,7 +7,6 @@ const middlewareInfos = { serveIndex: {path: "./serveIndex.js"}, discovery: {path: "./discovery.js"}, versionInfo: {path: "./versionInfo.js"}, - testRunner: {path: "./testRunner.js"}, nonReadRequests: {path: "./nonReadRequests.js"} }; diff --git a/packages/server/lib/middleware/testRunner.js b/packages/server/lib/middleware/testRunner.js deleted file mode 100644 index 71bb4f4b79b..00000000000 --- a/packages/server/lib/middleware/testRunner.js +++ /dev/null @@ -1,62 +0,0 @@ -import {promisify} from "node:util"; -import fs from "graceful-fs"; -const readFile = promisify(fs.readFile); -import {fileURLToPath} from "node:url"; -import mime from "mime-types"; -import parseurl from "parseurl"; -import {getLogger} from "@ui5/logger"; -const log = getLogger("server:middleware:testRunner"); - -const testRunnerResourceRegEx = /\/test-resources\/sap\/ui\/qunit\/(testrunner\.(html|css)|TestRunner.js)$/; -const resourceCache = Object.create(null); - -function serveResource(res, resourcePath, resourceContent) { - const type = mime.lookup(resourcePath) || "application/octet-stream"; - const charset = mime.charset(type); - const contentType = type + (charset ? "; charset=" + charset : ""); - - // resources served by this middleware do not change often - res.setHeader("Cache-Control", "public, max-age=1800"); - - res.setHeader("Content-Type", contentType); - res.end(resourceContent); -} - -/** - * Creates and returns the middleware to serve a resource index. - * - * @module @ui5/server/middleware/testRunner - * @param {object} parameters Parameters - * @param {object} parameters.resources Contains the resource reader or collection to access project related files - * @returns {Function} Returns a server middleware closure. - */ -function createMiddleware({resources}) { - return async function(req, res, next) { - try { - const pathname = parseurl(req).pathname; - const parts = testRunnerResourceRegEx.exec(pathname); - const resourceName = parts && parts[1]; - - if (resourceName) { // either "testrunner.html", "testrunner.css" or "TestRunner.js" (case sensitive!) - log.verbose(`Serving ${pathname}`); - let pResource; - if (!resourceCache[pathname]) { - const filePath = fileURLToPath(new URL(`./testRunner/${resourceName}`, import.meta.url)); - pResource = readFile(filePath, {encoding: "utf8"}); - resourceCache[pathname] = pResource; - } else { - pResource = resourceCache[pathname]; - } - - const resourceContent = await pResource; - serveResource(res, pathname, resourceContent); - } else { - next(); - } - } catch (err) { - next(err); - } - }; -} - -export default createMiddleware; diff --git a/packages/server/lib/middleware/testRunner/TestRunner.js b/packages/server/lib/middleware/testRunner/TestRunner.js deleted file mode 100644 index d7efe7ed8f9..00000000000 --- a/packages/server/lib/middleware/testRunner/TestRunner.js +++ /dev/null @@ -1,674 +0,0 @@ -(function(window) { - "use strict"; - - /*global CollectGarbage, Handlebars */ - /* eslint radix: 0 */ - - /* - * Simulate the JSUnit Testsuite to collect the available - * test pages per Suite - */ - window.jsUnitTestSuite = function() {}; - window.jsUnitTestSuite.prototype.getTestPages = function() { - return this.aPages; - }; - window.jsUnitTestSuite.prototype.addTestPage = function(sTestPage) { - this.aPages = this.aPages || []; - // in case of running in the root context the testsuites right now - // generate an invalid URL because they assume that test-resources is - // the context path - this section makes sure to remove the duplicate - // test-resources segments in the path - if (sTestPage.indexOf("/test-resources/test-resources") === 0 || sTestPage.indexOf("/test-resources/resources") === 0) { - sTestPage = sTestPage.substr("/test-resources".length); - } - this.aPages.push(sTestPage); - }; - - function XHRQueue(iMaxParallelRequests, iWaitTime) { - this.iLimit = iMaxParallelRequests === undefined ? Infinity : iMaxParallelRequests; - this.iWaitTime = iWaitTime === undefined ? 0 : iWaitTime; - this.aPendingTasks = []; - this.oRunningTasks = new Set(); - this.iLastTaskExecution = -Infinity; - } - - XHRQueue.prototype.ajax = function(sUrl, options) { - var oTask = { - url: sUrl, - options: options - }; - oTask.promise = new Promise(function(resolve, reject) { - oTask.resolve = resolve; - oTask.reject = reject; - }); - this.aPendingTasks.push(oTask); - this._processNext(); - return oTask.promise; - }; - - XHRQueue.prototype._processNext = function() { - var iNow = Date.now(); - var iDelay = iNow - this.iLastTaskExecution; - if ( iDelay < this.iWaitTime ) { - setTimeout(function() { - this._processNext(); - }.bind(this), iDelay); - return; - } - if ( this.aPendingTasks.length > 0 && this.oRunningTasks.size < this.iLimit ) { - var oTask = this.aPendingTasks.shift(); - this.oRunningTasks.add(oTask); - this.iLastTaskExecution = iNow; - Promise.resolve(jQuery.ajax(oTask.url, oTask.options)) - .then(oTask.resolve, oTask.reject) - .finally(function() { - this.oRunningTasks.delete(oTask); - this._processNext(); - }.bind(this)); - } - }; - - var oXHRQueue = new XHRQueue(50, 2); - - /* - * Template for test results - */ - var sResultsTemplate = "{{#tests}}" + - "
" + - "

{{header}}

" + - "
    " + - "{{#results}}" + - "
  1. " + - "

    {{result.TestName}} ({{result.Failed}} ,{{result.Passed}} ,{{result.All}})

    " + - " Rerun" + - "
      " + - "{{#result.testmessages}}" + - "
    1. {{message}}" + - "
      {{expected}}" + - "
      {{actual}}" + - "
      {{diff}}" + - "
      {{source}}" + - "
    2. " + - "{{/result.testmessages}}" + - "
    " + - "
  2. " + - "{{/results}}" + - "
" + - "
" + - "{{/tests}}"; - - /** - * Utility class to find test pages and check them for being - * a testsuite or a QUnit testpage - also it returns the coverage - * results. - */ - window.sap = window.sap || {}; - window.sap.ui = window.sap.ui || {}; - window.sap.ui.qunit = window.sap.ui.qunit || {}; - window.sap.ui.qunit.TestRunner = { - - checkTestPage: function(sTestPage, bSequential) { - var oPromise = - this._checkTestPage(sTestPage, bSequential) - .then(function(aTestPages) { - return aTestPages; - }); - oPromise.done = oPromise.then; // compat for Deferred - oPromise.fail = oPromise.catch; // compat for Deferred - return oPromise; - }, - - _checkTestPage: function(sTestPage, bSequential) { - - var oPromise = new Promise(function(resolve, reject) { - - if (typeof sTestPage !== "string") { - window.console.log("QUnit: invalid test page specified"); - reject("QUnit: invalid test page specified"); - } - - /* - if (window.console && typeof window.console.log === "function") { - window.console.log("QUnit: checking test page: " + sTestPage); - }*/ - - // check for an existing test page and check for test suite or page - oXHRQueue.ajax(sTestPage).then(function(sData) { - if (/(?:window\.suite\s*=|function\s*suite\s*\(\s*\)\s*{)/.test(sData) - || (/data-sap-ui-testsuite/.test(sData) && !/sap\/ui\/test\/starter\/runTest/.test(sData)) - || /sap\/ui\/test\/starter\/createSuite/.test(sData) ) { - var $frame = jQuery("