feat(service-credential-tester): slice 2 — Test button + gating (shared ServiceTestPanel)
Presentational ServiceTestPanel (props-driven, no internal hooks) wired into both CreateServiceDialog (ServicesPage.tsx) and ServiceConfigEditor (Settings.tsx). Parent owns testResult + saveAnyway state; store-previous pattern resets on input change (avoids setState-in-effect). Create/Save button gated on testPassed || saveAnyway. 7 panel tests (button states, success/failure pills, checkbox toggle). All gates: 158 vitest, build exit 0, lint 0 errors, 362 backend pytest (regression).
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { ServiceTestResult } from "../types";
|
||||
|
||||
interface Props {
|
||||
/** Current test result (null = not tested yet). Parent clears this when the form input changes. */
|
||||
result: ServiceTestResult | null;
|
||||
/** Whether the test mutation is in-flight. */
|
||||
isPending: boolean;
|
||||
/** Whether the "Save anyway" checkbox is checked. */
|
||||
saveAnyway: boolean;
|
||||
/** Fired when the user clicks "Test credentials". */
|
||||
onTest: () => void;
|
||||
/** Fired when the "Save anyway" checkbox toggles. */
|
||||
onSaveAnywayChange: (checked: boolean) => void;
|
||||
/** Disable the Test button (e.g. no draft yet). */
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared "Test credentials" panel used by both the add-service dialog and the
|
||||
* edit-service panel. Purely presentational — the parent owns the test result
|
||||
* + saveAnyway state and the mutation hook. This avoids setState-in-effect
|
||||
* issues with clearing the result on input change (the parent uses the
|
||||
* React-recommended "store previous prop" pattern instead).
|
||||
*/
|
||||
export function ServiceTestPanel({
|
||||
result,
|
||||
isPending,
|
||||
saveAnyway,
|
||||
onTest,
|
||||
onSaveAnywayChange,
|
||||
disabled,
|
||||
}: Props) {
|
||||
const testPassed = result?.ok === true;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onTest}
|
||||
disabled={isPending || disabled}
|
||||
className="mobile-touch-target"
|
||||
>
|
||||
{isPending ? "Testing…" : "Test credentials"}
|
||||
</Button>
|
||||
{result ? (
|
||||
<Alert variant={result.ok ? "default" : "destructive"}>
|
||||
<AlertDescription>
|
||||
{result.ok
|
||||
? `✓ Connected${result.evidence ? ` — ${result.evidence}` : ""}`
|
||||
: `✗ ${result.detail}`}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
<label className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={saveAnyway}
|
||||
onChange={(e) => onSaveAnywayChange(e.target.checked)}
|
||||
/>
|
||||
Save anyway (skip test)
|
||||
</label>
|
||||
{!testPassed && !saveAnyway ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Test credentials or check "Save anyway" to enable the save button.
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { ServiceTestPanel } from "../ServiceTestPanel";
|
||||
import type { ServiceTestResult } from "../../types";
|
||||
|
||||
function noop() {}
|
||||
|
||||
describe("ServiceTestPanel", () => {
|
||||
it("renders the Test credentials button", () => {
|
||||
render(
|
||||
<ServiceTestPanel
|
||||
result={null}
|
||||
isPending={false}
|
||||
saveAnyway={false}
|
||||
onTest={noop}
|
||||
onSaveAnywayChange={noop}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText("Test credentials")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows Testing… and disables button while pending", () => {
|
||||
render(
|
||||
<ServiceTestPanel
|
||||
result={null}
|
||||
isPending={true}
|
||||
saveAnyway={false}
|
||||
onTest={noop}
|
||||
onSaveAnywayChange={noop}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText("Testing…")).toBeTruthy();
|
||||
expect(screen.getByText("Testing…")).toBeDisabled();
|
||||
});
|
||||
|
||||
it("renders green ✓ Connected pill with evidence on success", () => {
|
||||
const result: ServiceTestResult = {
|
||||
ok: true,
|
||||
detail: "ok",
|
||||
evidence: "v4.5.0",
|
||||
};
|
||||
render(
|
||||
<ServiceTestPanel
|
||||
result={result}
|
||||
isPending={false}
|
||||
saveAnyway={false}
|
||||
onTest={noop}
|
||||
onSaveAnywayChange={noop}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText(/✓ Connected — v4.5.0/)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders red ✗ pill with detail on failure", () => {
|
||||
const result: ServiceTestResult = {
|
||||
ok: false,
|
||||
detail: "Authentication failed",
|
||||
evidence: null,
|
||||
};
|
||||
render(
|
||||
<ServiceTestPanel
|
||||
result={result}
|
||||
isPending={false}
|
||||
saveAnyway={false}
|
||||
onTest={noop}
|
||||
onSaveAnywayChange={noop}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText(/✗ Authentication failed/)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("fires onTest when Test credentials is clicked", () => {
|
||||
const onTest = vi.fn();
|
||||
render(
|
||||
<ServiceTestPanel
|
||||
result={null}
|
||||
isPending={false}
|
||||
saveAnyway={false}
|
||||
onTest={onTest}
|
||||
onSaveAnywayChange={noop}
|
||||
/>,
|
||||
);
|
||||
fireEvent.click(screen.getByText("Test credentials"));
|
||||
expect(onTest).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("fires onSaveAnywayChange when checkbox is toggled", () => {
|
||||
const onSaveAnywayChange = vi.fn();
|
||||
render(
|
||||
<ServiceTestPanel
|
||||
result={null}
|
||||
isPending={false}
|
||||
saveAnyway={false}
|
||||
onTest={noop}
|
||||
onSaveAnywayChange={onSaveAnywayChange}
|
||||
/>,
|
||||
);
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
fireEvent.click(checkbox);
|
||||
expect(onSaveAnywayChange).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it("renders the Save anyway checkbox", () => {
|
||||
render(
|
||||
<ServiceTestPanel
|
||||
result={null}
|
||||
isPending={false}
|
||||
saveAnyway={false}
|
||||
onTest={noop}
|
||||
onSaveAnywayChange={noop}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByRole("checkbox")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user