feat(frontend): terminal pilotable au doigt sur téléphone (#69)
Lots 3 et 4 du ticket #69. Sans ça un agent CLI est indriveable depuis un téléphone : le clavier virtuel n'a ni Esc, ni Tab, ni Ctrl, ni flèches, donc on peut taper un prompt mais pas l'interrompre, compléter un chemin, sortir d'un éditeur ou rappeler l'historique. - `TerminalView` expose un `onReady(api)` optionnel (donc desktop inchangé, et inerte quand xterm ne monte pas). `api.send` passe par `term.input()` : le *même* chemin qu'une frappe réelle, donc le relais PTY, le comptage de lignes et la suspension du write-portal s'appliquent à l'identique. Écrire sur le handle aurait court-circuité le portal. - `TerminalKeyBar` (web-only) : Esc/Tab/Ctrl-C/Ctrl-D/flèches en chips tactiles 44px, scroll horizontal. Chaque tap annule le déplacement de focus et refocalise xterm — sinon le clavier virtuel se referme à chaque touche. - La cellule agent passe de `h-64` fixe (une letterbox d'~20 lignes) à `h-[60dvh]` sur téléphone, `sm:h-80` au-delà. - Tests : séquences d'octets émises, préservation du focus, état désactivé avant montage de xterm, et garde de non-régression sur la frontière — le client web ne monte aucun layout-grid/split/dock desktop. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
76
frontend/src/features/web/TerminalKeyBar.test.tsx
Normal file
76
frontend/src/features/web/TerminalKeyBar.test.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
/**
|
||||
* #69 lot 3 — the mobile key toolbar.
|
||||
*
|
||||
* Guards the two properties that make the terminal drivable from a phone:
|
||||
* the chips emit the exact byte sequences a physical key would, and tapping one
|
||||
* never steals focus from xterm (which would collapse the virtual keyboard).
|
||||
*/
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
|
||||
import type { TerminalInputApi } from "@/features/terminals";
|
||||
import { TerminalKeyBar } from "./TerminalKeyBar";
|
||||
|
||||
function mockApi(): TerminalInputApi & { sent: string[]; focused: () => number } {
|
||||
const sent: string[] = [];
|
||||
let focusCount = 0;
|
||||
return {
|
||||
sent,
|
||||
focused: () => focusCount,
|
||||
send: (d) => void sent.push(d),
|
||||
focus: () => void focusCount++,
|
||||
};
|
||||
}
|
||||
|
||||
describe("TerminalKeyBar", () => {
|
||||
it("sends the byte sequence a physical key would emit", () => {
|
||||
const api = mockApi();
|
||||
render(<TerminalKeyBar api={api} />);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Ctrl-C (interrompre)" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Esc" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Tab" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Flèche haut" }));
|
||||
|
||||
expect(api.sent).toEqual(["\x03", "\x1b", "\t", "\x1b[A"]);
|
||||
});
|
||||
|
||||
it("emits the CSI sequences for every arrow key", () => {
|
||||
const api = mockApi();
|
||||
render(<TerminalKeyBar api={api} />);
|
||||
|
||||
for (const name of ["Flèche haut", "Flèche bas", "Flèche gauche", "Flèche droite"]) {
|
||||
fireEvent.click(screen.getByRole("button", { name }));
|
||||
}
|
||||
|
||||
expect(api.sent).toEqual(["\x1b[A", "\x1b[B", "\x1b[D", "\x1b[C"]);
|
||||
});
|
||||
|
||||
it("keeps the virtual keyboard up: cancels the tap's focus shift, refocuses xterm", () => {
|
||||
const api = mockApi();
|
||||
render(<TerminalKeyBar api={api} />);
|
||||
const ctrlC = screen.getByRole("button", { name: "Ctrl-C (interrompre)" });
|
||||
|
||||
// `fireEvent` returns false when the handler called preventDefault — that is
|
||||
// what stops the browser blurring xterm's textarea and closing the keyboard.
|
||||
expect(fireEvent.pointerDown(ctrlC)).toBe(false);
|
||||
|
||||
fireEvent.click(ctrlC);
|
||||
expect(api.focused()).toBe(1);
|
||||
});
|
||||
|
||||
it("renders disabled until the terminal reports its input API", () => {
|
||||
render(<TerminalKeyBar api={null} />);
|
||||
|
||||
for (const button of screen.getAllByRole("button")) {
|
||||
expect((button as HTMLButtonElement).disabled).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("exposes the keys as an accessible toolbar", () => {
|
||||
const api = mockApi();
|
||||
render(<TerminalKeyBar api={api} />);
|
||||
|
||||
expect(screen.getByRole("toolbar", { name: "Touches du terminal" })).toBeTruthy();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user