Add playground tests and CI integration

- Browser-based unit tests (111 tests) covering parsing, builtins,
  pipes, UTF-8, l10n, virtual filesystem, and WASM integration
- Headless Puppeteer test runner (scripts/run-tests.js)
- CI workflow step to build WASM and run tests
This commit is contained in:
Sylvestre Ledru
2026-04-03 21:47:38 +02:00
parent 5c22a5b3d6
commit adc6d38c5a
3 changed files with 695 additions and 1 deletions
+27 -1
View File
@@ -45,6 +45,8 @@ jobs:
- name: Install `rust` toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-wasip1
- name: Install system deps
run: |
@@ -55,7 +57,7 @@ jobs:
- name: Install necessary tools (mdbook and mdbook-toc)
uses: taiki-e/install-action@v2
with:
tool: mdbook,mdbook-toc
tool: mdbook@0.5.0,mdbook-toc@0.15.3
- name: Copy l10n locales into coreutils
run: |
@@ -112,6 +114,22 @@ jobs:
sed -i '/^multilingual/d' book.toml
mdbook build
- name: Build uutils WASM binary
run: |
cd coreutils
# Build the multicall binary for WASI target
# Use --no-default-features to avoid platform-specific dependencies
cargo build --release --target wasm32-wasip1 -p coreutils --no-default-features --features feat_wasm
if [ -f target/wasm32-wasip1/release/coreutils.wasm ]; then
mkdir -p ../uutils.github.io/static/wasm
cp target/wasm32-wasip1/release/coreutils.wasm ../uutils.github.io/static/wasm/uutils.wasm
# Optimize WASM size if wasm-opt is available
if command -v wasm-opt &> /dev/null; then
wasm-opt -Oz ../uutils.github.io/static/wasm/uutils.wasm -o ../uutils.github.io/static/wasm/uutils.wasm
fi
echo "WASM binary size: $(du -h ../uutils.github.io/static/wasm/uutils.wasm | cut -f1)"
fi
- name: Run Zola
uses: shalzz/zola-deploy-action@v0.21.0
env:
@@ -130,6 +148,14 @@ jobs:
cp -r "$lang_dir" "public/coreutils/docs-${lang}"
done
- name: Run playground JS tests
run: |
npm install puppeteer@24
node uutils.github.io/scripts/run-tests.js --dir public --port 8080
- name: Remove test files from deploy output
run: rm -f public/js/wasm-terminal.test.html
- name: Upload artifact for checking the output
uses: actions/upload-artifact@v7
with:
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env node
/**
* Run the wasm-terminal unit tests headlessly using Puppeteer.
*
* Usage:
* npm install puppeteer # one-time setup
* node scripts/run-tests.js [--port 8080] [--dir public]
*
* The script starts a local HTTP server, opens the test page in headless
* Chrome, waits for the results, and exits with code 1 on failure.
*/
const http = require("http");
const fs = require("fs");
const path = require("path");
// Parse CLI args
const args = process.argv.slice(2);
let port = 8080;
let dir = "public";
for (let i = 0; i < args.length; i++) {
if (args[i] === "--port" && args[i + 1]) port = parseInt(args[++i]);
if (args[i] === "--dir" && args[i + 1]) dir = args[++i];
}
// Resolve serve directory — fall back to static/ if public/ doesn't exist
// (public/ is the zola build output; static/ works for running without a build)
const serveDir = fs.existsSync(path.resolve(dir))
? path.resolve(dir)
: path.resolve("static");
const MIME_TYPES = {
".html": "text/html",
".js": "application/javascript",
".css": "text/css",
".wasm": "application/wasm",
".json": "application/json",
".svg": "image/svg+xml",
".png": "image/png",
};
function startServer() {
return new Promise((resolve) => {
const server = http.createServer((req, res) => {
const url = req.url.split("?")[0];
let filePath = path.join(serveDir, url === "/" ? "index.html" : url);
// Prevent path traversal outside the serve directory
if (!path.resolve(filePath).startsWith(serveDir + path.sep) && path.resolve(filePath) !== serveDir) {
res.writeHead(403);
res.end("Forbidden");
return;
}
if (!fs.existsSync(filePath)) {
res.writeHead(404);
res.end("Not found");
return;
}
if (fs.statSync(filePath).isDirectory()) {
filePath = path.join(filePath, "index.html");
}
const ext = path.extname(filePath);
const mime = MIME_TYPES[ext] || "application/octet-stream";
res.writeHead(200, { "Content-Type": mime });
fs.createReadStream(filePath).pipe(res);
});
server.listen(port, "127.0.0.1", () => {
console.log(`Serving ${serveDir} on http://127.0.0.1:${port}`);
resolve(server);
});
});
}
async function runTests(server) {
let puppeteer;
try {
puppeteer = require("puppeteer");
} catch {
console.error("Puppeteer not found. Install it with: npm install puppeteer");
process.exit(1);
}
const browser = await puppeteer.launch({
headless: true,
args: ["--no-sandbox", "--disable-setuid-sandbox"],
});
const page = await browser.newPage();
page.on("console", (msg) => console.log("BROWSER:", msg.text()));
page.on("pageerror", (err) => console.error("PAGE ERROR:", err.message));
const testUrl = `http://127.0.0.1:${port}/js/wasm-terminal.test.html`;
console.log(`Opening ${testUrl}`);
await page.goto(testUrl, { waitUntil: "networkidle0", timeout: 30000 });
await page.waitForFunction("window.__testsFailed !== undefined", {
timeout: 30000,
});
const failed = await page.evaluate(() => window.__testsFailed);
const passed = await page.evaluate(() => window.__testsPassed);
console.log(`\nResults: ${passed} passed, ${failed} failed`);
await browser.close();
server.close();
process.exit(failed > 0 ? 1 : 0);
}
(async () => {
const server = await startServer();
await runTests(server);
})();
File diff suppressed because it is too large Load Diff