251 lines
10 KiB
TypeScript
251 lines
10 KiB
TypeScript
import { describe, expect, it, beforeEach, afterEach, vi } from "vitest";
|
|
import {
|
|
createDefaultAccountSettings,
|
|
getAccountNotificationConsumption,
|
|
getAccountSettings,
|
|
getPostReleaseNotificationPreference,
|
|
getReleaseGateNotificationPreference,
|
|
shouldShowAccountNotification,
|
|
updateAccountSettings,
|
|
} from "@/api/accountSettings";
|
|
|
|
describe("account settings api", () => {
|
|
const storageSetItem = Storage.prototype.setItem;
|
|
const originalFetch = globalThis.fetch;
|
|
|
|
beforeEach(() => {
|
|
window.localStorage.clear();
|
|
Storage.prototype.setItem = storageSetItem;
|
|
globalThis.fetch = originalFetch;
|
|
});
|
|
|
|
afterEach(() => {
|
|
Storage.prototype.setItem = storageSetItem;
|
|
globalThis.fetch = originalFetch;
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it("creates defaults from the current account seed", async () => {
|
|
const settings = await getAccountSettings({
|
|
displayName: "陈雨",
|
|
roleTitle: "独立开发者",
|
|
email: "yu@example.com",
|
|
});
|
|
|
|
expect(settings.profile.displayName).toBe("陈雨");
|
|
expect(settings.profile.roleTitle).toBe("独立开发者");
|
|
expect(settings.profile.email).toBe("yu@example.com");
|
|
expect(settings.saveScope).toBe("local");
|
|
expect(settings.aiPreferences.answerDepth).toBe("balanced");
|
|
expect(settings.notifications.pendingConfirmation.enabled).toBe(true);
|
|
expect(settings.notifications.executionWriteback.enabled).toBe(true);
|
|
expect(settings.notifications.releaseGate.enabled).toBe(true);
|
|
expect(settings.notifications.failureAlert.enabled).toBe(true);
|
|
});
|
|
|
|
it("persists updated preferences through the api layer", async () => {
|
|
const defaults = createDefaultAccountSettings({ displayName: "陈雨" });
|
|
const saved = await updateAccountSettings({
|
|
...defaults,
|
|
profile: { ...defaults.profile, defaultLanguage: "en-US" },
|
|
aiPreferences: { answerDepth: "deep", evidenceDisplay: "full" },
|
|
notifications: {
|
|
...defaults.notifications,
|
|
failureAlert: { enabled: false, desktop: false, emailDigest: false },
|
|
},
|
|
});
|
|
const restored = await getAccountSettings({ displayName: "陈雨" });
|
|
|
|
expect(saved.updatedAt).toBeTruthy();
|
|
expect(saved.saveScope).toBe("local");
|
|
expect(restored.profile.defaultLanguage).toBe("en-US");
|
|
expect(restored.aiPreferences.answerDepth).toBe("deep");
|
|
expect(restored.aiPreferences.evidenceDisplay).toBe("full");
|
|
expect(restored.notifications.failureAlert.enabled).toBe(false);
|
|
});
|
|
|
|
it("exposes notification preferences as product surface consumption rules", () => {
|
|
const defaults = createDefaultAccountSettings();
|
|
const settings = {
|
|
...defaults,
|
|
notifications: {
|
|
...defaults.notifications,
|
|
executionWriteback: { enabled: false, desktop: false, emailDigest: false },
|
|
releaseGate: { enabled: true, desktop: false, emailDigest: true },
|
|
},
|
|
};
|
|
|
|
const consumption = getAccountNotificationConsumption(settings);
|
|
const execution = consumption.find((item) => item.key === "executionWriteback");
|
|
const release = consumption.find((item) => item.key === "releaseGate");
|
|
|
|
expect(shouldShowAccountNotification(settings, "pendingConfirmation")).toBe(true);
|
|
expect(shouldShowAccountNotification(settings, "executionWriteback")).toBe(false);
|
|
expect(execution?.productArea).toBe("Execution Bridge");
|
|
expect(execution?.showInAppReminder).toBe(false);
|
|
expect(execution?.statusLabel).toBe("页面内提醒静默");
|
|
expect(release?.productArea).toBe("Release / Growth");
|
|
expect(release?.showInAppReminder).toBe(true);
|
|
expect(release?.emailDigestPreference).toBe(true);
|
|
});
|
|
|
|
it("maps release gate and release validation events to the releaseGate preference", () => {
|
|
const defaults = createDefaultAccountSettings();
|
|
const settings = {
|
|
...defaults,
|
|
notifications: {
|
|
...defaults.notifications,
|
|
releaseGate: { enabled: true, desktop: false, emailDigest: true },
|
|
},
|
|
};
|
|
|
|
const blockedGate = getReleaseGateNotificationPreference(settings, "release_gate_blocked");
|
|
const validation = getReleaseGateNotificationPreference(settings, "release_validation_needs_confirmation");
|
|
|
|
expect(blockedGate.notificationKey).toBe("releaseGate");
|
|
expect(blockedGate.surface).toBe("release_growth");
|
|
expect(blockedGate.inAppEnabled).toBe(true);
|
|
expect(blockedGate.desktopPreference).toBe(false);
|
|
expect(blockedGate.emailDigestPreference).toBe(true);
|
|
expect(blockedGate.reason).toContain("发布门禁");
|
|
expect(validation.notificationKey).toBe("releaseGate");
|
|
expect(validation.inAppEnabled).toBe(true);
|
|
expect(validation.reason).toContain("发布验证");
|
|
});
|
|
|
|
it("maps release failed events to failureAlert without enabling disabled channels", () => {
|
|
const defaults = createDefaultAccountSettings();
|
|
const settings = {
|
|
...defaults,
|
|
notifications: {
|
|
...defaults.notifications,
|
|
failureAlert: { enabled: false, desktop: true, emailDigest: true },
|
|
},
|
|
};
|
|
|
|
const failedEvent = getReleaseGateNotificationPreference(settings, "failed_event");
|
|
|
|
expect(failedEvent.notificationKey).toBe("failureAlert");
|
|
expect(failedEvent.surface).toBe("system_failure");
|
|
expect(failedEvent.inAppEnabled).toBe(false);
|
|
expect(failedEvent.desktopPreference).toBe(false);
|
|
expect(failedEvent.emailDigestPreference).toBe(false);
|
|
expect(failedEvent.reason).toContain("failed event");
|
|
});
|
|
|
|
it("maps post-release feedback, revenue, compliance, and iteration events to local reminder preferences", () => {
|
|
const defaults = createDefaultAccountSettings();
|
|
const settings = {
|
|
...defaults,
|
|
notifications: {
|
|
...defaults.notifications,
|
|
releaseGate: { enabled: true, desktop: false, emailDigest: true },
|
|
pendingConfirmation: { enabled: false, desktop: true, emailDigest: true },
|
|
},
|
|
};
|
|
|
|
const feedback = getPostReleaseNotificationPreference(settings, "feedback_received");
|
|
const revenue = getPostReleaseNotificationPreference(settings, "revenue_signal");
|
|
const compliance = getPostReleaseNotificationPreference(settings, "compliance_blocked");
|
|
const iteration = getPostReleaseNotificationPreference(settings, "iteration_recommendation");
|
|
|
|
expect(feedback.notificationKey).toBe("releaseGate");
|
|
expect(feedback.surface).toBe("release_growth");
|
|
expect(feedback.spaceReadable).toBe(true);
|
|
expect(feedback.showInAppReminder).toBe(true);
|
|
expect(feedback.desktopPreference).toBe(false);
|
|
expect(feedback.emailDigestPreference).toBe(true);
|
|
expect(feedback.reminderPolicy).toBe("page_reminder_only");
|
|
expect(feedback.evidencePolicy).toBe("preserve_business_evidence");
|
|
expect(feedback.mutedDetail).toContain("不代表系统级推送已接入");
|
|
expect(revenue.notificationKey).toBe("releaseGate");
|
|
expect(revenue.reason).toContain("收入");
|
|
expect(compliance.notificationKey).toBe("releaseGate");
|
|
expect(compliance.reason).toContain("合规");
|
|
expect(iteration.notificationKey).toBe("pendingConfirmation");
|
|
expect(iteration.showInAppReminder).toBe(false);
|
|
expect(iteration.desktopPreference).toBe(false);
|
|
expect(iteration.emailDigestPreference).toBe(false);
|
|
expect(iteration.mutedDetail).toContain("证据仍必须");
|
|
});
|
|
|
|
it("falls back missing notification fields to safe defaults", async () => {
|
|
window.localStorage.setItem(
|
|
"pmflow:account-settings:v1",
|
|
JSON.stringify({
|
|
notifications: {
|
|
failureAlert: { enabled: false },
|
|
},
|
|
})
|
|
);
|
|
|
|
const restored = await getAccountSettings();
|
|
const consumption = getAccountNotificationConsumption(restored);
|
|
|
|
expect(restored.notifications.pendingConfirmation.desktop).toBe(true);
|
|
expect(restored.notifications.failureAlert.enabled).toBe(false);
|
|
expect(restored.notifications.failureAlert.desktop).toBe(true);
|
|
expect(shouldShowAccountNotification(restored, "failureAlert")).toBe(false);
|
|
expect(consumption.find((item) => item.key === "failureAlert")?.detail).toContain("仍保留");
|
|
});
|
|
|
|
it("fails explicitly when local persistence is unavailable", async () => {
|
|
vi.spyOn(Storage.prototype, "setItem").mockImplementation(() => {
|
|
throw new Error("storage denied");
|
|
});
|
|
|
|
await expect(updateAccountSettings(createDefaultAccountSettings())).rejects.toThrow("storage denied");
|
|
});
|
|
|
|
it("uses account sync when an authenticated backend contract is available", async () => {
|
|
window.localStorage.setItem("pmflow_token", "token");
|
|
const accountSaved = {
|
|
...createDefaultAccountSettings({ displayName: "陈雨", email: "yu@example.com" }),
|
|
saveScope: "account" as const,
|
|
aiPreferences: { answerDepth: "deep" as const, evidenceDisplay: "full" as const },
|
|
updatedAt: "2026-04-26T10:00:00",
|
|
};
|
|
const fetchMock = vi.fn().mockResolvedValue(
|
|
new Response(JSON.stringify({ code: 0, message: "success", data: accountSaved }), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
})
|
|
);
|
|
globalThis.fetch = fetchMock;
|
|
|
|
const saved = await updateAccountSettings(accountSaved);
|
|
|
|
expect(saved.saveScope).toBe("account");
|
|
expect(saved.aiPreferences.answerDepth).toBe("deep");
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
"/api/account-settings/me",
|
|
expect.objectContaining({
|
|
method: "PUT",
|
|
})
|
|
);
|
|
});
|
|
|
|
it("falls back to honest local persistence when backend sync fails", async () => {
|
|
window.localStorage.setItem("pmflow_token", "token");
|
|
vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
globalThis.fetch = vi.fn().mockResolvedValue(
|
|
new Response(JSON.stringify({ code: 500, message: "backend unavailable", data: null }), {
|
|
status: 500,
|
|
headers: { "Content-Type": "application/json" },
|
|
})
|
|
);
|
|
const defaults = createDefaultAccountSettings({ displayName: "陈雨" });
|
|
|
|
const saved = await updateAccountSettings({
|
|
...defaults,
|
|
aiPreferences: { answerDepth: "deep", evidenceDisplay: "inline" },
|
|
});
|
|
|
|
expect(saved.saveScope).toBe("local");
|
|
expect(saved.aiPreferences.answerDepth).toBe("deep");
|
|
const restored = await getAccountSettings({ displayName: "陈雨" });
|
|
expect(restored.saveScope).toBe("local");
|
|
expect(restored.aiPreferences.answerDepth).toBe("deep");
|
|
});
|
|
});
|