Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export default {
files: ["test/integration/boot.js"],
files: ["test/integration/boot.js", "test/integration/connect.js"],
watchMode: {
ignoreChanges: [
"tmp/**"
Expand Down
18 changes: 10 additions & 8 deletions packages/middleware-code-coverage/lib/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,12 @@ export default async function({log, middlewareUtil, options={}, resources}) {
);

if (reportData) {
res.json(reportData);
const body = JSON.stringify(reportData);
res.writeHead(200, {"Content-Type": "application/json"});
res.end(body);
} else {
res.err("No report data provided");
res.writeHead(400, {"Content-Type": "application/json"});
res.end(JSON.stringify({error: "No report data provided"}));
}
}
);
Expand All @@ -86,9 +89,9 @@ export default async function({log, middlewareUtil, options={}, resources}) {
* Endpoint to check for middleware existence
*/
router.get("/.ui5/coverage/ping", async (req, res) => {
res.json({
version: middlewareVersion
});
const body = JSON.stringify({version: middlewareVersion});
res.writeHead(200, {"Content-Type": "application/json"});
res.end(body);
});

/**
Expand Down Expand Up @@ -123,9 +126,8 @@ export default async function({log, middlewareUtil, options={}, resources}) {
return;
}

log.verbose(`handling ${req.path}...`);

const pathname = middlewareUtil.getPathname(req);
log.verbose(`handling ${pathname}...`);
const matchedResource = await resources.all.byPath(pathname);

if (!matchedResource) {
Expand All @@ -147,7 +149,7 @@ export default async function({log, middlewareUtil, options={}, resources}) {
}

// send out instrumented source + source map
res.type(".js");
res.setHeader("Content-Type", "text/javascript");
res.end(instrumentedSource);
});

Expand Down
14 changes: 8 additions & 6 deletions packages/middleware-code-coverage/lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,18 @@ export function getLatestSourceMap(instrumenter) {
* @returns {boolean}
*/
export function shouldInstrumentResource(request, excludePatterns) {
if (!request.url) {
return false;
}
const {pathname, searchParams} = new URL(request.url, "http://localhost");
return (
request.path &&
request.path.endsWith(".js") && // Only .js file requests
!isFalsyValue(request.query.instrument) && // instrument only flagged files, ignore "falsy" values
pathname.endsWith(".js") &&
!isFalsyValue(searchParams.get("instrument")) &&
!(excludePatterns || []).some((pattern) => {
if (pattern instanceof RegExp) {
// The ones coming from .library files are regular expressions
return pattern.test(request.path);
return pattern.test(pathname);
} else {
return request.path.includes(pattern);
return pathname.includes(pattern);
}
})
);
Expand Down
1 change: 1 addition & 0 deletions packages/middleware-code-coverage/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"@eslint/js": "^9.39.2",
"@istanbuljs/esm-loader-hook": "^0.3.0",
"ava": "^6.4.1",
"connect": "^3.7.0",
"eslint": "^9.39.5",
"eslint-config-google": "^0.14.0",
"eslint-plugin-ava": "^15.1.0",
Expand Down
153 changes: 153 additions & 0 deletions packages/middleware-code-coverage/test/integration/connect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import test from "ava";
import connect from "connect";
import request from "supertest";
import middleware from "../../lib/middleware.js";

const sampleJS = `sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/m/MessageToast"
], (Controller, MessageToast) => Controller.extend("ui5.sample.controller.App", {

onInit: () => { },

onButtonPress() {
MessageToast.show(this.getMessage());
},

getMessage() {
return this.getView().getModel("i18n").getProperty("message");
},

formatMessage(message) {
return message.toUpperCase();
}
}));`;

const resources = {
all: {
byGlob() {
return [];
},
async byPath() {
return {
async getString() {
return sampleJS;
}
};
}
}
};

const middlewareUtil = {
getPathname(req) {
return new URL(req.url, "http://localhost").pathname;
}
};

const log = {
verbose() {},
warn() {},
error() {}
};

async function createApp(options = {}) {
const mw = await middleware({log, middlewareUtil, options, resources});
const app = connect();
app.use(mw);
return request(app);
}

test.beforeEach(async (t) => {
t.context.app = await createApp();
});

const coverageMap = {
"/resources/Control1.js": {
path: "/resources/Control1.js",
statementMap: {},
fnMap: {},
branchMap: {},
s: {},
f: {},
b: {}
}
};

// Case 1: Ping endpoint — exercises the JSON response path of the ping handler
test("Ping endpoint returns 200 JSON with version", async (t) => {
const res = await t.context.app
.get("/.ui5/coverage/ping")
.expect(200);

t.is(res.headers["content-type"].split(";")[0], "application/json");
t.truthy(res.body.version);
});

// Case 2: Send coverage report — exercises body-parser + the JSON response path
test("POST report with coverage map returns coverageMap and availableReports", async (t) => {
const res = await t.context.app
.post("/.ui5/coverage/report")
.set("Content-Type", "application/json")
.send(coverageMap)
.expect(200);

t.is(res.headers["content-type"].split(";")[0], "application/json");
t.true(Array.isArray(res.body.coverageMap));
t.true(Array.isArray(res.body.availableReports));
t.true(res.body.availableReports.some((report) => report.report === "html"));
});

// Case 3: Empty body — reportCoverage always returns a (possibly empty) report,
// so the request succeeds with an empty coverage map. The "no report data" 400
// branch is only reachable when reportCoverage returns falsy, which body-parser
// (always providing at least `{}`) prevents at the HTTP level; that branch is
// covered by the unit tests instead.
test("POST report with empty body returns 200 with an empty coverage map", async (t) => {
const res = await t.context.app
.post("/.ui5/coverage/report")
.set("Content-Type", "application/json")
.send({})
.expect(200);

t.true(Array.isArray(res.body.coverageMap));
t.is(res.body.coverageMap.length, 0);
});

// Case 4: Generated report is served via serve-static (framework-agnostic)
test("Generated report is served after posting coverage data", async (t) => {
const reportApp = await createApp();
await reportApp
.post("/.ui5/coverage/report")
.set("Content-Type", "application/json")
.send(coverageMap)
.expect(200);

const res = await reportApp
.get("/.ui5/coverage/report/html/index.html")
.expect(200);

t.true(res.text.includes("Code coverage report"));
});

// Case 5: Instrument a .js resource — exercises req.url parsing in
// shouldInstrumentResource and the Content-Type header set on the response
test("GET with ?instrument=true returns instrumented JS with sourceMappingURL", async (t) => {
const res = await t.context.app
.get("/resources/lib1/Control1.js?instrument=true")
.expect(200);

const contentType = res.headers["content-type"];
t.is(contentType, "text/javascript");
t.true(res.text.includes("path=\"/resources/lib1/Control1.js\""));
t.true(res.text.includes("sourceMappingURL=data:application/json"));
});

// Case 6: Non-instrumented resource falls through to connect's default 404,
// proving the middleware calls next() instead of swallowing unrelated requests
test("Non-instrumented resource falls through to 404", async (t) => {
await t.context.app
.get("/resources/lib1/Control1.js")
.expect(404);

t.pass();
});
Loading