BROWSER=1 on e2e-camera-snapshot.sh signs in with the real Supabase test user, opens the fleet page and checks the snapshot view decodes, refreshes and has picture content.
117 lines
4.6 KiB
TypeScript
117 lines
4.6 KiB
TypeScript
/**
|
|
* The P1/A1 camera in the real web console, in a real Chrome, for
|
|
* scripts/e2e-camera-snapshot.sh (BROWSER=1). Signs in the same way as
|
|
* e2e-camera-browser.ts. Reads pixel statistics and keeps no screenshot: the
|
|
* frame is the inside of someone's workshop.
|
|
*
|
|
* Results are `ok<TAB>label` / `FAILED<TAB>label`.
|
|
* Environment: APP_URL, SUPABASE_E2E_EMAIL, SUPABASE_E2E_PASSWORD, CHROME_BIN.
|
|
*/
|
|
import puppeteer from "puppeteer-core";
|
|
|
|
const appUrl = process.env.APP_URL!;
|
|
const email = process.env.SUPABASE_E2E_EMAIL!;
|
|
const password = process.env.SUPABASE_E2E_PASSWORD!;
|
|
const chromeBin = process.env.CHROME_BIN ?? "/usr/bin/google-chrome";
|
|
|
|
let failed = false;
|
|
const say = (good: boolean, label: string, detail = "") => {
|
|
if (!good) failed = true;
|
|
console.log(`${good ? "ok" : "FAILED"}\t${label}${detail ? ` (${detail})` : ""}`);
|
|
};
|
|
|
|
const browser = await puppeteer.launch({
|
|
executablePath: chromeBin,
|
|
headless: true,
|
|
args: ["--no-sandbox", "--disable-dev-shm-usage"],
|
|
});
|
|
|
|
try {
|
|
const page = await browser.newPage();
|
|
const pageErrors: string[] = [];
|
|
page.on("pageerror", (err) => pageErrors.push(String((err as Error).message ?? err).slice(0, 160)));
|
|
|
|
await page.goto(`${appUrl}/login`, { waitUntil: "domcontentloaded", timeout: 120_000 });
|
|
await page.waitForFunction(
|
|
() => typeof (window as any).useNuxtApp === "function" && Boolean((window as any).useNuxtApp().$supabase),
|
|
{ timeout: 120_000 },
|
|
);
|
|
const signInError = await page.evaluate(
|
|
async (e: string, p: string) => {
|
|
const { client } = (window as any).useNuxtApp().$supabase;
|
|
const { error } = await client.auth.signInWithPassword({ email: e, password: p });
|
|
return error ? String(error.message) : null;
|
|
},
|
|
email,
|
|
password,
|
|
);
|
|
say(signInError === null, "the app's own Supabase client signs the user in", signInError ?? "");
|
|
if (signInError) process.exit(1);
|
|
|
|
await page.goto(`${appUrl}/`, { waitUntil: "domcontentloaded", timeout: 60_000 });
|
|
const stayed = await page.waitForSelector(".fleet", { timeout: 60_000 }).then(() => true).catch(() => false);
|
|
say(stayed && !page.url().includes("/login"), "the fleet page loads and does not bounce back to /login");
|
|
|
|
const live = await page
|
|
.waitForFunction(() => document.querySelector(".app-shell__status.is-online") !== null, { timeout: 30_000 })
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
say(live, "the header reports Live");
|
|
|
|
const shown = await page
|
|
.waitForFunction(() => document.querySelector(".snapshot-view__status.is-connected") !== null, { timeout: 60_000 })
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
const status = await page.evaluate(
|
|
() => document.querySelector(".snapshot-view__status")?.textContent?.trim() ?? "no snapshot view rendered",
|
|
);
|
|
say(shown, "the snapshot view reports connected", `status: ${status}`);
|
|
say(
|
|
(await page.$(".webrtc-view")) === null,
|
|
"no WebRTC player is rendered for a snapshot camera",
|
|
);
|
|
|
|
if (shown) {
|
|
// Two different frames, a few seconds apart: the picture is refreshing.
|
|
const result = await page.evaluate(async () => {
|
|
const img = document.querySelector<HTMLImageElement>(".snapshot-view img")!;
|
|
const first = img.src;
|
|
const t0 = Date.now();
|
|
while (img.src === first && Date.now() - t0 < 15_000) await new Promise((r) => setTimeout(r, 250));
|
|
const refreshed = img.src !== first;
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = 64;
|
|
canvas.height = 36;
|
|
const ctx = canvas.getContext("2d")!;
|
|
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
const px = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
|
|
let sum = 0;
|
|
let sumSq = 0;
|
|
const n = px.length / 4;
|
|
for (let i = 0; i < px.length; i += 4) {
|
|
const y = 0.299 * px[i] + 0.587 * px[i + 1] + 0.114 * px[i + 2];
|
|
sum += y;
|
|
sumSq += y * y;
|
|
}
|
|
const mean = sum / n;
|
|
return {
|
|
refreshed,
|
|
width: img.naturalWidth,
|
|
height: img.naturalHeight,
|
|
mean,
|
|
stdev: Math.sqrt(Math.max(0, sumSq / n - mean * mean)),
|
|
};
|
|
});
|
|
say(result.width > 0, "Chrome decoded the JPEG", `${result.width}x${result.height}`);
|
|
say(result.refreshed, "the picture refreshes by itself");
|
|
say(result.stdev > 2, "the frame has picture content, not black or one flat colour",
|
|
`mean luma ${result.mean.toFixed(0)}, stdev ${result.stdev.toFixed(1)}`);
|
|
}
|
|
|
|
say(pageErrors.length === 0, "the page raised no uncaught errors", pageErrors.slice(0, 2).join(" | "));
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
|
|
process.exit(failed ? 1 : 0);
|