feat(frontend): démarrage auto du serveur + erreur port occupé actionnable (#89)
Étend ServerExposureSettings avec autoStart (défaut false, mock aligné sur le backend). useDeployment expose setAutoStart, qui persiste immédiatement le draft sans jamais appeler start() ni toucher le comportement de stop(), et ne met à jour l'état local qu'après confirmation de la sauvegarde (même piste de validation que start/save). DeploymentSettings ajoute la case à cocher sous le panneau Serveur, et des actions « Modifier le port »/« Réessayer » quand le statut échoue avec le code PORT_IN_USE stabilisé côté backend. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@ -210,4 +210,152 @@ describe("DeploymentSettings", () => {
|
||||
await waitFor(() => expect(screen.getByText("Échec")).toBeTruthy());
|
||||
expect(screen.getByRole("alert").textContent).toMatch(/already in use/);
|
||||
});
|
||||
|
||||
describe("auto-start at launch (ticket #89)", () => {
|
||||
it("shows the checkbox unchecked by default, with its exact label and help text", async () => {
|
||||
renderView();
|
||||
await settle();
|
||||
|
||||
const checkbox = screen.getByRole("checkbox", {
|
||||
name: "Démarrer le serveur au lancement d'IdeA",
|
||||
});
|
||||
expect(checkbox).toHaveProperty("checked", false);
|
||||
expect(
|
||||
screen.getByText(
|
||||
"IdeA utilisera les réglages réseau enregistrés ci-dessous au prochain démarrage de l'application desktop.",
|
||||
),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it("persists autoStart:true on toggle ON without starting the server", async () => {
|
||||
const gateway = new MockDesktopServerGateway();
|
||||
const save = vi.spyOn(gateway, "saveExposureSettings");
|
||||
const start = vi.spyOn(gateway, "start");
|
||||
renderView(gateway);
|
||||
await settle();
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole("checkbox", { name: "Démarrer le serveur au lancement d'IdeA" }),
|
||||
);
|
||||
|
||||
await waitFor(() => expect(save).toHaveBeenCalledWith(expect.objectContaining({ autoStart: true })));
|
||||
expect(start).not.toHaveBeenCalled();
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByRole("checkbox", { name: "Démarrer le serveur au lancement d'IdeA" }),
|
||||
).toHaveProperty("checked", true),
|
||||
);
|
||||
expect(screen.getByText("Arrêté")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("persists autoStart:false on toggle OFF", async () => {
|
||||
const gateway = new MockDesktopServerGateway();
|
||||
const save = vi.spyOn(gateway, "saveExposureSettings");
|
||||
renderView(gateway);
|
||||
await settle();
|
||||
|
||||
const checkbox = screen.getByRole("checkbox", {
|
||||
name: "Démarrer le serveur au lancement d'IdeA",
|
||||
});
|
||||
fireEvent.click(checkbox);
|
||||
await waitFor(() => expect(checkbox).toHaveProperty("checked", true));
|
||||
|
||||
fireEvent.click(checkbox);
|
||||
await waitFor(() =>
|
||||
expect(save).toHaveBeenLastCalledWith(expect.objectContaining({ autoStart: false })),
|
||||
);
|
||||
await waitFor(() => expect(checkbox).toHaveProperty("checked", false));
|
||||
});
|
||||
|
||||
it("does not persist the toggle when the draft is otherwise invalid", async () => {
|
||||
const gateway = new MockDesktopServerGateway();
|
||||
renderView(gateway);
|
||||
await settle();
|
||||
// A remote mode with no public origin — saveExposureSettings must reject.
|
||||
selectMode("Accès distant, proxy sur cet ordinateur");
|
||||
|
||||
const checkbox = await screen.findByRole("checkbox", {
|
||||
name: "Démarrer le serveur au lancement d'IdeA",
|
||||
});
|
||||
fireEvent.click(checkbox);
|
||||
|
||||
await waitFor(() => expect(screen.getByRole("alert").textContent).toMatch(/publicOrigin/));
|
||||
// Rejected save: the checkbox must not silently flip to checked.
|
||||
expect(checkbox).toHaveProperty("checked", false);
|
||||
});
|
||||
|
||||
it("leaves autoStart untouched when the server is stopped manually", async () => {
|
||||
const gateway = new MockDesktopServerGateway();
|
||||
const save = vi.spyOn(gateway, "saveExposureSettings");
|
||||
renderView(gateway);
|
||||
await settle();
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole("checkbox", { name: "Démarrer le serveur au lancement d'IdeA" }),
|
||||
);
|
||||
await waitFor(() => expect(save).toHaveBeenCalledTimes(1));
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Démarrer" }));
|
||||
await waitFor(() => expect(screen.getByText("En cours d'exécution")).toBeTruthy());
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Arrêter" }));
|
||||
await waitFor(() => expect(screen.getByText("Arrêté")).toBeTruthy());
|
||||
|
||||
// `stop()` never calls `saveExposureSettings` — autoStart is untouched.
|
||||
expect(save).toHaveBeenCalledTimes(2); // the toggle, then the pre-start persist
|
||||
expect(
|
||||
screen.getByRole("checkbox", { name: "Démarrer le serveur au lancement d'IdeA" }),
|
||||
).toHaveProperty("checked", true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("port-conflict actionable error (ticket #89)", () => {
|
||||
it("shows the dedicated message with « Modifier le port » and « Réessayer » actions", async () => {
|
||||
const gateway = new MockDesktopServerGateway();
|
||||
renderView(gateway);
|
||||
await settle();
|
||||
|
||||
gateway.setStatus({
|
||||
state: "failed",
|
||||
error: { code: "PORT_IN_USE", message: "address already in use" },
|
||||
});
|
||||
|
||||
const alert = await screen.findByRole("alert");
|
||||
expect(alert.textContent).toBe(
|
||||
"Le serveur n'a pas démarré automatiquement. address already in use",
|
||||
);
|
||||
expect(screen.getByRole("button", { name: "Modifier le port" })).toBeTruthy();
|
||||
expect(screen.getByRole("button", { name: "Réessayer" })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("« Modifier le port » focuses the Port field", async () => {
|
||||
const gateway = new MockDesktopServerGateway();
|
||||
renderView(gateway);
|
||||
await settle();
|
||||
|
||||
gateway.setStatus({
|
||||
state: "failed",
|
||||
error: { code: "PORT_IN_USE", message: "address already in use" },
|
||||
});
|
||||
await screen.findByRole("button", { name: "Modifier le port" });
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Modifier le port" }));
|
||||
expect(screen.getByLabelText("Port")).toBe(document.activeElement);
|
||||
});
|
||||
|
||||
it("« Réessayer » calls start() again", async () => {
|
||||
const gateway = new MockDesktopServerGateway();
|
||||
const start = vi.spyOn(gateway, "start");
|
||||
renderView(gateway);
|
||||
await settle();
|
||||
|
||||
gateway.setStatus({
|
||||
state: "failed",
|
||||
error: { code: "PORT_IN_USE", message: "address already in use" },
|
||||
});
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Réessayer" }));
|
||||
|
||||
await waitFor(() => expect(start).toHaveBeenCalled());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user