diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 021cec038..4befcf237 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -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: diff --git a/scripts/run-tests.js b/scripts/run-tests.js new file mode 100644 index 000000000..b10cb0dab --- /dev/null +++ b/scripts/run-tests.js @@ -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); +})(); diff --git a/static/js/wasm-terminal.test.html b/static/js/wasm-terminal.test.html new file mode 100644 index 000000000..9046fdefc --- /dev/null +++ b/static/js/wasm-terminal.test.html @@ -0,0 +1,548 @@ + + + + + wasm-terminal unit tests + + + +

wasm-terminal unit tests

+
+
+ + + + +