Check the P1/A1 camera in real Chrome
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.
This commit is contained in:
@@ -0,0 +1,116 @@
|
|||||||
|
/**
|
||||||
|
* 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);
|
||||||
@@ -26,6 +26,14 @@ R2_ENDPOINT="${R2_ENDPOINT:-http://localhost:9000}"
|
|||||||
export DATABASE_URL VALKEY_URL EDGE_JWT_SECRET PORT R2_ACCESS_KEY_ID R2_SECRET_ACCESS_KEY R2_ENDPOINT
|
export DATABASE_URL VALKEY_URL EDGE_JWT_SECRET PORT R2_ACCESS_KEY_ID R2_SECRET_ACCESS_KEY R2_ENDPOINT
|
||||||
# Where to keep the last frame for a human to look at. Unset: not kept.
|
# Where to keep the last frame for a human to look at. Unset: not kept.
|
||||||
SAVE_FRAME="${SAVE_FRAME:-}"
|
SAVE_FRAME="${SAVE_FRAME:-}"
|
||||||
|
# BROWSER=1: also open the real web console in real Chrome (needs the real
|
||||||
|
# Supabase test user, so source ../dev-secrets.sh).
|
||||||
|
BROWSER="${BROWSER:-0}"
|
||||||
|
APP_HOST="${APP_HOST:-127.0.0.1}"
|
||||||
|
APP_DIR="${APP_DIR:-$REPO_ROOT/../continuum-app}"
|
||||||
|
APP_PORT="${APP_PORT:-3052}"
|
||||||
|
CHROME_BIN="${CHROME_BIN:-/usr/bin/google-chrome}"
|
||||||
|
APP_PID=""
|
||||||
|
|
||||||
WORK="$(mktemp -d)"
|
WORK="$(mktemp -d)"
|
||||||
BACKEND_PID=""; PROXY_PID=""; AUTH_PID=""
|
BACKEND_PID=""; PROXY_PID=""; AUTH_PID=""
|
||||||
@@ -37,8 +45,10 @@ USER_ID="$(cat /proc/sys/kernel/random/uuid)"
|
|||||||
cleanup() {
|
cleanup() {
|
||||||
local rc=$?
|
local rc=$?
|
||||||
for pid in "$PROXY_PID" "$BACKEND_PID" "$AUTH_PID"; do [ -n "$pid" ] && kill "$pid" 2>/dev/null || true; done
|
for pid in "$PROXY_PID" "$BACKEND_PID" "$AUTH_PID"; do [ -n "$pid" ] && kill "$pid" 2>/dev/null || true; done
|
||||||
|
[ -n "$APP_PID" ] && kill -- "-$APP_PID" 2>/dev/null || true
|
||||||
for _ in $(seq 1 20); do [ -n "$PROXY_PID" ] && kill -0 "$PROXY_PID" 2>/dev/null || break; sleep 0.3; done
|
for _ in $(seq 1 20); do [ -n "$PROXY_PID" ] && kill -0 "$PROXY_PID" 2>/dev/null || break; sleep 0.3; done
|
||||||
for pid in "$PROXY_PID" "$BACKEND_PID" "$AUTH_PID"; do [ -n "$pid" ] && kill -9 "$pid" 2>/dev/null || true; done
|
for pid in "$PROXY_PID" "$BACKEND_PID" "$AUTH_PID"; do [ -n "$pid" ] && kill -9 "$pid" 2>/dev/null || true; done
|
||||||
|
[ -n "$APP_PID" ] && kill -9 -- "-$APP_PID" 2>/dev/null || true
|
||||||
[ -n "${ORG_ID:-}" ] && psql_q "DELETE FROM organisations WHERE id = '$ORG_ID'" >/dev/null 2>&1 || true
|
[ -n "${ORG_ID:-}" ] && psql_q "DELETE FROM organisations WHERE id = '$ORG_ID'" >/dev/null 2>&1 || true
|
||||||
if [ $rc -ne 0 ]; then
|
if [ $rc -ne 0 ]; then
|
||||||
echo; echo "--- backend log (tail) ---"; tail -20 "$WORK/backend.log" 2>/dev/null || true
|
echo; echo "--- backend log (tail) ---"; tail -20 "$WORK/backend.log" 2>/dev/null || true
|
||||||
@@ -66,6 +76,19 @@ if [ -n "$CAM_HOST" ]; then
|
|||||||
ok "$CAMERA_ID accepts connections on 6000"
|
ok "$CAMERA_ID accepts connections on 6000"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "$BROWSER" = "1" ]; then
|
||||||
|
for name in SUPABASE_URL SUPABASE_SECRET_KEY SUPABASE_PUBLISHABLE_KEY; do
|
||||||
|
[ -n "${!name:-}" ] || fail "$name is not set - source ../dev-secrets.sh (BROWSER=1)"
|
||||||
|
done
|
||||||
|
[ -x "$CHROME_BIN" ] || fail "no Chrome at $CHROME_BIN"
|
||||||
|
[ -d "$APP_DIR/apps/web/node_modules" ] || fail "$APP_DIR has no installed web app"
|
||||||
|
SB_USER="$(bun scripts/e2e-supabase-user.ts)" || fail "could not provision the Supabase test user"
|
||||||
|
sb_field() { printf '%s' "$SB_USER" | python3 -c 'import json,sys; print(json.load(sys.stdin)[sys.argv[1]])' "$1"; }
|
||||||
|
USER_ID="$(sb_field id)"; USER_TOKEN="$(sb_field accessToken)"; SB_EMAIL="$(sb_field email)"; SB_PASSWORD="$(sb_field password)"
|
||||||
|
unset SB_USER
|
||||||
|
ok "signed in as the real Supabase test user"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "==> seeding an organisation and a farm claiming '$CAMERA_ID'"
|
echo "==> seeding an organisation and a farm claiming '$CAMERA_ID'"
|
||||||
EDGE_PROXY_TOKEN="e2e-$(date +%s)"
|
EDGE_PROXY_TOKEN="e2e-$(date +%s)"
|
||||||
ORG_ID="$(psql_q "INSERT INTO organisations (name) VALUES ('e2e snapshot org') RETURNING id")"
|
ORG_ID="$(psql_q "INSERT INTO organisations (name) VALUES ('e2e snapshot org') RETURNING id")"
|
||||||
@@ -74,13 +97,15 @@ FARM_ID="$(psql_q "INSERT INTO farms (name, org_id, edge_proxy_token) VALUES ('e
|
|||||||
psql_q "INSERT INTO printers (farm_id, name, vendor, gateway_printer_id) VALUES ('$FARM_ID', 'snapshot under test', 'bambu', '$CAMERA_ID')" >/dev/null
|
psql_q "INSERT INTO printers (farm_id, name, vendor, gateway_printer_id) VALUES ('$FARM_ID', 'snapshot under test', 'bambu', '$CAMERA_ID')" >/dev/null
|
||||||
CAM_UUID="$(psql_q "SELECT id FROM printers WHERE farm_id='$FARM_ID'")"
|
CAM_UUID="$(psql_q "SELECT id FROM printers WHERE farm_id='$FARM_ID'")"
|
||||||
|
|
||||||
echo "==> starting the dev-auth issuer and the backend"
|
echo "==> starting the auth issuer and the backend"
|
||||||
bun scripts/dev-auth.ts --port "$AUTH_PORT" > "$WORK/auth.log" 2>&1 &
|
if [ "$BROWSER" != "1" ]; then
|
||||||
AUTH_PID=$!
|
bun scripts/dev-auth.ts --port "$AUTH_PORT" > "$WORK/auth.log" 2>&1 &
|
||||||
AUTH_URL="http://localhost:$AUTH_PORT"
|
AUTH_PID=$!
|
||||||
for _ in $(seq 1 40); do curl -sf "$AUTH_URL/auth/v1/.well-known/jwks.json" >/dev/null 2>&1 && break; sleep 0.25; done
|
AUTH_URL="http://localhost:$AUTH_PORT"
|
||||||
export SUPABASE_URL="$AUTH_URL"
|
for _ in $(seq 1 40); do curl -sf "$AUTH_URL/auth/v1/.well-known/jwks.json" >/dev/null 2>&1 && break; sleep 0.25; done
|
||||||
USER_TOKEN="$(curl -sf "$AUTH_URL/token?sub=$USER_ID" | python3 -c 'import json,sys; print(json.load(sys.stdin)["access_token"])')"
|
export SUPABASE_URL="$AUTH_URL"
|
||||||
|
USER_TOKEN="$(curl -sf "$AUTH_URL/token?sub=$USER_ID" | python3 -c 'import json,sys; print(json.load(sys.stdin)["access_token"])')"
|
||||||
|
fi
|
||||||
DISPATCH_ENABLED=false bun run src/index.ts > "$WORK/backend.log" 2>&1 &
|
DISPATCH_ENABLED=false bun run src/index.ts > "$WORK/backend.log" 2>&1 &
|
||||||
BACKEND_PID=$!
|
BACKEND_PID=$!
|
||||||
for _ in $(seq 1 60); do curl -sf "$BASE/health" >/dev/null 2>&1 && break; sleep 0.5; done
|
for _ in $(seq 1 60); do curl -sf "$BASE/health" >/dev/null 2>&1 && break; sleep 0.5; done
|
||||||
@@ -161,6 +186,30 @@ if [ -n "${SOAK_S:-}" ]; then
|
|||||||
ok "camera still answers after the soak"
|
ok "camera still answers after the soak"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "$BROWSER" = "1" ]; then
|
||||||
|
echo "==> the real console in real Chrome"
|
||||||
|
setsid env -C "$APP_DIR/apps/web" NUXT_PUBLIC_API_BASE="$BASE" NUXT_PUBLIC_WS_BASE="ws://localhost:$PORT" \
|
||||||
|
SUPABASE_URL="$SUPABASE_URL" SUPABASE_KEY="$SUPABASE_PUBLISHABLE_KEY" NUXT_PUBLIC_PASSWORD_LOGIN=true \
|
||||||
|
pnpm exec nuxt dev --host "$APP_HOST" --port "$APP_PORT" > "$WORK/app.log" 2>&1 &
|
||||||
|
APP_PID=$!
|
||||||
|
for _ in $(seq 1 120); do curl -sf -o /dev/null "http://127.0.0.1:$APP_PORT/login" 2>/dev/null && break; sleep 1; done
|
||||||
|
curl -sf -o /dev/null "http://127.0.0.1:$APP_PORT/login" || { tail -20 "$WORK/app.log" >&2; fail "the web console did not start"; }
|
||||||
|
OUT="$(env APP_URL="http://127.0.0.1:$APP_PORT" SUPABASE_E2E_EMAIL="$SB_EMAIL" SUPABASE_E2E_PASSWORD="$SB_PASSWORD" \
|
||||||
|
CHROME_BIN="$CHROME_BIN" timeout 240 bun scripts/e2e-camera-snapshot-browser.ts 2>&1 || true)"
|
||||||
|
seen=0; bad=0
|
||||||
|
while IFS=$'\t' read -r verdict label; do
|
||||||
|
case "$verdict" in
|
||||||
|
ok) ok "$label"; seen=$((seen + 1)) ;;
|
||||||
|
FAILED) echo " FAILED: $label" >&2; seen=$((seen + 1)); bad=$((bad + 1)) ;;
|
||||||
|
esac
|
||||||
|
done < <(printf '%s\n' "$OUT" | grep -E "^(ok|FAILED)$(printf '\t')")
|
||||||
|
if [ "$seen" -lt 8 ] && [ "$bad" -eq 0 ]; then bad=1; echo " FAILED: only $seen of 8 browser results" >&2; fi
|
||||||
|
if [ "$bad" -ne 0 ]; then
|
||||||
|
printf '%s\n' "$OUT" | sed 's/^/ | /' >&2; tail -15 "$WORK/app.log" >&2
|
||||||
|
fail "the real browser did not show the snapshot camera"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
[ -n "$SAVE_FRAME" ] && cp "$WORK/f2.jpg" "$SAVE_FRAME" && ok "kept a frame at $SAVE_FRAME"
|
[ -n "$SAVE_FRAME" ] && cp "$WORK/f2.jpg" "$SAVE_FRAME" && ok "kept a frame at $SAVE_FRAME"
|
||||||
echo
|
echo
|
||||||
echo "PASS - a P1/A1 camera frame travelled printer -> proxy -> backend -> an authenticated viewer."
|
echo "PASS - a P1/A1 camera frame travelled printer -> proxy -> backend -> an authenticated viewer."
|
||||||
|
|||||||
Reference in New Issue
Block a user