Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions ui/e2e/infobar-lock-toggles.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { test, expect } from "@playwright/test";

import { ensureLocalAuthMode, waitForWebRTCReady, getLedState, waitForLedState } from "./helpers";

test.describe("InfoBar lock toggle buttons", () => {
test.setTimeout(60_000);

test.beforeEach(async ({ page }) => {
await page.goto("/");
await page.waitForLoadState("networkidle");
await ensureLocalAuthMode(page, { mode: "noPassword" });
await waitForWebRTCReady(page);
});

test("Caps Lock toggle button sends scancode and reflects LED state", async ({ page }) => {
const initial = await getLedState(page);
expect(initial, "LED state should be available").not.toBeNull();
const initialCapsLock = initial!.caps_lock;

const capsBtn = page.getByTestId("caps-lock-toggle");
await expect(capsBtn).toBeVisible();

await capsBtn.click();
await waitForLedState(page, "caps_lock", !initialCapsLock);

await capsBtn.click();
await waitForLedState(page, "caps_lock", initialCapsLock);
});

test("Num Lock toggle button sends scancode and reflects LED state", async ({ page }) => {
const initial = await getLedState(page);
expect(initial, "LED state should be available").not.toBeNull();
const initialNumLock = initial!.num_lock;

const numBtn = page.getByTestId("num-lock-toggle");
await expect(numBtn).toBeVisible();

await numBtn.click();
await waitForLedState(page, "num_lock", !initialNumLock);

await numBtn.click();
await waitForLedState(page, "num_lock", initialNumLock);
});
});
52 changes: 30 additions & 22 deletions ui/src/components/InfoBar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";

import {
useHidStore,
Expand All @@ -9,9 +9,11 @@ import {
VideoState,
} from "@hooks/stores";
import { useHidRpc } from "@hooks/useHidRpc";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import { keys, modifiers } from "@/keyboardMappings";
import { cx } from "@/cva.config";
import { m } from "@localizations/messages.js";
import { Button } from "@components/Button";

export default function InfoBar() {
const { keysDownState } = useHidStore();
Expand All @@ -32,6 +34,18 @@ export default function InfoBar() {
const { isTurnServerInUse, peerConnection, peerConnectionState } = useRTCStore();
const { hdmiState } = useVideoStore();

const { send } = useJsonRpc();

const sendLockKey = useCallback(
(keyCode: number) => {
send("keypressReport", { key: keyCode, press: true });
setTimeout(() => {
send("keypressReport", { key: keyCode, press: false });
}, 20);
},
[send],
);

const [videoCodec, setVideoCodec] = useState<string | null>(null);
const [videoBitrate, setVideoBitrate] = useState<string | null>(null);

Expand Down Expand Up @@ -211,27 +225,21 @@ export default function InfoBar() {
</div>
)}

<div
className={cx(
"shrink-0 p-1 px-1.5 text-xs",
keyboardLedState.caps_lock
? "text-black dark:text-white"
: "text-slate-800/20 dark:text-slate-300/20",
)}
>
{m.info_caps_lock()}
</div>

<div
className={cx(
"shrink-0 p-1 px-1.5 text-xs",
keyboardLedState.num_lock
? "text-black dark:text-white"
: "text-slate-800/20 dark:text-slate-300/20",
)}
>
{m.info_num_lock()}
</div>
<Button
size="XS"
theme={keyboardLedState.caps_lock ? "primary" : "light"}
text={m.info_caps_lock()}
onClick={() => sendLockKey(keys.CapsLock)}
data-testid="caps-lock-toggle"
/>

<Button
size="XS"
theme={keyboardLedState.num_lock ? "primary" : "light"}
text={m.info_num_lock()}
onClick={() => sendLockKey(keys.NumLock)}
data-testid="num-lock-toggle"
/>

<div
className={cx(
Expand Down
Loading