2026-07-02 00:02:24 +08:00
|
|
|
// @vitest-environment jsdom
|
|
|
|
|
|
|
|
|
|
import "@testing-library/jest-dom/vitest";
|
2026-07-25 19:50:53 +08:00
|
|
|
import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
2026-07-27 15:37:30 +08:00
|
|
|
import { MemoryRouter } from "react-router";
|
2026-07-02 00:02:24 +08:00
|
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
|
|
|
|
|
|
|
const submitFeedback = vi.hoisted(() => vi.fn().mockResolvedValue({ ok: true, accepted: true }));
|
2026-07-22 00:31:30 +08:00
|
|
|
const trackFeedbackPromptShown = vi.hoisted(() => vi.fn());
|
|
|
|
|
const trackFeedbackPromptDismissed = vi.hoisted(() => vi.fn());
|
2026-07-02 00:02:24 +08:00
|
|
|
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();
|
2026-07-22 00:31:30 +08:00
|
|
|
return { ...actual, submitFeedback, trackFeedbackPromptShown, trackFeedbackPromptDismissed };
|
2026-07-02 00:02:24 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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";
|
|
|
|
|
|
2026-07-22 00:31:30 +08:00
|
|
|
// The worker writes this marker on the instance's first successful processing.
|
|
|
|
|
// The survey is only eligible once it exists; without it the overlay stays hidden.
|
|
|
|
|
const PROCESSED = { "onboarding.firstProcessedAt": "2026-01-01T00:00:00Z" };
|
|
|
|
|
|
2026-07-02 00:02:24 +08:00
|
|
|
afterEach(() => {
|
|
|
|
|
cleanup();
|
|
|
|
|
submitFeedback.mockClear();
|
2026-07-22 00:31:30 +08:00
|
|
|
trackFeedbackPromptShown.mockClear();
|
|
|
|
|
trackFeedbackPromptDismissed.mockClear();
|
2026-07-02 00:02:24 +08:00
|
|
|
apiGet.mockClear();
|
|
|
|
|
apiPut.mockClear();
|
|
|
|
|
useAuth.mockReset();
|
|
|
|
|
});
|
|
|
|
|
|
2026-07-02 18:37:12 +08:00
|
|
|
function renderOverlay(initialPath = "/") {
|
|
|
|
|
return render(
|
|
|
|
|
<MemoryRouter initialEntries={[initialPath]}>
|
|
|
|
|
<UsageSurveyOverlay />
|
|
|
|
|
</MemoryRouter>,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-02 00:02:24 +08:00
|
|
|
describe("UsageSurveyOverlay", () => {
|
|
|
|
|
it("renders nothing for a non-admin", () => {
|
2026-07-02 18:37:12 +08:00
|
|
|
useAuth.mockReturnValue({ role: "user", mustChangePassword: false });
|
2026-07-02 00:02:24 +08:00
|
|
|
|
2026-07-02 18:37:12 +08:00
|
|
|
renderOverlay();
|
2026-07-02 00:02:24 +08:00
|
|
|
|
|
|
|
|
expect(apiGet).not.toHaveBeenCalled();
|
|
|
|
|
expect(screen.queryByText("How are you using SnapOtter?")).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
2026-07-22 00:31:30 +08:00
|
|
|
it("stays hidden until the instance's first processing has completed", async () => {
|
|
|
|
|
useAuth.mockReturnValue({ role: "admin", mustChangePassword: false });
|
|
|
|
|
apiGet.mockResolvedValue({ settings: {} });
|
|
|
|
|
|
|
|
|
|
renderOverlay();
|
|
|
|
|
|
|
|
|
|
await waitFor(() => expect(apiGet).toHaveBeenCalled());
|
|
|
|
|
expect(screen.queryByText("How are you using SnapOtter?")).toBeNull();
|
|
|
|
|
expect(trackFeedbackPromptShown).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
2026-07-02 00:02:24 +08:00
|
|
|
it("renders nothing once already answered or dismissed", async () => {
|
2026-07-02 18:37:12 +08:00
|
|
|
useAuth.mockReturnValue({ role: "admin", mustChangePassword: false });
|
2026-07-02 00:02:24 +08:00
|
|
|
apiGet.mockResolvedValue({
|
2026-07-22 00:31:30 +08:00
|
|
|
settings: { ...PROCESSED, "onboarding.usageSurvey.dismissedAt": "2026-01-01T00:00:00Z" },
|
2026-07-02 00:02:24 +08:00
|
|
|
});
|
|
|
|
|
|
2026-07-02 18:37:12 +08:00
|
|
|
renderOverlay();
|
2026-07-02 00:02:24 +08:00
|
|
|
|
|
|
|
|
await waitFor(() => expect(apiGet).toHaveBeenCalled());
|
|
|
|
|
expect(screen.queryByText("How are you using SnapOtter?")).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
2026-07-22 00:31:30 +08:00
|
|
|
it("shows the telemetry-blind questions after processing and emits a shown event", async () => {
|
2026-07-02 18:37:12 +08:00
|
|
|
useAuth.mockReturnValue({ role: "admin", mustChangePassword: false });
|
2026-07-22 00:31:30 +08:00
|
|
|
apiGet.mockResolvedValue({ settings: PROCESSED });
|
2026-07-02 00:02:24 +08:00
|
|
|
|
2026-07-02 18:37:12 +08:00
|
|
|
renderOverlay();
|
2026-07-02 00:02:24 +08:00
|
|
|
|
|
|
|
|
expect(await screen.findByText("How are you using SnapOtter?")).toBeDefined();
|
2026-07-02 18:37:12 +08:00
|
|
|
expect(screen.getByRole("radio", { name: /Just me/ })).toBeDefined();
|
2026-07-02 00:02:24 +08:00
|
|
|
expect(screen.getByRole("button", { name: "Continue" })).toBeDisabled();
|
2026-07-25 18:45:38 +08:00
|
|
|
// Opening ask is one question. The optional three stay collapsed until the
|
|
|
|
|
// required one is answered, so the first impression is not a wall of 20.
|
|
|
|
|
expect(screen.queryByText("What were you using before?")).toBeNull();
|
|
|
|
|
expect(screen.queryByText(/Why self-host it/)).toBeNull();
|
2026-07-22 00:31:30 +08:00
|
|
|
await waitFor(() => expect(trackFeedbackPromptShown).toHaveBeenCalledWith("onboarding"));
|
2026-07-02 00:02:24 +08:00
|
|
|
});
|
|
|
|
|
|
2026-07-22 00:31:30 +08:00
|
|
|
it("submits only the usage type when nothing else is picked", async () => {
|
2026-07-02 18:37:12 +08:00
|
|
|
useAuth.mockReturnValue({ role: "admin", mustChangePassword: false });
|
2026-07-22 00:31:30 +08:00
|
|
|
apiGet.mockResolvedValue({ settings: PROCESSED });
|
2026-07-02 00:02:24 +08:00
|
|
|
|
2026-07-02 18:37:12 +08:00
|
|
|
renderOverlay();
|
2026-07-02 00:02:24 +08:00
|
|
|
await screen.findByText("How are you using SnapOtter?");
|
|
|
|
|
|
2026-07-02 18:37:12 +08:00
|
|
|
fireEvent.click(screen.getByRole("radio", { name: /Small team/ }));
|
2026-07-02 00:02:24 +08:00
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Continue" }));
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(submitFeedback).toHaveBeenCalledWith({
|
|
|
|
|
source: "onboarding",
|
|
|
|
|
surveyId: "onboarding-usage-v1",
|
2026-07-25 18:45:38 +08:00
|
|
|
promptVariant: "onboarding-card-v1",
|
2026-07-02 00:02:24 +08:00
|
|
|
usageType: "team_internal",
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
expect(apiPut).toHaveBeenCalledWith("/v1/settings", {
|
|
|
|
|
"onboarding.usageSurvey.answeredAt": expect.any(String),
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-07-22 00:31:30 +08:00
|
|
|
it("includes prior tool, motivation, and discovery source when the admin selects them", async () => {
|
2026-07-11 16:46:28 +08:00
|
|
|
useAuth.mockReturnValue({ role: "admin", mustChangePassword: false });
|
2026-07-22 00:31:30 +08:00
|
|
|
apiGet.mockResolvedValue({ settings: PROCESSED });
|
2026-07-11 16:46:28 +08:00
|
|
|
|
|
|
|
|
renderOverlay();
|
|
|
|
|
await screen.findByText("How are you using SnapOtter?");
|
|
|
|
|
|
|
|
|
|
fireEvent.click(screen.getByRole("radio", { name: /Just me/ }));
|
2026-07-25 18:45:38 +08:00
|
|
|
// Answering the required question reveals the optional follow-ups.
|
|
|
|
|
expect(await screen.findByText("What were you using before?")).toBeDefined();
|
2026-07-22 00:31:30 +08:00
|
|
|
fireEvent.click(screen.getByRole("radio", { name: /Command line/ }));
|
|
|
|
|
fireEvent.click(screen.getByRole("radio", { name: /Privacy and data control/ }));
|
|
|
|
|
fireEvent.change(screen.getByLabelText(/How did you hear about us/), {
|
|
|
|
|
target: { value: "github" },
|
2026-07-11 16:46:28 +08:00
|
|
|
});
|
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Continue" }));
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(submitFeedback).toHaveBeenCalledWith({
|
|
|
|
|
source: "onboarding",
|
|
|
|
|
surveyId: "onboarding-usage-v1",
|
2026-07-25 18:45:38 +08:00
|
|
|
promptVariant: "onboarding-card-v1",
|
2026-07-11 16:46:28 +08:00
|
|
|
usageType: "personal",
|
2026-07-22 00:31:30 +08:00
|
|
|
priorTool: "command_line",
|
|
|
|
|
selfHostMotivation: "privacy_control",
|
|
|
|
|
discoverySource: "github",
|
2026-07-11 16:46:28 +08:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-07-22 00:31:30 +08:00
|
|
|
it("dismissing writes the dismiss key, emits a dismissed event, and does not submit", async () => {
|
2026-07-02 18:37:12 +08:00
|
|
|
useAuth.mockReturnValue({ role: "admin", mustChangePassword: false });
|
2026-07-22 00:31:30 +08:00
|
|
|
apiGet.mockResolvedValue({ settings: PROCESSED });
|
2026-07-02 18:37:12 +08:00
|
|
|
|
|
|
|
|
renderOverlay();
|
|
|
|
|
await screen.findByText("How are you using SnapOtter?");
|
|
|
|
|
|
2026-07-22 00:31:30 +08:00
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Don't ask again" }));
|
2026-07-02 18:37:12 +08:00
|
|
|
|
2026-07-22 00:31:30 +08:00
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(apiPut).toHaveBeenCalledWith("/v1/settings", {
|
|
|
|
|
"onboarding.usageSurvey.dismissedAt": expect.any(String),
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
expect(trackFeedbackPromptDismissed).toHaveBeenCalledWith("onboarding", "dont_ask_again");
|
|
|
|
|
expect(submitFeedback).not.toHaveBeenCalled();
|
2026-07-02 18:37:12 +08:00
|
|
|
});
|
|
|
|
|
|
2026-07-22 00:31:30 +08:00
|
|
|
it("does not resubmit feedback if only the settings write failed on the first attempt", async () => {
|
2026-07-02 18:37:12 +08:00
|
|
|
useAuth.mockReturnValue({ role: "admin", mustChangePassword: false });
|
2026-07-22 00:31:30 +08:00
|
|
|
apiGet.mockResolvedValue({ settings: PROCESSED });
|
2026-07-02 18:37:12 +08:00
|
|
|
apiPut.mockRejectedValueOnce(new Error("network error")).mockResolvedValueOnce({});
|
|
|
|
|
|
|
|
|
|
renderOverlay();
|
|
|
|
|
await screen.findByText("How are you using SnapOtter?");
|
|
|
|
|
|
|
|
|
|
fireEvent.click(screen.getByRole("radio", { name: /Just me/ }));
|
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Continue" }));
|
|
|
|
|
|
2026-07-22 00:31:30 +08:00
|
|
|
await waitFor(() => expect(apiPut).toHaveBeenCalledTimes(1));
|
|
|
|
|
expect(submitFeedback).toHaveBeenCalledTimes(1);
|
|
|
|
|
|
2026-07-02 18:37:12 +08:00
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Continue" }));
|
|
|
|
|
|
|
|
|
|
await waitFor(() => expect(apiPut).toHaveBeenCalledTimes(2));
|
2026-07-22 00:31:30 +08:00
|
|
|
expect(submitFeedback).toHaveBeenCalledTimes(1);
|
2026-07-02 18:37:12 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("stays visible and re-enables Continue if the feedback submission itself fails", async () => {
|
|
|
|
|
useAuth.mockReturnValue({ role: "admin", mustChangePassword: false });
|
2026-07-22 00:31:30 +08:00
|
|
|
apiGet.mockResolvedValue({ settings: PROCESSED });
|
2026-07-02 18:37:12 +08:00
|
|
|
submitFeedback.mockRejectedValueOnce(new Error("network error"));
|
|
|
|
|
|
|
|
|
|
renderOverlay();
|
|
|
|
|
await screen.findByText("How are you using SnapOtter?");
|
|
|
|
|
|
|
|
|
|
fireEvent.click(screen.getByRole("radio", { name: /Just me/ }));
|
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Continue" }));
|
|
|
|
|
|
|
|
|
|
await waitFor(() => expect(submitFeedback).toHaveBeenCalledTimes(1));
|
|
|
|
|
await waitFor(() =>
|
|
|
|
|
expect(screen.getByRole("button", { name: "Continue" })).not.toBeDisabled(),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(screen.getByText("How are you using SnapOtter?")).toBeDefined();
|
|
|
|
|
expect(apiPut).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("ignores a second dismiss click while the first write is in flight", async () => {
|
|
|
|
|
useAuth.mockReturnValue({ role: "admin", mustChangePassword: false });
|
2026-07-22 00:31:30 +08:00
|
|
|
apiGet.mockResolvedValue({ settings: PROCESSED });
|
2026-07-02 18:37:12 +08:00
|
|
|
let resolveApiPut: (() => void) | undefined;
|
|
|
|
|
apiPut.mockImplementationOnce(
|
|
|
|
|
() =>
|
|
|
|
|
new Promise((resolve) => {
|
|
|
|
|
resolveApiPut = () => resolve({});
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
renderOverlay();
|
|
|
|
|
await screen.findByText("How are you using SnapOtter?");
|
|
|
|
|
|
|
|
|
|
const dismissButton = screen.getByRole("button", { name: "Don't ask again" });
|
|
|
|
|
fireEvent.click(dismissButton);
|
|
|
|
|
fireEvent.click(dismissButton);
|
|
|
|
|
fireEvent.click(dismissButton);
|
|
|
|
|
|
|
|
|
|
resolveApiPut?.();
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(apiPut).toHaveBeenCalledWith("/v1/settings", {
|
|
|
|
|
"onboarding.usageSurvey.dismissedAt": expect.any(String),
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
expect(apiPut).toHaveBeenCalledTimes(1);
|
|
|
|
|
});
|
|
|
|
|
|
2026-07-25 18:45:38 +08:00
|
|
|
// The prompt used to be an opaque full-screen takeover with aria-modal, a
|
|
|
|
|
// focus trap, and no Escape handler, so the only exits were answering the
|
|
|
|
|
// required question or finding a text-xs grey link. These pin the friction fix.
|
|
|
|
|
describe("does not block the app", () => {
|
|
|
|
|
it("is not a modal and does not cover the screen", async () => {
|
|
|
|
|
useAuth.mockReturnValue({ role: "admin", mustChangePassword: false });
|
|
|
|
|
apiGet.mockResolvedValue({ settings: PROCESSED });
|
|
|
|
|
|
|
|
|
|
renderOverlay();
|
|
|
|
|
await screen.findByText("How are you using SnapOtter?");
|
|
|
|
|
|
|
|
|
|
const dialog = screen.getByRole("dialog");
|
|
|
|
|
expect(dialog).not.toHaveAttribute("aria-modal", "true");
|
|
|
|
|
expect(dialog.className).not.toContain("inset-0");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("closes on Escape and records the dismissal", async () => {
|
|
|
|
|
useAuth.mockReturnValue({ role: "admin", mustChangePassword: false });
|
|
|
|
|
apiGet.mockResolvedValue({ settings: PROCESSED });
|
|
|
|
|
|
|
|
|
|
renderOverlay();
|
|
|
|
|
await screen.findByText("How are you using SnapOtter?");
|
2026-07-25 19:50:53 +08:00
|
|
|
await act(async () => {});
|
2026-07-25 18:45:38 +08:00
|
|
|
|
2026-07-25 19:50:53 +08:00
|
|
|
// Escape is the one control here whose handler is a document listener
|
|
|
|
|
// registered by a passive effect; every other control is an onClick prop
|
|
|
|
|
// attached at commit. That makes it the only one that can miss an event
|
|
|
|
|
// dispatched too early, and it flaked exactly once on main: dialog fully
|
|
|
|
|
// rendered, handler never ran. Locally the listener is always attached by
|
|
|
|
|
// the time findByText resolves, so the window only opens under CI
|
|
|
|
|
// contention and cannot be reproduced here. Rather than bet on one
|
|
|
|
|
// mechanism, retry the dispatch until it lands. handleDismiss guards on
|
|
|
|
|
// `busy` and the settings write is idempotent, so repeats are harmless.
|
2026-07-25 18:45:38 +08:00
|
|
|
await waitFor(() => {
|
2026-07-25 19:50:53 +08:00
|
|
|
fireEvent.keyDown(document, { key: "Escape" });
|
2026-07-25 18:45:38 +08:00
|
|
|
expect(apiPut).toHaveBeenCalledWith("/v1/settings", {
|
|
|
|
|
"onboarding.usageSurvey.dismissedAt": expect.any(String),
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
expect(trackFeedbackPromptDismissed).toHaveBeenCalledWith("onboarding", "close");
|
|
|
|
|
expect(submitFeedback).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("offers a visible close control, not just a faint text link", async () => {
|
|
|
|
|
useAuth.mockReturnValue({ role: "admin", mustChangePassword: false });
|
|
|
|
|
apiGet.mockResolvedValue({ settings: PROCESSED });
|
|
|
|
|
|
|
|
|
|
renderOverlay();
|
|
|
|
|
await screen.findByText("How are you using SnapOtter?");
|
|
|
|
|
|
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Close feedback dialog" }));
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(apiPut).toHaveBeenCalledWith("/v1/settings", {
|
|
|
|
|
"onboarding.usageSurvey.dismissedAt": expect.any(String),
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
expect(trackFeedbackPromptDismissed).toHaveBeenCalledWith("onboarding", "close");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// RouteAnnouncer focuses and announces document.querySelector("h1") on every
|
|
|
|
|
// route change. While this prompt's title was an h1 it stole focus the moment
|
|
|
|
|
// the card appeared and made every later navigation announce the survey
|
|
|
|
|
// instead of the page the user opened.
|
|
|
|
|
it("does not claim the page's h1", async () => {
|
|
|
|
|
useAuth.mockReturnValue({ role: "admin", mustChangePassword: false });
|
|
|
|
|
apiGet.mockResolvedValue({ settings: PROCESSED });
|
|
|
|
|
|
|
|
|
|
renderOverlay();
|
|
|
|
|
const title = await screen.findByText("How are you using SnapOtter?");
|
|
|
|
|
|
|
|
|
|
expect(title.tagName).not.toBe("H1");
|
|
|
|
|
expect(screen.queryByRole("heading", { level: 1 })).toBeNull();
|
|
|
|
|
// Still the dialog's accessible name.
|
|
|
|
|
expect(screen.getByRole("dialog")).toHaveAttribute("aria-labelledby", title.id);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("can be sent after a single click, without touching the optional questions", async () => {
|
|
|
|
|
useAuth.mockReturnValue({ role: "admin", mustChangePassword: false });
|
|
|
|
|
apiGet.mockResolvedValue({ settings: PROCESSED });
|
|
|
|
|
|
|
|
|
|
renderOverlay();
|
|
|
|
|
await screen.findByText("How are you using SnapOtter?");
|
|
|
|
|
|
|
|
|
|
fireEvent.click(screen.getByRole("radio", { name: /Just me/ }));
|
|
|
|
|
expect(screen.getByRole("button", { name: "Continue" })).not.toBeDisabled();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-07-02 18:37:12 +08:00
|
|
|
it("renders nothing when the admin must still change their password", async () => {
|
|
|
|
|
useAuth.mockReturnValue({ role: "admin", mustChangePassword: true });
|
2026-07-22 00:31:30 +08:00
|
|
|
apiGet.mockResolvedValue({ settings: PROCESSED });
|
2026-07-02 18:37:12 +08:00
|
|
|
|
|
|
|
|
renderOverlay();
|
|
|
|
|
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
|
|
|
expect(apiGet).not.toHaveBeenCalled();
|
|
|
|
|
expect(screen.queryByText("How are you using SnapOtter?")).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("renders nothing on the change-password route even if mustChangePassword is stale-false", async () => {
|
|
|
|
|
useAuth.mockReturnValue({ role: "admin", mustChangePassword: false });
|
2026-07-22 00:31:30 +08:00
|
|
|
apiGet.mockResolvedValue({ settings: PROCESSED });
|
2026-07-02 18:37:12 +08:00
|
|
|
|
|
|
|
|
renderOverlay("/change-password");
|
|
|
|
|
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
|
|
|
expect(apiGet).not.toHaveBeenCalled();
|
|
|
|
|
expect(screen.queryByText("How are you using SnapOtter?")).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("renders nothing on the privacy policy route", async () => {
|
|
|
|
|
useAuth.mockReturnValue({ role: "admin", mustChangePassword: false });
|
2026-07-22 00:31:30 +08:00
|
|
|
apiGet.mockResolvedValue({ settings: PROCESSED });
|
2026-07-02 18:37:12 +08:00
|
|
|
|
|
|
|
|
renderOverlay("/privacy");
|
|
|
|
|
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
|
|
|
expect(apiGet).not.toHaveBeenCalled();
|
|
|
|
|
expect(screen.queryByText("How are you using SnapOtter?")).toBeNull();
|
|
|
|
|
});
|
2026-07-02 00:02:24 +08:00
|
|
|
});
|