mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
fix(settings): hold the channel test until changes are saved
The webhook, ntfy, email and Telegram test routes send through the saved channel config. After a field changed, the card kept showing the Saved note from the last save and Test still ran, so it reported on the old settings: a webhook switched to the Gotify format was tested with the generic body and Gotify refused it. The note now disappears on a change, and the test button stays off with a save-first hint while the form differs from what is saved. Refs #947
This commit is contained in:
1 parent
d4766a580b
commit
3bb08c3c8a
16 files changed
+268
-8
No files matched your search
@@ -0,0 +1,105 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
isEmailFormDirty,
|
||||
isNtfyFormDirty,
|
||||
isTelegramFormDirty,
|
||||
isWebhookFormDirty,
|
||||
} from "../channel-form-dirty";
|
||||
|
||||
// The Test button sends through the saved config, so each card has to know
|
||||
// when its form holds something the server does not (#947: a Gotify format
|
||||
// picked after the last save was tested as the old generic body).
|
||||
describe("channel form dirty state", () => {
|
||||
const webhookSaved = {
|
||||
url: "https://gotify.example.com/message",
|
||||
headerName: "X-Gotify-Key",
|
||||
format: "gotify",
|
||||
};
|
||||
const webhookForm = {
|
||||
url: "https://gotify.example.com/message",
|
||||
headerName: "X-Gotify-Key",
|
||||
headerValue: "",
|
||||
format: "gotify" as const,
|
||||
};
|
||||
|
||||
it("webhook: a form that matches the saved config is clean", () => {
|
||||
expect(isWebhookFormDirty(webhookForm, webhookSaved)).toBe(false);
|
||||
});
|
||||
|
||||
it("webhook: a changed format, URL, header name or a typed header value is a change", () => {
|
||||
expect(
|
||||
isWebhookFormDirty(webhookForm, { ...webhookSaved, format: undefined }),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isWebhookFormDirty(
|
||||
{ ...webhookForm, url: "https://gotify.example.com/message?token=x" },
|
||||
webhookSaved,
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isWebhookFormDirty({ ...webhookForm, headerName: "" }, webhookSaved),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isWebhookFormDirty({ ...webhookForm, headerValue: "tok" }, webhookSaved),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("webhook: a config saved before the format choice reads as generic", () => {
|
||||
expect(
|
||||
isWebhookFormDirty(
|
||||
{ ...webhookForm, format: "generic" },
|
||||
{ ...webhookSaved, format: undefined },
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("ntfy: server, topic and a typed auth token count", () => {
|
||||
const saved = { serverUrl: "https://ntfy.sh", topic: "t" };
|
||||
const form = { serverUrl: "https://ntfy.sh", topic: "t", authToken: "" };
|
||||
expect(isNtfyFormDirty(form, saved)).toBe(false);
|
||||
expect(isNtfyFormDirty({ ...form, topic: "u" }, saved)).toBe(true);
|
||||
expect(isNtfyFormDirty({ ...form, serverUrl: "https://x" }, saved)).toBe(
|
||||
true,
|
||||
);
|
||||
expect(isNtfyFormDirty({ ...form, authToken: "a" }, saved)).toBe(true);
|
||||
});
|
||||
|
||||
it("email: the recipient counts", () => {
|
||||
expect(
|
||||
isEmailFormDirty({ recipient: "a@x.io" }, { recipient: "a@x.io" }),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isEmailFormDirty({ recipient: "b@x.io" }, { recipient: "a@x.io" }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("telegram: a typed bot token or a changed chat id counts; whitespace alone does not", () => {
|
||||
expect(
|
||||
isTelegramFormDirty({ botToken: "", chatId: "1" }, { chatId: "1" }),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isTelegramFormDirty({ botToken: " ", chatId: "" }, { chatId: null }),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isTelegramFormDirty(
|
||||
{ botToken: "123:abc", chatId: "1" },
|
||||
{ chatId: "1" },
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isTelegramFormDirty({ botToken: "", chatId: "2" }, { chatId: "1" }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("nothing loaded yet is never a change", () => {
|
||||
expect(isWebhookFormDirty(webhookForm, undefined)).toBe(false);
|
||||
expect(
|
||||
isNtfyFormDirty({ serverUrl: "", topic: "", authToken: "x" }, undefined),
|
||||
).toBe(false);
|
||||
expect(isEmailFormDirty({ recipient: "x" }, undefined)).toBe(false);
|
||||
expect(isTelegramFormDirty({ botToken: "x", chatId: "" }, undefined)).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,7 @@ const mocks = vi.hoisted(() => ({
|
||||
onError?: (error: Error, enabled: boolean, previous: unknown) => void;
|
||||
}>,
|
||||
apiFetchRaw: vi.fn(),
|
||||
testButtonProps: [] as Array<{ endpoint: string; unsavedChanges?: boolean }>,
|
||||
queryClient: {
|
||||
cancelQueries: vi.fn(),
|
||||
getQueryData: vi.fn(),
|
||||
@@ -70,7 +71,13 @@ vi.mock("@/lib/api/api-fetch", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../test-connection-button", () => ({
|
||||
TestConnectionButton: () => null,
|
||||
TestConnectionButton: (props: {
|
||||
endpoint: string;
|
||||
unsavedChanges?: boolean;
|
||||
}) => {
|
||||
mocks.testButtonProps.push(props);
|
||||
return null;
|
||||
},
|
||||
}));
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
@@ -88,6 +95,7 @@ function render(card: ReactElement) {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.mutationOptions.length = 0;
|
||||
mocks.testButtonProps.length = 0;
|
||||
mocks.queryClient.cancelQueries.mockResolvedValue(undefined);
|
||||
mocks.apiFetchRaw.mockResolvedValue(
|
||||
new Response(JSON.stringify({ data: { saved: true }, error: null }), {
|
||||
@@ -97,6 +105,21 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
describe("notification channel cards", () => {
|
||||
it.each([
|
||||
["webhook", <WebhookCard key="webhook" isAuthenticated />],
|
||||
["ntfy", <NtfyCard key="ntfy" isAuthenticated />],
|
||||
["email", <EmailCard key="email" isAuthenticated />],
|
||||
["telegram", <TelegramCard key="telegram" isAuthenticated />],
|
||||
] as const)(
|
||||
"%s hands its unsaved-change state to the test button, clean once seeded from the server",
|
||||
(channel, card) => {
|
||||
render(card);
|
||||
const props = mocks.testButtonProps.at(-1);
|
||||
expect(props?.endpoint).toBe(`/api/settings/${channel}/test`);
|
||||
expect(props?.unsavedChanges).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["webhook", <WebhookCard key="webhook" isAuthenticated />, 1],
|
||||
["ntfy", <NtfyCard key="ntfy" isAuthenticated />, 1],
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@/lib/api/api-fetch", () => ({ apiFetchRaw: vi.fn() }));
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { TestConnectionButton } from "../test-connection-button";
|
||||
|
||||
function render(unsavedChanges?: boolean) {
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale="en">
|
||||
<TestConnectionButton
|
||||
endpoint="/api/settings/webhook/test"
|
||||
unsavedChanges={unsavedChanges}
|
||||
/>
|
||||
</I18nProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
// The test route sends through the saved config. With an unsaved change on
|
||||
// the card, a test would report on something other than what is on screen.
|
||||
describe("TestConnectionButton with unsaved changes", () => {
|
||||
it("locks the button and says to save first", () => {
|
||||
const html = render(true);
|
||||
expect(html).toMatch(/<button[^>]*disabled=""/);
|
||||
expect(html).toContain('data-slot="test-connection-save-first"');
|
||||
expect(html).toContain(
|
||||
"Save your changes first. Test uses the saved settings.",
|
||||
);
|
||||
});
|
||||
|
||||
it("stays usable and quiet when the form matches what is saved", () => {
|
||||
const html = render(false);
|
||||
expect(html).not.toMatch(/<button[^>]*disabled=""/);
|
||||
expect(html).not.toContain("test-connection-save-first");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user