feat(frontend): F1 widgets (select/info/freetext) + reserved controls
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { FreetextWidget } from "./FreetextWidget";
|
||||
test("submits typed text", async () => {
|
||||
const onRespond = vi.fn();
|
||||
render(<FreetextWidget descriptor={{ id: "u1", widget: "freetext" }} onRespond={onRespond} />);
|
||||
await userEvent.type(screen.getByRole("textbox"), "ciao");
|
||||
await userEvent.click(screen.getByRole("button", { name: /invia/i }));
|
||||
expect(onRespond).toHaveBeenCalledWith({ id: "u1", kind: "freetext", text: "ciao" });
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { useState } from "react";
|
||||
import type { WidgetProps } from "./types";
|
||||
export function FreetextWidget({ descriptor, onRespond }: WidgetProps) {
|
||||
const [text, setText] = useState("");
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{descriptor.title && <p className="font-medium">{descriptor.title}</p>}
|
||||
<textarea className="w-full border rounded p-2" value={text} onChange={(e) => setText(e.target.value)} />
|
||||
<button className="border rounded px-3 py-1" onClick={() => onRespond({ id: descriptor.id, kind: "freetext", text })}>Invia</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { WidgetProps } from "./types";
|
||||
export function InfoWidget({ descriptor }: WidgetProps) {
|
||||
const color = descriptor.level === "error" ? "text-red-600" : descriptor.level === "warning" ? "text-amber-600" : "text-foreground";
|
||||
return <p className={`text-sm ${color}`}>{descriptor.text ?? descriptor.title}</p>;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
const LABELS: Record<string, string> = { back: "Torna indietro", exit: "Esci", other: "Altro — specifica" };
|
||||
export function ReservedControls({ reserved, onControl }: { reserved?: string[]; onControl: (c: string) => void }) {
|
||||
if (!reserved?.length) return null;
|
||||
return <div className="flex gap-2 pt-2">{reserved.map((c) => (
|
||||
<button key={c} className="text-sm border rounded px-2 py-1" onClick={() => onControl(c)}>{LABELS[c] ?? c}</button>
|
||||
))}</div>;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { SelectWidget } from "./SelectWidget";
|
||||
|
||||
test("picking an option responds with its id", async () => {
|
||||
const onRespond = vi.fn();
|
||||
render(<SelectWidget descriptor={{ id: "u1", widget: "select", options: [{ id: "a", label: "A" }, { id: "b", label: "B", recommended: true }] }} onRespond={onRespond} />);
|
||||
expect(screen.getByText(/consigliato/i)).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByRole("button", { name: /A/ }));
|
||||
expect(onRespond).toHaveBeenCalledWith({ id: "u1", kind: "select", choices: ["a"] });
|
||||
});
|
||||
test("a reserved control responds with control, not a choice", async () => {
|
||||
const onRespond = vi.fn();
|
||||
render(<SelectWidget descriptor={{ id: "u1", widget: "select", options: [{ id: "a", label: "A" }], reserved: ["back"] }} onRespond={onRespond} />);
|
||||
await userEvent.click(screen.getByRole("button", { name: /indietro/i }));
|
||||
expect(onRespond).toHaveBeenCalledWith({ id: "u1", control: "back" });
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { WidgetProps } from "./types";
|
||||
import { ReservedControls } from "./ReservedControls";
|
||||
export function SelectWidget({ descriptor, onRespond }: WidgetProps) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{descriptor.title && <p className="font-medium">{descriptor.title}</p>}
|
||||
{descriptor.intro && <p className="text-sm text-muted-foreground">{descriptor.intro}</p>}
|
||||
<div className="flex flex-col gap-2">
|
||||
{descriptor.options?.map((o) => (
|
||||
<button key={o.id} className="border rounded px-3 py-2 text-left hover:bg-accent"
|
||||
onClick={() => onRespond({ id: descriptor.id, kind: "select", choices: [o.id] })}>
|
||||
{o.label}{o.recommended && <span className="ml-2 text-xs text-green-600">(consigliato)</span>}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<ReservedControls reserved={descriptor.reserved} onControl={(c) => onRespond({ id: descriptor.id, control: c })} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { register } from "./registry";
|
||||
import { SelectWidget } from "./SelectWidget";
|
||||
import { InfoWidget } from "./InfoWidget";
|
||||
import { FreetextWidget } from "./FreetextWidget";
|
||||
register("select", SelectWidget); register("info", InfoWidget); register("freetext", FreetextWidget);
|
||||
export { resolve } from "./registry";
|
||||
Reference in New Issue
Block a user