mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
fix(mood): open the reminder's entry sheet only in the own record
This commit is contained in:
1 parent
3c4e053dcd
commit
a4a3318de3
3 files changed
+143
-9
No files matched your search
@@ -0,0 +1,106 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
/**
|
||||
* `/mood?add=1` is the mood reminder's tap target. It opens the entry sheet
|
||||
* only in the caller's own record: a session that is inside a managed
|
||||
* profile or a shared record opens the page without the sheet, so a reminder
|
||||
* meant for the recipient never becomes a write form on somebody else.
|
||||
*/
|
||||
|
||||
const caps = {
|
||||
current: {
|
||||
inSharedRecord: false,
|
||||
recordSessionPending: undefined as boolean | undefined,
|
||||
accessRefused: undefined as boolean | undefined,
|
||||
canManageDomain: (_d: string) => true,
|
||||
},
|
||||
};
|
||||
const params = { current: new URLSearchParams("add=1") };
|
||||
|
||||
vi.mock("@/hooks/use-record-capabilities", () => ({
|
||||
useRecordCapabilities: () => caps.current,
|
||||
}));
|
||||
vi.mock("@/hooks/use-auth", () => ({
|
||||
useAuth: () => ({
|
||||
user: { modules: {} },
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
}),
|
||||
}));
|
||||
vi.mock("@/hooks/use-mounted", () => ({ useMounted: () => true }));
|
||||
vi.mock("@/hooks/use-pull-to-refresh", () => ({
|
||||
usePullToRefresh: () => ({}),
|
||||
}));
|
||||
vi.mock("@tanstack/react-query", () => ({
|
||||
useQueryClient: () => ({ invalidateQueries: () => {} }),
|
||||
}));
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => ({ replace: () => {}, push: () => {} }),
|
||||
useSearchParams: () => params.current,
|
||||
}));
|
||||
vi.mock("@/lib/i18n/context", () => ({
|
||||
useTranslations: () => ({ t: (key: string) => key }),
|
||||
}));
|
||||
vi.mock("@/components/ui/responsive-sheet", () => ({
|
||||
ResponsiveSheet: ({ open }: { open: boolean; children?: ReactNode }) =>
|
||||
open ? <div data-slot="mood-sheet-open" /> : null,
|
||||
}));
|
||||
vi.mock("@/components/mood/mood-form", () => ({ MoodForm: () => null }));
|
||||
vi.mock("@/components/mood/mood-list", () => ({ MoodList: () => null }));
|
||||
vi.mock("@/components/ui/pull-to-refresh-indicator", () => ({
|
||||
PullToRefreshIndicator: () => null,
|
||||
}));
|
||||
|
||||
import MoodPageClient from "../page-client";
|
||||
|
||||
function sheetOpen(): boolean {
|
||||
return renderToStaticMarkup(<MoodPageClient />).includes(
|
||||
'data-slot="mood-sheet-open"',
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
params.current = new URLSearchParams("add=1");
|
||||
caps.current = {
|
||||
inSharedRecord: false,
|
||||
recordSessionPending: undefined,
|
||||
accessRefused: undefined,
|
||||
canManageDomain: () => true,
|
||||
};
|
||||
});
|
||||
|
||||
describe("/mood ?add=1 and the record on screen", () => {
|
||||
it("opens the entry sheet in the caller's own record", () => {
|
||||
expect(sheetOpen()).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps the sheet closed without the param", () => {
|
||||
params.current = new URLSearchParams();
|
||||
expect(sheetOpen()).toBe(false);
|
||||
});
|
||||
|
||||
it("does not open the sheet inside a managed profile the caller manages", () => {
|
||||
// A guardian switched into a managed profile holds MANAGE on mind, so
|
||||
// the sheet would otherwise open and save onto that profile.
|
||||
caps.current = { ...caps.current, inSharedRecord: true };
|
||||
expect(sheetOpen()).toBe(false);
|
||||
});
|
||||
|
||||
it("does not open the sheet while the record context is pending or refused", () => {
|
||||
caps.current = {
|
||||
...caps.current,
|
||||
inSharedRecord: true,
|
||||
recordSessionPending: true,
|
||||
canManageDomain: () => false,
|
||||
};
|
||||
expect(sheetOpen()).toBe(false);
|
||||
caps.current = {
|
||||
...caps.current,
|
||||
recordSessionPending: undefined,
|
||||
accessRefused: true,
|
||||
};
|
||||
expect(sheetOpen()).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -21,7 +21,12 @@ import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
export default function MoodPageClient() {
|
||||
const { user, isAuthenticated, isLoading } = useAuth();
|
||||
const { canManageDomain, inSharedRecord } = useRecordCapabilities();
|
||||
const {
|
||||
canManageDomain,
|
||||
inSharedRecord,
|
||||
recordSessionPending,
|
||||
accessRefused,
|
||||
} = useRecordCapabilities();
|
||||
// A mood entry is created at MANAGE under the mind section.
|
||||
const canAddMood = canManageDomain("mind");
|
||||
const mounted = useMounted();
|
||||
@@ -33,15 +38,35 @@ export default function MoodPageClient() {
|
||||
// hydration render agree; the effect below strips the one-shot param so the
|
||||
// back button does not reopen the sheet. A later client-side navigation
|
||||
// back onto `?add=1` is handled render-side, the measurements-page pattern.
|
||||
//
|
||||
// The request is held apart from the sheet's own open state because it is
|
||||
// honoured only in the caller's own record. A reminder is about the person
|
||||
// who receives it; a session that is currently inside a managed profile or
|
||||
// a shared record would otherwise open a write form there, and an entry
|
||||
// saved from it lands on the wrong person. In that case the page opens
|
||||
// without the sheet and the shell's shared-record banner offers the way
|
||||
// back. The answer is read once the account query settled (`isLoading`
|
||||
// gates the content render), so the request is dropped, not deferred: a
|
||||
// later switch back to the own record must not pop a stale sheet.
|
||||
const addParam = searchParams.get("add") === "1";
|
||||
const [dialogOpen, setDialogOpen] = useState(() => addParam);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [addRequested, setAddRequested] = useState(() => addParam);
|
||||
const [consumedAdd, setConsumedAdd] = useState(() => addParam);
|
||||
if (!addParam && consumedAdd) {
|
||||
setConsumedAdd(false);
|
||||
} else if (addParam && !consumedAdd) {
|
||||
setConsumedAdd(true);
|
||||
setDialogOpen(true);
|
||||
setAddRequested(true);
|
||||
}
|
||||
const ownRecord = !inSharedRecord && !recordSessionPending && !accessRefused;
|
||||
if (addRequested && mounted && !isLoading && !ownRecord) {
|
||||
setAddRequested(false);
|
||||
}
|
||||
const sheetOpen = (dialogOpen || (addRequested && ownRecord)) && canAddMood;
|
||||
const setSheetOpen = useCallback((open: boolean) => {
|
||||
setDialogOpen(open);
|
||||
if (!open) setAddRequested(false);
|
||||
}, []);
|
||||
// The list's empty state carries the add action while there is nothing
|
||||
// to list, so the header drops its own then.
|
||||
const [listEmpty, setListEmpty] = useState(false);
|
||||
@@ -68,7 +93,7 @@ export default function MoodPageClient() {
|
||||
);
|
||||
const pull = usePullToRefresh({
|
||||
onRefresh: refreshVisible,
|
||||
disabled: dialogOpen,
|
||||
disabled: sheetOpen,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -149,14 +174,14 @@ export default function MoodPageClient() {
|
||||
/>
|
||||
|
||||
<ResponsiveSheet
|
||||
open={dialogOpen && canAddMood}
|
||||
onOpenChange={setDialogOpen}
|
||||
open={sheetOpen}
|
||||
onOpenChange={setSheetOpen}
|
||||
title={t("dashboard.quickAddMood")}
|
||||
footer={<div ref={setFooterEl} className="flex w-full" />}
|
||||
>
|
||||
<MoodForm
|
||||
onSuccess={() => setDialogOpen(false)}
|
||||
onCancel={() => setDialogOpen(false)}
|
||||
onSuccess={() => setSheetOpen(false)}
|
||||
onCancel={() => setSheetOpen(false)}
|
||||
footerSlot={footerEl}
|
||||
/>
|
||||
</ResponsiveSheet>
|
||||
|
||||
@@ -36,7 +36,10 @@ export const NOTIFICATION_TAP_PATHS: Record<EventType, string | null> = {
|
||||
WITHINGS_SYNC_FAILED: "/settings/integrations",
|
||||
SYSTEM_ALERT: "/",
|
||||
PERSONAL_RECORD: "/achievements",
|
||||
// `?add=1` opens the mood entry sheet on the first paint.
|
||||
// `?add=1` opens the mood entry sheet on the first paint, and only in the
|
||||
// recipient's own record: a session switched into another record opens the
|
||||
// page without the sheet (`src/app/mood/page-client.tsx`). No other target
|
||||
// here opens a write form.
|
||||
MOOD_REMINDER: "/mood?add=1",
|
||||
CYCLE_PERIOD_SOON: "/cycle",
|
||||
CYCLE_PERIOD_CONFIRM: "/cycle",
|
||||
|
||||
Reference in new issue
Block a user