Scripts to generate and serve index HTML file for all tests

Node.js, requires no additional interpreter or dependencies.
Generates a single index.html file at root which displays launch links for all tests.
This commit is contained in:
David Richardson
2026-09-03 09:45:37 -06:00
parent a2181c88d7
commit ab8e47fd01
3 changed files with 255 additions and 0 deletions
+1
View File
@@ -27,3 +27,4 @@ node_modules
/dist/cappuccino/lib
/dist/cappuccino/bin
Tests/Manual/.Frameworks
/Tests/Manual/index.html
+140
View File
@@ -0,0 +1,140 @@
#!/usr/bin/env node
/*
* generate-index.js
* Tests/Manual
*
* Created by David Richardson, September 7, 2026
* Copyright 2026, David Richardson. All rights reserved.
*
* Writes ./index.html: links every manual test app's source entry
* points, plus its Build/Debug and Build/Release product entry
* points when they exist.
*
* No npm dependencies -- Node core modules only.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
const fs = require("fs");
const path = require("path");
const ROOT = __dirname;
// Same detection rule as Tests/Manual/Jakefile: an app directory has
// Info.plist or index.html, and is never Frameworks/.Frameworks.
function listAppDirs()
{
return fs.readdirSync(ROOT, { withFileTypes: true })
.filter(function(entry)
{
if (!entry.isDirectory())
return false;
if (entry.name.charAt(0) === ".")
return false;
if (entry.name === "Frameworks")
return false;
var dir = path.join(ROOT, entry.name);
return fs.existsSync(path.join(dir, "Info.plist")) ||
fs.existsSync(path.join(dir, "index.html"));
})
.map(function(entry) { return entry.name; })
.sort();
}
// The built product directory is named after productName, not the app
// folder (confirmed: ArrayController1 -> Build/Debug/ArrayController).
// Find it by reading what's actually there, not by assuming a name.
function findBuiltProduct(appDir, configuration)
{
var configDir = path.join(ROOT, appDir, "Build", configuration);
if (!fs.existsSync(configDir))
return null;
var entries = fs.readdirSync(configDir, { withFileTypes: true })
.filter(function(entry) { return entry.isDirectory(); });
for (var i = 0; i < entries.length; i++)
{
var candidate = path.join(configDir, entries[i].name, "index.html");
if (fs.existsSync(candidate))
return path.join(appDir, "Build", configuration, entries[i].name, "index.html");
}
return null;
}
function escapeHtml(s)
{
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
function link(href, label)
{
return "<a href=\"" + encodeURI(href) + "\">" + escapeHtml(label) + "</a>";
}
function buildIndexHtml()
{
var appDirs = listAppDirs();
var rows = appDirs.map(function(appDir)
{
var links = [];
if (fs.existsSync(path.join(ROOT, appDir, "index.html")))
links.push(link(path.join(appDir, "index.html"), "source"));
if (fs.existsSync(path.join(ROOT, appDir, "index-debug.html")))
links.push(link(path.join(appDir, "index-debug.html"), "source debug"));
var debugProduct = findBuiltProduct(appDir, "Debug");
if (debugProduct)
links.push(link(debugProduct, "built debug"));
var releaseProduct = findBuiltProduct(appDir, "Release");
if (releaseProduct)
links.push(link(releaseProduct, "built release"));
return "<tr><td>" + escapeHtml(appDir) + "</td><td>" + links.join(" &nbsp; ") + "</td></tr>";
});
return "<!DOCTYPE html>\n" +
"<html><head><meta charset=\"utf-8\"><title>Manual Tests</title>\n" +
"<style>body{font-family:sans-serif;font-size:14px}" +
"table{border-collapse:collapse}td{padding:2px 8px;vertical-align:top}" +
"tr:nth-child(even){background:#f4f4f4}</style></head><body>\n" +
"<h1>Manual Integration Tests</h1>\n" +
"<p>" + appDirs.length + " applications. Generated by generate-index.js.</p>\n" +
"<table>\n" + rows.join("\n") + "\n</table>\n" +
"</body></html>\n";
}
function generateIndex()
{
var html = buildIndexHtml();
fs.writeFileSync(path.join(ROOT, "index.html"), html);
return html;
}
module.exports = { generateIndex: generateIndex };
if (require.main === module)
{
generateIndex();
console.log("Wrote " + path.join(ROOT, "index.html"));
}
+114
View File
@@ -0,0 +1,114 @@
#!/usr/bin/env node
/*
* serve.js
* Tests/Manual
*
* Regenerates index.html, then serves this directory over plain HTTP,
* so tests run in any browser -- not only Safari via file://.
*
* No npm dependencies -- Node core modules only. Ctrl-C to stop.
*
* Writes ./index.html: links every manual test app's source entry
* points, plus its Build/Debug and Build/Release product entry
* points when they exist.
*
* No npm dependencies -- Node core modules only.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
const http = require("http");
const fs = require("fs");
const path = require("path");
const { exec } = require("child_process");
const { generateIndex } = require("./generate-index.js");
const ROOT = __dirname;
const PORT = 8347;
const MIME_TYPES = {
".html": "text/html",
".htm": "text/html",
".js": "application/javascript",
".sj": "application/javascript",
".css": "text/css",
".json": "application/json",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".svg": "image/svg+xml",
".txt": "text/plain",
".plist": "text/plain"
};
function resolveRequestPath(url)
{
var decoded = decodeURIComponent(url.split("?")[0]);
var resolved = path.normalize(path.join(ROOT, decoded));
// Reject traversal outside ROOT.
if (resolved !== ROOT && !resolved.startsWith(ROOT + path.sep))
return null;
return resolved;
}
function serveFile(filePath, res)
{
fs.readFile(filePath, function(err, data)
{
if (err)
{
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("404 Not Found: " + filePath);
return;
}
var type = MIME_TYPES[path.extname(filePath).toLowerCase()] || "application/octet-stream";
res.writeHead(200, { "Content-Type": type });
res.end(data);
});
}
const server = http.createServer(function(req, res)
{
var filePath = resolveRequestPath(req.url);
if (!filePath)
{
res.writeHead(403, { "Content-Type": "text/plain" });
res.end("403 Forbidden");
return;
}
fs.stat(filePath, function(err, stats)
{
if (!err && stats.isDirectory())
filePath = path.join(filePath, "index.html");
serveFile(filePath, res);
});
});
generateIndex();
server.listen(PORT, function()
{
var url = "http://localhost:" + PORT + "/index.html";
console.log("Serving " + ROOT + " at " + url);
console.log("Ctrl-C to stop.");
exec("open " + url);
});