Merge solo sealed answers that unlock on pairing into staging

This commit is contained in:
jjbrunton committed 2026-08-30 19:49:51 +01:00
commit 7d392e756d
26 files changed
+510 -75

No files matched your search

@@ -2,6 +2,7 @@ import { act, renderHook, waitFor } from "@testing-library/react-native";
import { useSwipeScreen } from "@/features/swipe/hooks/useSwipeScreen";
import { ApiError, apiClient } from "@/lib/apiClient";
import { useAuthStore } from "@/store";
import * as swipeService from "@/features/swipe/services/swipeService";
import { getSkippedQuestionIds } from "@/lib/skippedQuestions";
@@ -15,12 +16,13 @@ jest.mock("expo-router", () => ({
// Stable identities: the hook's effects key off these, so fresh objects per render
// would retrigger the fetch effect forever and the screen would never settle.
jest.mock("@/store", () => {
const auth = { user: { id: "user-1" }, partner: { id: "user-2" }, couple: { id: "couple-1", couple_id: "couple-1" } };
const auth = { user: { id: "user-1" }, partner: { id: "user-2" }, couple: { id: "couple-1", couple_id: "couple-1" }, sealedCount: 0 };
const packs = { enabledPackIds: ["pack-1"], ensureEnabledPacksLoaded: async () => undefined, packs: [] };
const packState = { ...packs, invalidatePacks: jest.fn() };
const responses = { invalidateResponses: jest.fn() };
const useAuthStore = () => auth;
useAuthStore.getState = () => auth.user;
useAuthStore.setState = jest.fn();
const usePacksStore = () => packs;
usePacksStore.getState = () => packState;
const useResponsesStore = () => responses;
@@ -146,6 +148,19 @@ describe("useSwipeScreen daily limit", () => {
expect(result.current.dailyLimitInfo).toMatchObject({ responses_today: 5, remaining: 5 });
});
it("updates the sealed answer count optimistically when the server reports one", async () => {
// A solo answer has no partner to match against, so sealed_count is the
// only feedback the server gives back; the banner reads it from auth state.
const { result } = await renderReady();
jest.spyOn(apiClient, "post").mockResolvedValueOnce({ match: null, sealed_count: 3 });
await act(async () => {
await result.current.handleAnswer("q1", "yes");
});
expect(useAuthStore.setState).toHaveBeenCalledWith({ sealedCount: 3 });
});
it("refreshes the shared streak once the answer lands", async () => {
const { result } = await renderReady();
mockFetchStreak.mockClear();
@@ -0,0 +1,22 @@
import { buildInviteShareMessage } from "../../lib/inviteShareCopy";
describe("buildInviteShareMessage", () => {
it("leads with a plain invite when there are no sealed answers", () => {
const message = buildInviteShareMessage("ABCD1234", 0);
expect(message).toBe(
"Join me on Sauci! Tap this link to pair up instantly: https://sauci.app/join/ABCD1234 (or enter code ABCD1234 in the app)"
);
});
it("leads with earned value and singular wording for exactly one sealed answer", () => {
const message = buildInviteShareMessage("ABCD1234", 1);
expect(message).toBe(
"Join me on Sauci! I have already answered 1 question about us. Tap this link to unlock them: https://sauci.app/join/ABCD1234 (or enter code ABCD1234 in the app)"
);
});
it("pluralises for more than one sealed answer", () => {
const message = buildInviteShareMessage("ABCD1234", 5);
expect(message).toContain("I have already answered 5 questions about us.");
});
});
@@ -30,10 +30,15 @@ const profile = {
describe("authStore", () => {
beforeEach(() => {
jest.clearAllMocks();
// fetchUser always fetches couple state now, since it also carries the
// sealed answer count for a user who has no couple yet. Individual tests
// override this when they care about the couple/partner shape.
jest.spyOn(coupleApi, "getState").mockResolvedValue({ couple: null, partner: null, sealed_count: 0 });
useAuthStore.setState({
user: null,
couple: null,
partner: null,
sealedCount: 0,
isLoading: true,
isAuthenticated: false,
isAnonymous: false,
@@ -89,12 +94,12 @@ describe("authStore", () => {
max_intensity: 3,
};
useAuthStore.setState({ user: pairedProfile } as any);
const stateSpy = jest.spyOn(coupleApi, "getState").mockResolvedValueOnce({ couple, partner });
const stateSpy = jest.spyOn(coupleApi, "getState").mockResolvedValueOnce({ couple, partner, sealed_count: 3 });
await useAuthStore.getState().fetchCouple();
expect(stateSpy).toHaveBeenCalledWith();
expect(useAuthStore.getState()).toMatchObject({ couple, partner });
expect(useAuthStore.getState()).toMatchObject({ couple, partner, sealedCount: 3 });
});
it("updates activity through the standalone API", async () => {