mirror of
https://github.com/jetkvm/kvm.git
synced 2026-05-21 05:20:35 +00:00
* fix: add settling delay in VideoStart() when waking from sleep mode (#519) * test: improve e2e test reliability and reduce flakiness - Replace fire-sleep-assert with polling in wheel scroll tests - Add retry loops for macro test and RPC setup after USB re-enumeration - Add waitForRpcReady helper to handle session dialogs and stale pages - Remove flaky paste modal UI test (redundant with keyboard scan tests) - Preserve SSH keys and dev mode across config resets in setup/teardown - Add saveSSHDevState/restoreSSHDevState helpers * fix: trim sysfs whitespace in getSleepMode() so sleep detection works Linux sysfs attributes include a trailing newline, so comparing raw content with "1" always returned false. Use strings.TrimSpace to match the convention used elsewhere in the codebase.
This commit is contained in:
@@ -3,6 +3,7 @@ package native
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -85,7 +86,7 @@ func (n *Native) getSleepMode() (bool, error) {
|
||||
|
||||
data, err := os.ReadFile(sleepModeFile)
|
||||
if err == nil {
|
||||
return string(data) == "1", nil
|
||||
return strings.TrimSpace(string(data)) == "1", nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
@@ -192,9 +193,19 @@ func (n *Native) VideoStart() error {
|
||||
n.videoLock.Lock()
|
||||
defer n.videoLock.Unlock()
|
||||
|
||||
// check if the chip is currently in sleep mode
|
||||
wasSleeping, _ := n.getSleepMode()
|
||||
|
||||
// disable sleep mode before starting video
|
||||
_ = n.setSleepMode(false)
|
||||
|
||||
// when waking from sleep, the capture chip needs time to re-lock the HDMI
|
||||
// signal before we can start streaming (similar to the delay in useExtraLock)
|
||||
if wasSleeping {
|
||||
n.l.Info().Msg("capture chip was sleeping, waiting for signal re-lock")
|
||||
time.Sleep(extraLockTimeout)
|
||||
}
|
||||
|
||||
videoStart()
|
||||
return nil
|
||||
}
|
||||
|
||||
+13
-1
@@ -1,7 +1,15 @@
|
||||
import * as fs from "fs";
|
||||
import { promisify } from "util";
|
||||
import { exec } from "child_process";
|
||||
import { sshExec, getDeviceHost, resetConfigViaSSH, restartAppViaSSH, SSH_OPTS } from "./helpers";
|
||||
import {
|
||||
sshExec,
|
||||
getDeviceHost,
|
||||
resetConfigViaSSH,
|
||||
restartAppViaSSH,
|
||||
saveSSHDevState,
|
||||
restoreSSHDevState,
|
||||
SSH_OPTS,
|
||||
} from "./helpers";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
@@ -25,7 +33,11 @@ export default async function globalSetup() {
|
||||
await execAsync(`${sshCmd} < "${binaryPath}"`);
|
||||
|
||||
await sshExec("chmod +x /userdata/jetkvm/bin/jetkvm_app");
|
||||
|
||||
const saved = await saveSSHDevState();
|
||||
await resetConfigViaSSH();
|
||||
await restoreSSHDevState(saved);
|
||||
|
||||
await restartAppViaSSH();
|
||||
console.log("[global-setup] Device ready.");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { sshExec, resetConfigViaSSH, restartAppViaSSH } from "./helpers";
|
||||
import {
|
||||
sshExec,
|
||||
resetConfigViaSSH,
|
||||
restartAppViaSSH,
|
||||
saveSSHDevState,
|
||||
restoreSSHDevState,
|
||||
} from "./helpers";
|
||||
|
||||
export default async function globalTeardown() {
|
||||
const resultsDir = path.resolve(
|
||||
@@ -31,7 +37,9 @@ export default async function globalTeardown() {
|
||||
|
||||
console.log("[global-teardown] Resetting device to clean state...");
|
||||
try {
|
||||
const saved = await saveSSHDevState();
|
||||
await resetConfigViaSSH();
|
||||
await restoreSSHDevState(saved);
|
||||
await restartAppViaSSH();
|
||||
console.log("[global-teardown] Device reset complete.");
|
||||
} catch {
|
||||
|
||||
@@ -687,6 +687,33 @@ export async function resetConfigViaSSH(): Promise<void> {
|
||||
await sshExec("sync");
|
||||
}
|
||||
|
||||
export interface SSHDevState {
|
||||
sshKey: string;
|
||||
devModeEnabled: boolean;
|
||||
}
|
||||
|
||||
export async function saveSSHDevState(): Promise<SSHDevState> {
|
||||
const sshKey = await sshExec("cat /userdata/dropbear/.ssh/authorized_keys 2>/dev/null", true);
|
||||
const devMode = await sshExec(
|
||||
"test -f /userdata/jetkvm/devmode.enable && echo 1 || echo 0",
|
||||
true,
|
||||
);
|
||||
return { sshKey: sshKey.trim(), devModeEnabled: devMode.trim() === "1" };
|
||||
}
|
||||
|
||||
export async function restoreSSHDevState(state: SSHDevState): Promise<void> {
|
||||
if (state.sshKey) {
|
||||
await sshExec("mkdir -p /userdata/dropbear/.ssh && chmod 700 /userdata/dropbear/.ssh");
|
||||
const b64 = Buffer.from(state.sshKey).toString("base64");
|
||||
await sshExec(
|
||||
`echo ${b64} | base64 -d > /userdata/dropbear/.ssh/authorized_keys && chmod 600 /userdata/dropbear/.ssh/authorized_keys`,
|
||||
);
|
||||
}
|
||||
if (state.devModeEnabled) {
|
||||
await sshExec("mkdir -p /userdata/jetkvm && touch /userdata/jetkvm/devmode.enable");
|
||||
}
|
||||
}
|
||||
|
||||
export async function restartAppViaSSH(): Promise<void> {
|
||||
await sshExec("killall jetkvm_app", true);
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* Run with:
|
||||
* JETKVM_URL=http://<kvm-ip> JETKVM_REMOTE_HOST=<host-ip> npx playwright test ra-all
|
||||
*/
|
||||
import { execSync } from "child_process";
|
||||
import { test, expect, type Page } from "@playwright/test";
|
||||
import {
|
||||
HID_KEY,
|
||||
@@ -20,7 +21,35 @@ import {
|
||||
waitForLedState,
|
||||
restartAppViaSSH,
|
||||
} from "../helpers";
|
||||
import { createRemoteAgent, KEY, HID_TO_LINUX } from "./remote-agent";
|
||||
import {
|
||||
createRemoteAgent,
|
||||
KEY,
|
||||
HID_TO_LINUX,
|
||||
type MouseEvent as RAMouseEvent,
|
||||
type KeyboardEvent as RAKeyboardEvent,
|
||||
} from "./remote-agent";
|
||||
|
||||
/** Run a command on the remote host (the machine whose display is captured by the KVM). */
|
||||
function remoteHostExec(cmd: string): string {
|
||||
const target = process.env.JETKVM_REMOTE_HOST;
|
||||
if (!target) throw new Error("JETKVM_REMOTE_HOST not set");
|
||||
const sshOpts =
|
||||
"-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o ConnectTimeout=10";
|
||||
return execSync(`ssh ${sshOpts} ${target} '${cmd}'`, {
|
||||
encoding: "utf8",
|
||||
timeout: 15000,
|
||||
});
|
||||
}
|
||||
|
||||
/** Toggle DPMS on the remote host via GNOME ScreenSaver D-Bus API. */
|
||||
function remoteHostSetDPMS(off: boolean): void {
|
||||
remoteHostExec(
|
||||
`DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus ` +
|
||||
`gdbus call --session --dest org.gnome.ScreenSaver ` +
|
||||
`--object-path /org/gnome/ScreenSaver ` +
|
||||
`--method org.gnome.ScreenSaver.SetActive ${off ? "true" : "false"}`,
|
||||
);
|
||||
}
|
||||
|
||||
const agent = createRemoteAgent();
|
||||
|
||||
@@ -201,13 +230,47 @@ async function ensureNoPasswordViaAPI() {
|
||||
}
|
||||
}
|
||||
|
||||
async function setupMacrosViaRPC(page: Page) {
|
||||
const existing = (await callJsonRpc(page, "getKeyboardMacros")) as { id: string }[];
|
||||
const ids = new Set(existing.map(m => m.id));
|
||||
if (TEST_MACROS.every(m => ids.has(m.id))) return;
|
||||
async function setupMacrosViaRPC(page: Page, retries = 3) {
|
||||
for (let attempt = 1; attempt <= retries; attempt++) {
|
||||
try {
|
||||
const existing = (await callJsonRpc(page, "getKeyboardMacros")) as { id: string }[];
|
||||
const ids = new Set(existing.map(m => m.id));
|
||||
if (TEST_MACROS.every(m => ids.has(m.id))) return;
|
||||
|
||||
const merged = [...existing.filter(m => !m.id.startsWith("e2e_test_")), ...TEST_MACROS];
|
||||
await callJsonRpc(page, "setKeyboardMacros", { params: { macros: merged } });
|
||||
const merged = [...existing.filter(m => !m.id.startsWith("e2e_test_")), ...TEST_MACROS];
|
||||
await callJsonRpc(page, "setKeyboardMacros", { params: { macros: merged } });
|
||||
return;
|
||||
} catch (err) {
|
||||
if (attempt === retries) throw err;
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForRpcReady(page: Page, timeoutMs = 30000) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
let reloaded = false;
|
||||
while (Date.now() < deadline) {
|
||||
// Dismiss "Another Active Session" dialog if it appears
|
||||
const useHereBtn = page.getByRole("button", { name: "Use Here" });
|
||||
if (await useHereBtn.isVisible({ timeout: 200 }).catch(() => false)) {
|
||||
await useHereBtn.click();
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
}
|
||||
try {
|
||||
await callJsonRpc(page, "getDeviceID");
|
||||
return;
|
||||
} catch {
|
||||
// If RPC keeps failing, try a full page reload once
|
||||
if (!reloaded && Date.now() > deadline - timeoutMs + 10000) {
|
||||
reloaded = true;
|
||||
await page.reload({ waitUntil: "networkidle" });
|
||||
await waitForWebRTCReady(page);
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
}
|
||||
}
|
||||
throw new Error(`RPC channel not ready after ${timeoutMs}ms`);
|
||||
}
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
@@ -217,7 +280,20 @@ test.beforeAll(async ({ browser }) => {
|
||||
|
||||
sharedPage = await browser.newPage();
|
||||
await sharedPage.goto("/", { waitUntil: "networkidle" });
|
||||
|
||||
// If the page redirected to the welcome/setup flow, complete setup and reload
|
||||
if (sharedPage.url().includes("/welcome")) {
|
||||
const host = getDeviceHost();
|
||||
await fetch(`http://${host}/device/setup`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ localAuthMode: "noPassword" }),
|
||||
});
|
||||
await sharedPage.goto("/", { waitUntil: "networkidle" });
|
||||
}
|
||||
|
||||
await waitForWebRTCReady(sharedPage);
|
||||
await waitForRpcReady(sharedPage);
|
||||
|
||||
await setupMacrosViaRPC(sharedPage);
|
||||
await sharedPage.reload({ waitUntil: "networkidle" });
|
||||
@@ -228,7 +304,6 @@ test.beforeAll(async ({ browser }) => {
|
||||
|
||||
test.afterAll(async () => {
|
||||
if (!agent) return;
|
||||
// Clean up test macros via RPC (no SSH needed)
|
||||
try {
|
||||
const existing = (await callJsonRpc(sharedPage, "getKeyboardMacros")) as { id: string }[];
|
||||
const filtered = existing.filter(m => !m.id.startsWith("e2e_test_"));
|
||||
@@ -549,60 +624,73 @@ test.describe("Remote Host Agent", () => {
|
||||
// Vertical scroll
|
||||
await agent!.clearMouseEvents();
|
||||
await callJsonRpc(sharedPage, "wheelReport", { wheelY: 1, wheelX: 0 });
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
|
||||
const vEvents = await agent!.getMouseEvents();
|
||||
const vWheel = vEvents.filter(ev => ev.type === "mouse_move_rel" && ev.code === REL_WHEEL);
|
||||
const vWheel = await agent!.waitForMouseEvent(
|
||||
ev => ev.type === "mouse_move_rel" && ev.code === REL_WHEEL,
|
||||
3000,
|
||||
);
|
||||
expect(vWheel.length, "Vertical wheel event should be received").toBeGreaterThan(0);
|
||||
expect(vWheel[0].value).not.toBe(0);
|
||||
|
||||
// Horizontal scroll
|
||||
await agent!.clearMouseEvents();
|
||||
await callJsonRpc(sharedPage, "wheelReport", { wheelY: 0, wheelX: 1 });
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
|
||||
const hEvents = await agent!.getMouseEvents();
|
||||
const hWheel = hEvents.filter(ev => ev.type === "mouse_move_rel" && ev.code === REL_HWHEEL);
|
||||
const hWheel = await agent!.waitForMouseEvent(
|
||||
ev => ev.type === "mouse_move_rel" && ev.code === REL_HWHEEL,
|
||||
3000,
|
||||
);
|
||||
expect(hWheel.length, "Horizontal wheel event should be received").toBeGreaterThan(0);
|
||||
expect(hWheel[0].value).not.toBe(0);
|
||||
|
||||
// Both axes simultaneously
|
||||
await agent!.clearMouseEvents();
|
||||
await callJsonRpc(sharedPage, "wheelReport", { wheelY: -1, wheelX: 1 });
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
|
||||
const bothEvents = await agent!.getMouseEvents();
|
||||
const bothV = bothEvents.filter(ev => ev.type === "mouse_move_rel" && ev.code === REL_WHEEL);
|
||||
const bothH = bothEvents.filter(ev => ev.type === "mouse_move_rel" && ev.code === REL_HWHEEL);
|
||||
const bothV = await agent!.waitForMouseEvent(
|
||||
ev => ev.type === "mouse_move_rel" && ev.code === REL_WHEEL,
|
||||
3000,
|
||||
);
|
||||
expect(bothV.length, "Vertical wheel in combined event").toBeGreaterThan(0);
|
||||
const bothEvents = await agent!.getMouseEvents();
|
||||
const bothH = bothEvents.filter(ev => ev.type === "mouse_move_rel" && ev.code === REL_HWHEEL);
|
||||
expect(bothH.length, "Horizontal wheel in combined event").toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("mouse: wheel scroll works in relative-only mouse mode", async () => {
|
||||
test.setTimeout(30_000);
|
||||
const REL_WHEEL = 0x08;
|
||||
const REL_HWHEEL = 0x06;
|
||||
|
||||
await callJsonRpc(sharedPage, "setUsbDevices", { devices: USB_DEVICES_REL_MOUSE_ONLY });
|
||||
await agent!.waitForInputDevices(["keyboard", "relative_mouse"], 10000);
|
||||
|
||||
// After USB device re-enumeration the remote agent needs time to re-open
|
||||
// the new /dev/input/event* nodes — poll with retries instead of fixed sleep.
|
||||
try {
|
||||
// Vertical scroll
|
||||
await agent!.clearMouseEvents();
|
||||
await callJsonRpc(sharedPage, "wheelReport", { wheelY: 1, wheelX: 0 });
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
|
||||
const vEvents = await agent!.getMouseEvents();
|
||||
const vWheel = vEvents.filter(ev => ev.type === "mouse_move_rel" && ev.code === REL_WHEEL);
|
||||
// Vertical scroll — retry sending until the agent picks it up
|
||||
const vDeadline = Date.now() + 10000;
|
||||
let vWheel: RAMouseEvent[] = [];
|
||||
while (Date.now() < vDeadline) {
|
||||
await agent!.clearMouseEvents();
|
||||
await callJsonRpc(sharedPage, "wheelReport", { wheelY: 1, wheelX: 0 });
|
||||
try {
|
||||
vWheel = await agent!.waitForMouseEvent(
|
||||
ev => ev.type === "mouse_move_rel" && ev.code === REL_WHEEL,
|
||||
2000,
|
||||
);
|
||||
break;
|
||||
} catch {
|
||||
/* agent not ready yet, retry */
|
||||
}
|
||||
}
|
||||
expect(vWheel.length, "Vertical wheel in relative-only mode").toBeGreaterThan(0);
|
||||
expect(vWheel[0].value).not.toBe(0);
|
||||
|
||||
// Horizontal scroll
|
||||
await agent!.clearMouseEvents();
|
||||
await callJsonRpc(sharedPage, "wheelReport", { wheelY: 0, wheelX: 1 });
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
|
||||
const hEvents = await agent!.getMouseEvents();
|
||||
const hWheel = hEvents.filter(ev => ev.type === "mouse_move_rel" && ev.code === REL_HWHEEL);
|
||||
const hWheel = await agent!.waitForMouseEvent(
|
||||
ev => ev.type === "mouse_move_rel" && ev.code === REL_HWHEEL,
|
||||
3000,
|
||||
);
|
||||
expect(hWheel.length, "Horizontal wheel in relative-only mode").toBeGreaterThan(0);
|
||||
expect(hWheel[0].value).not.toBe(0);
|
||||
} finally {
|
||||
@@ -612,60 +700,32 @@ test.describe("Remote Host Agent", () => {
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// INPUT: PASTE + MACROS
|
||||
// INPUT: MACROS
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
test("input: paste text and macros", async () => {
|
||||
// ── Paste ──
|
||||
const expectedPasteKeys = [KEY.H, KEY.I, KEY.KEY_5];
|
||||
await agent!.clearKeyboardEvents();
|
||||
test("input: keyboard macros", async () => {
|
||||
test.setTimeout(30_000);
|
||||
|
||||
await sharedPage.getByRole("button", { name: "Paste text" }).click();
|
||||
const textarea = sharedPage.locator("textarea#asd");
|
||||
await textarea.waitFor({ state: "visible", timeout: 3000 });
|
||||
await textarea.fill("hi5");
|
||||
|
||||
const confirmBtn = sharedPage.getByRole("button", { name: "Confirm Paste" });
|
||||
await confirmBtn.waitFor({ state: "visible", timeout: 2000 });
|
||||
await confirmBtn.click({ force: true });
|
||||
|
||||
const pasteDeadline = Date.now() + 5000;
|
||||
let pasteMatchIdx = 0;
|
||||
while (Date.now() < pasteDeadline) {
|
||||
const events = await agent!.getKeyboardEvents();
|
||||
const pressedCodes = events.filter(ev => ev.type === "key_press").map(ev => ev.code);
|
||||
pasteMatchIdx = 0;
|
||||
for (const code of pressedCodes) {
|
||||
if (code === expectedPasteKeys[pasteMatchIdx]) {
|
||||
pasteMatchIdx++;
|
||||
if (pasteMatchIdx === expectedPasteKeys.length) break;
|
||||
}
|
||||
}
|
||||
if (pasteMatchIdx === expectedPasteKeys.length) break;
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
}
|
||||
expect(pasteMatchIdx, `Paste: expected 3 keys but matched ${pasteMatchIdx}`).toBe(
|
||||
expectedPasteKeys.length,
|
||||
);
|
||||
|
||||
// Dismiss any lingering paste dialog
|
||||
const cancelBtn = sharedPage.getByRole("button", { name: "Cancel" });
|
||||
if (await cancelBtn.isVisible({ timeout: 300 }).catch(() => false)) {
|
||||
await cancelBtn.click();
|
||||
}
|
||||
|
||||
// ── Macros ──
|
||||
|
||||
// Single key press (A)
|
||||
// Single key press (A) — retry in case the remote agent is still
|
||||
// re-opening input devices after the previous USB mode switch.
|
||||
const keyABtn = sharedPage.getByRole("button", { name: "E2E KeyA" });
|
||||
await keyABtn.waitFor({ state: "visible", timeout: 5000 });
|
||||
await agent!.clearKeyboardEvents();
|
||||
await keyABtn.click();
|
||||
|
||||
let macroEvents = await agent!.waitForKeyboardEvent(
|
||||
ev => ev.code === KEY.A && ev.type === "key_press",
|
||||
3000,
|
||||
);
|
||||
const macroDeadline = Date.now() + 15000;
|
||||
let macroEvents: RAKeyboardEvent[] = [];
|
||||
while (Date.now() < macroDeadline) {
|
||||
await agent!.clearKeyboardEvents();
|
||||
await keyABtn.click();
|
||||
try {
|
||||
macroEvents = await agent!.waitForKeyboardEvent(
|
||||
ev => ev.code === KEY.A && ev.type === "key_press",
|
||||
3000,
|
||||
);
|
||||
break;
|
||||
} catch {
|
||||
/* agent not ready, retry */
|
||||
}
|
||||
}
|
||||
expect(macroEvents.length).toBeGreaterThan(0);
|
||||
|
||||
// Modifier combo (Ctrl+A)
|
||||
@@ -957,6 +1017,90 @@ test.describe("Remote Host Agent", () => {
|
||||
await callJsonRpc(sharedPage, "setVideoSleepMode", { duration: originalDuration });
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// HDMI SLEEP WAKE: SIGNAL RE-DETECTION AFTER DPMS OFF→ON
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
test("hdmi-sleep-wake: re-detects signal after DPMS off→on with chip asleep", async () => {
|
||||
test.setTimeout(120_000);
|
||||
|
||||
const SLEEP_MODE_SYSFS = "/sys/devices/platform/ff470000.i2c/i2c-4/4-000f/sleep_mode";
|
||||
|
||||
const sleepInfo = (await callJsonRpc(sharedPage, "getVideoSleepMode")) as {
|
||||
supported: boolean;
|
||||
duration: number;
|
||||
};
|
||||
|
||||
if (!sleepInfo.supported) {
|
||||
test.skip(true, "HDMI sleep mode not supported on this device");
|
||||
return;
|
||||
}
|
||||
|
||||
const originalDuration = sleepInfo.duration;
|
||||
|
||||
try {
|
||||
// Set a short sleep timer (3s) so the chip enters sleep quickly
|
||||
await callJsonRpc(sharedPage, "setVideoSleepMode", { duration: 3 });
|
||||
|
||||
// Disconnect WebRTC so there are no active sessions → sleep timer starts
|
||||
await sharedPage.goto("about:blank");
|
||||
|
||||
// Wait for sleep timer + margin
|
||||
await new Promise(r => setTimeout(r, 6000));
|
||||
|
||||
// Verify chip entered sleep mode
|
||||
const sleepState = (await sshExec(`cat ${SLEEP_MODE_SYSFS}`)).trim();
|
||||
expect(sleepState, "Capture chip should be in sleep mode").toBe("1");
|
||||
|
||||
// Toggle DPMS off on the remote host (simulates host GPU cutting signal)
|
||||
remoteHostSetDPMS(true);
|
||||
|
||||
// Wait for the GPU to fully cut the TMDS clock
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
|
||||
// Bring the display back on
|
||||
remoteHostSetDPMS(false);
|
||||
|
||||
// Wait for host display to stabilize
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
|
||||
// Reconnect — this triggers VideoStart() which must wake the chip and re-lock
|
||||
await sharedPage.goto("/", { waitUntil: "networkidle" });
|
||||
await waitForWebRTCReady(sharedPage);
|
||||
|
||||
// Verify the chip woke up
|
||||
const wakeState = (await sshExec(`cat ${SLEEP_MODE_SYSFS}`)).trim();
|
||||
expect(wakeState, "Capture chip should be awake after reconnect").toBe("0");
|
||||
|
||||
// Verify video state shows a valid signal (no error)
|
||||
const videoState = (await callJsonRpc(sharedPage, "getVideoState")) as {
|
||||
ready: boolean;
|
||||
error?: string;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
expect(videoState.ready, `Video should be ready but got error: ${videoState.error}`).toBe(
|
||||
true,
|
||||
);
|
||||
expect(videoState.width).toBeGreaterThan(0);
|
||||
expect(videoState.height).toBeGreaterThan(0);
|
||||
} finally {
|
||||
// Always restore DPMS and sleep duration, even if test fails
|
||||
try {
|
||||
remoteHostSetDPMS(false);
|
||||
} catch {
|
||||
// best effort
|
||||
}
|
||||
|
||||
// Reconnect if needed to restore sleep duration via RPC
|
||||
if (sharedPage.url() === "about:blank") {
|
||||
await sharedPage.goto("/", { waitUntil: "networkidle" });
|
||||
await waitForWebRTCReady(sharedPage);
|
||||
}
|
||||
await callJsonRpc(sharedPage, "setVideoSleepMode", { duration: originalDuration });
|
||||
}
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// CONFIG RESET (must be last — resets device config)
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user