mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat: add usage onboarding survey overlay (#388)
* feat: add usage-survey feedback types and gating function Claude-Session: https://claude.ai/code/session_01KAC9Lbx8AmebAnj9WQZXHp * feat: add onboarding usage-survey i18n strings to all locales Relabels three ambiguous feedback.usageTypes values (personal/team_internal/ business_workflow) and adds a new onboarding namespace (4 keys) across the reference locale and all 20 translations, so the tree compiles at every commit instead of only after both locale groups land. Claude-Session: https://claude.ai/code/session_01KAC9Lbx8AmebAnj9WQZXHp * feat: add UsageSurveyOverlay component * feat: mount UsageSurveyOverlay inside AuthGuard Claude-Session: https://claude.ai/code/session_01KAC9Lbx8AmebAnj9WQZXHp * fix: use text-start instead of text-left for RTL support in UsageSurveyOverlay * refactor: drop redundant usage-type field from the admin feedback dialog Claude-Session: https://claude.ai/code/session_01KAC9Lbx8AmebAnj9WQZXHp * feat: accept onboarding source and survey id in the feedback route Claude-Session: https://claude.ai/code/session_01KAC9Lbx8AmebAnj9WQZXHp * test: cover the onboarding source in the feedback route integration test Claude-Session: https://claude.ai/code/session_01KAC9Lbx8AmebAnj9WQZXHp * chore: remove orphaned usageTypeLabel i18n key * refactor: derive feedback source/survey_id enums from a single source of truth * perf: skip the settings fetch in UsageSurveyOverlay for non-admin users
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const submitFeedback = vi.hoisted(() => vi.fn().mockResolvedValue({ ok: true, accepted: true }));
|
||||
const apiGet = vi.hoisted(() => vi.fn());
|
||||
const apiPut = vi.hoisted(() => vi.fn().mockResolvedValue({}));
|
||||
const useAuth = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("@/lib/feedback", async (importOriginal) => {
|
||||
const actual: Record<string, unknown> = await importOriginal();
|
||||
return { ...actual, submitFeedback };
|
||||
});
|
||||
|
||||
vi.mock("@/lib/api", async (importOriginal) => {
|
||||
const actual: Record<string, unknown> = await importOriginal();
|
||||
return { ...actual, apiGet, apiPut };
|
||||
});
|
||||
|
||||
vi.mock("@/hooks/use-auth", () => ({ useAuth }));
|
||||
|
||||
vi.mock("@/stores/analytics-store", () => ({
|
||||
useAnalyticsStore: (
|
||||
selector: (state: { config: { enabled: boolean }; configLoaded: boolean }) => unknown,
|
||||
) => selector({ config: { enabled: true }, configLoaded: true }),
|
||||
}));
|
||||
|
||||
import { UsageSurveyOverlay } from "@/components/onboarding/usage-survey-overlay";
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
submitFeedback.mockClear();
|
||||
apiGet.mockClear();
|
||||
apiPut.mockClear();
|
||||
useAuth.mockReset();
|
||||
});
|
||||
|
||||
describe("UsageSurveyOverlay", () => {
|
||||
it("renders nothing for a non-admin", () => {
|
||||
useAuth.mockReturnValue({ role: "user" });
|
||||
|
||||
render(<UsageSurveyOverlay />);
|
||||
|
||||
expect(apiGet).not.toHaveBeenCalled();
|
||||
expect(screen.queryByText("How are you using SnapOtter?")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders nothing once already answered or dismissed", async () => {
|
||||
useAuth.mockReturnValue({ role: "admin" });
|
||||
apiGet.mockResolvedValue({
|
||||
settings: { "onboarding.usageSurvey.dismissedAt": "2026-01-01T00:00:00Z" },
|
||||
});
|
||||
|
||||
render(<UsageSurveyOverlay />);
|
||||
|
||||
await waitFor(() => expect(apiGet).toHaveBeenCalled());
|
||||
expect(screen.queryByText("How are you using SnapOtter?")).toBeNull();
|
||||
});
|
||||
|
||||
it("shows both questions for an admin instance that hasn't answered", async () => {
|
||||
useAuth.mockReturnValue({ role: "admin" });
|
||||
apiGet.mockResolvedValue({ settings: {} });
|
||||
|
||||
render(<UsageSurveyOverlay />);
|
||||
|
||||
expect(await screen.findByText("How are you using SnapOtter?")).toBeDefined();
|
||||
expect(screen.getByText("What matters most to you?")).toBeDefined();
|
||||
expect(screen.getByRole("button", { name: /Just me/ })).toBeDefined();
|
||||
expect(screen.getByRole("button", { name: "Continue" })).toBeDisabled();
|
||||
});
|
||||
|
||||
it("submits the selected answers and records the settings key", async () => {
|
||||
useAuth.mockReturnValue({ role: "admin" });
|
||||
apiGet.mockResolvedValue({ settings: {} });
|
||||
|
||||
render(<UsageSurveyOverlay />);
|
||||
await screen.findByText("How are you using SnapOtter?");
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /Small team/ }));
|
||||
fireEvent.click(screen.getByRole("button", { name: /Images/ }));
|
||||
fireEvent.click(screen.getByRole("button", { name: /PDF\/docs/ }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Continue" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(submitFeedback).toHaveBeenCalledWith({
|
||||
source: "onboarding",
|
||||
surveyId: "onboarding-usage-v1",
|
||||
promptVariant: "onboarding-overlay-v1",
|
||||
usageType: "team_internal",
|
||||
importantAreas: ["images", "pdf_docs"],
|
||||
});
|
||||
});
|
||||
expect(apiPut).toHaveBeenCalledWith("/v1/settings", {
|
||||
"onboarding.usageSurvey.answeredAt": expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it("dismissing writes the dismiss key without submitting feedback", async () => {
|
||||
useAuth.mockReturnValue({ role: "admin" });
|
||||
apiGet.mockResolvedValue({ settings: {} });
|
||||
|
||||
render(<UsageSurveyOverlay />);
|
||||
await screen.findByText("How are you using SnapOtter?");
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Don't ask again" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(apiPut).toHaveBeenCalledWith("/v1/settings", {
|
||||
"onboarding.usageSurvey.dismissedAt": expect.any(String),
|
||||
});
|
||||
});
|
||||
expect(submitFeedback).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user