From ab8e47fd01d1fcd86392e6082bc07c3ce2c96374 Mon Sep 17 00:00:00 2001 From: David Richardson Date: Thu, 3 Sep 2026 09:45:37 -0600 Subject: [PATCH] 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. --- .gitignore | 1 + Tests/Manual/generate-index.js | 140 +++++++++++++++++++++++++++++++++ Tests/Manual/serve.js | 114 +++++++++++++++++++++++++++ 3 files changed, 255 insertions(+) create mode 100755 Tests/Manual/generate-index.js create mode 100755 Tests/Manual/serve.js diff --git a/.gitignore b/.gitignore index f84d28caf..ea63775cc 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ node_modules /dist/cappuccino/lib /dist/cappuccino/bin Tests/Manual/.Frameworks +/Tests/Manual/index.html diff --git a/Tests/Manual/generate-index.js b/Tests/Manual/generate-index.js new file mode 100755 index 000000000..2a32ee138 --- /dev/null +++ b/Tests/Manual/generate-index.js @@ -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, "&").replace(//g, ">"); +} + +function link(href, label) +{ + return "" + escapeHtml(label) + ""; +} + +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 "" + escapeHtml(appDir) + "" + links.join("   ") + ""; + }); + + return "\n" + + "Manual Tests\n" + + "\n" + + "

Manual Integration Tests

\n" + + "

" + appDirs.length + " applications. Generated by generate-index.js.

\n" + + "\n" + rows.join("\n") + "\n
\n" + + "\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")); +} diff --git a/Tests/Manual/serve.js b/Tests/Manual/serve.js new file mode 100755 index 000000000..a9990b7a1 --- /dev/null +++ b/Tests/Manual/serve.js @@ -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); +});