fix: critical first-login soft-lock in usage survey overlay (#392)

* fix: prevent UsageSurveyOverlay from soft-locking the first-login password-change flow

Claude-Session: https://claude.ai/code/session_01KAC9Lbx8AmebAnj9WQZXHp

* fix: prevent double feedback submission when the settings write fails

Claude-Session: https://claude.ai/code/session_01KAC9Lbx8AmebAnj9WQZXHp

* refactor: consolidate feedback enums into packages/shared as a single source of truth

Claude-Session: https://claude.ai/code/session_01KAC9Lbx8AmebAnj9WQZXHp

* feat: add ARIA semantics, dismiss-button guard, and shared auth-route list to UsageSurveyOverlay

Claude-Session: https://claude.ai/code/session_01KAC9Lbx8AmebAnj9WQZXHp

* test: cover the submit-failure retry path and a persona-only minimal payload

Claude-Session: https://claude.ai/code/session_01KAC9Lbx8AmebAnj9WQZXHp
This commit is contained in:
SnapOtter
2026-07-02 18:37:12 +08:00
committed by GitHub
parent bd1838e40b
commit ca076f91fd
11 changed files with 442 additions and 190 deletions
@@ -166,6 +166,43 @@ describe("POST /api/v1/feedback", () => {
);
});
it("accepts a persona-only onboarding submission with no important areas selected", async () => {
process.env.ANALYTICS_BAKED_OVERRIDE = "on";
await refreshAnalyticsGate();
const token = await loginAsAdmin(testApp.app);
const res = await testApp.app.inject({
method: "POST",
url: "/api/v1/feedback",
headers: { authorization: `Bearer ${token}` },
payload: {
source: "onboarding",
surveyId: "onboarding-usage-v1",
promptVariant: "onboarding-overlay-v1",
usageType: "personal",
importantAreas: [],
},
});
expect(res.statusCode).toBe(200);
expect(JSON.parse(res.body)).toEqual({ ok: true, accepted: true });
expect(captureFeedback).toHaveBeenCalledWith(
expect.objectContaining({
source: "onboarding",
survey_id: "onboarding-usage-v1",
prompt_variant: "onboarding-overlay-v1",
usage_type: "personal",
}),
undefined,
);
// captureFeedback is mocked, so it receives the route's raw properties: the
// empty importantAreas array is forwarded as-is. Dropping an empty
// important_areas before it reaches PostHog happens inside the real,
// unmocked cleanFeedbackProperties (analytics.ts), which this test bypasses.
const lastCall = captureFeedback.mock.calls.at(-1);
expect(lastCall?.[0].important_areas).toEqual([]);
});
it("drops identifying contact fields when contact consent is not checked", async () => {
process.env.ANALYTICS_BAKED_OVERRIDE = "on";
await refreshAnalyticsGate();