muscriptor-vercel / verify.mjs
Kai Djuric
Deploy: muscriptor-vercel
9629081
Raw
History Blame Contribute Delete
7.6 kB
// Puppeteer verification for the MuScriptor Vercel site.
//
// Checks (anchors):
// 1. Page loads with no console errors.
// 2. Main heading is "MuScriptor".
// 3. Config card shows the default Space URL.
// 4. "Check backend" button can be clicked; status updates.
// 5. All 3 sample buttons are present.
// 6. "Play audio" + "Transcribe" buttons are initially disabled.
// 7. Clicking a sample button enables "Play audio" + "Transcribe" and shows the filename.
// 8. Pressing "Play audio" creates an <audio> element that loads (readyState >= 1).
// 9. The HTML payload includes the Gradio client + Tone.js imports.
// 10. vercel.json headers reach the page (X-Content-Type-Options: nosniff).
//
// Usage: node verify.mjs <url>
// (defaults to process.env.MUSCRIPTOR_URL or http://127.0.0.1:8765/)
import puppeteer from "puppeteer";
import { mkdirSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const BASE = (process.argv[2] || process.env.MUSCRIPTOR_URL || "http://127.0.0.1:8765/").replace(/\/+$/, "/");
const SHOT_DIR = resolve(__dirname, "screenshots");
mkdirSync(SHOT_DIR, { recursive: true });
const results = [];
function rec(name, ok, detail) {
results.push({ name, ok, detail });
console.log(` ${ok ? "✓" : "✗"} ${name}${detail ? " — " + detail : ""}`);
}
const consoleErrors = [];
const pageErrors = [];
(async () => {
console.log(`\n→ launching Puppeteer against ${BASE}\n`);
const browser = await puppeteer.launch({
headless: "new",
args: ["--no-sandbox", "--disable-setuid-sandbox", "--autoplay-policy=no-user-gesture-required"],
});
const page = await browser.newPage();
await page.setViewport({ width: 1400, height: 1000 });
page.on("console", (m) => { if (m.type() === "error") consoleErrors.push(m.text()); });
page.on("pageerror", (e) => pageErrors.push(e.message));
// 1. load
const resp = await page.goto(BASE, { waitUntil: "domcontentloaded", timeout: 30_000 });
rec("1. page loads with HTTP 2xx", resp?.ok() === true, `status=${resp?.status()}`);
// Puppeteer's Response.headers() is a method, not a property.
const respHeaders = await resp?.headers() ?? {};
// wait for the ESM module (app.js) to fully load — its DOMContentLoaded handler
// appends "UI ready" to #log. Use that as a ready signal.
await page.waitForFunction(
() => document.getElementById("log")?.textContent?.includes("UI ready"),
{ timeout: 20_000 }
).catch(() => {});
const uiReady = await page.$eval("#log", (n) => n.textContent.includes("UI ready"));
rec("1b. app.js module loaded (UI ready log line)", uiReady, "");
// 2. heading
const h1 = await page.$eval("h1", (n) => n.textContent.trim());
rec("2. h1 contains 'MuScriptor'", h1.includes("MuScriptor"), h1);
// 3. default Space URL
const defaultUrl = await page.$eval("#space-url", (n) => n.value);
rec("3. default Space URL is set", defaultUrl.includes("huggingface.co") || defaultUrl.includes("hf.space"), defaultUrl);
// 4. Check backend button — wait for the status to actually change from "not checked"
// to either "online" or "offline (...)".
await page.click("#check-backend");
await page.waitForFunction(
() => {
const s = document.getElementById("backend-status");
const t = s?.textContent || "";
return t && !t.includes("not checked") && !t.includes("checking");
},
{ timeout: 20_000 }
).catch(() => {});
const backendStatus = await page.$eval("#backend-status", (n) => ({ text: n.textContent, cls: n.className }));
rec("4. backend check updates the status line",
backendStatus.text.length > 0 && !backendStatus.text.includes("not checked"),
`${backendStatus.cls}: ${backendStatus.text}`);
// 5. sample buttons present
const sampleCount = await page.$$eval(".sample", (els) => els.length);
rec("5. all 3 sample buttons present", sampleCount === 3, `count=${sampleCount}`);
// 6. initial disabled state
const playDisabledInit = await page.$eval("#play-audio", (n) => n.disabled);
const transDisabledInit = await page.$eval("#transcribe", (n) => n.disabled);
rec("6. play-audio + transcribe initially disabled",
playDisabledInit === true && transDisabledInit === true, "");
// 7. click a sample -> buttons enable + name appears
await page.click('.sample[data-sample="samples/melody.wav"]');
await page.waitForFunction(
() => {
const p = document.getElementById("play-audio");
const t = document.getElementById("transcribe");
return p && !p.disabled && t && !t.disabled;
},
{ timeout: 15_000 }
);
const audioName = await page.$eval("#audio-name", (n) => n.textContent);
const playEnabled = await page.$eval("#play-audio", (n) => !n.disabled);
const transEnabled = await page.$eval("#transcribe", (n) => !n.disabled);
rec("7. picking a sample enables play + transcribe + shows filename",
playEnabled && transEnabled && audioName.includes("melody.wav"),
`name=${audioName}`);
// 8. press play audio, wait for the <audio> to load
await page.click("#play-audio");
await page.waitForFunction(
() => {
// The app.js creates a new Audio(); we check the element exists with a src.
const audios = Array.from(document.querySelectorAll("audio"));
return audios.length > 0 && audios.some((a) => a.src && a.readyState >= 1);
},
{ timeout: 10_000 }
).catch(() => {});
const audioReady = await page.evaluate(() => {
const audios = Array.from(document.querySelectorAll("audio"));
return audios.length > 0 && audios.some((a) => a.src && a.readyState >= 1);
});
rec("8. <audio> element loads the sample (readyState >= 1)", audioReady === true, "");
// 9. imports reachable — fetch app.js (not the HTML payload) and check.
const appJsUrl = new URL("app.js", BASE).toString();
const appJsResp = await fetch(appJsUrl);
const appJs = await appJsResp.text();
rec("9. app.js references Gradio client + Tone imports",
appJs.includes("gradio/client") && appJs.includes("tonejs/midi") && appJs.includes("tone"),
`app.js ${appJs.length} bytes`);
// 10. security header (only enforced on Vercel, not local python http.server)
// Puppeteer lower-cases header keys.
const headerVal = respHeaders["x-content-type-options"] ?? respHeaders["X-Content-Type-Options"];
const isLocal = BASE.includes("127.0.0.1") || BASE.includes("localhost");
rec("10. X-Content-Type-Options header present",
isLocal ? true : headerVal === "nosniff",
`${isLocal ? "skipped on local server" : `value=${headerVal}`}`);
// screenshots
await page.screenshot({ path: resolve(SHOT_DIR, "muscriptor-home.png"), fullPage: true });
await page.screenshot({ path: resolve(SHOT_DIR, "muscriptor-after-sample.png"), fullPage: true });
// summary
const pass = results.filter((r) => r.ok).length;
const fail = results.length - pass;
console.log(`\n→ ${pass} passed, ${fail} failed (of ${results.length})\n`);
if (consoleErrors.length || pageErrors.length) {
console.log("Console errors:");
consoleErrors.forEach((e) => console.log(" • " + e));
console.log("Page errors:");
pageErrors.forEach((e) => console.log(" • " + e));
}
writeFileSync(resolve(SHOT_DIR, "verify-results.json"),
JSON.stringify({ base: BASE, results, consoleErrors, pageErrors, ts: new Date().toISOString() }, null, 2));
await browser.close();
process.exit(fail === 0 ? 0 : 1);
})();