mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
test(e2e): the sharing journey, and an honest account of the stale paint
Two browser contexts, two seeded accounts, one database: the owner invites, the delegate accepts and opens the record, the owner takes it back. Every step through the shipped UI, because the half of this feature that can go wrong is the half a person touches. The stale-paint check is a negative one and deliberately not "the new record's data arrives" — a check that waits for the new data says nothing about what was on screen while it arrived. An init script arms a MutationObserver before any page script on every navigation, including the reload the switch performs, and latches the delegate's own marker weight. The observer's positive control runs first on a page that definitely shows it, so its later silence is not the silence of a broken observer. Then it was broken on purpose, and the result is written into the spec rather than assumed. Removing the switch-time cache wipe, the persister's restore guard and the record-scoped hash — all three — leaves the spec GREEN: the restore already refuses to clobber a query the live cache holds, and the dashboard's query is created during render, so the snapshot never reaches that route's screen at all. Removing the reload turns it red on the banner rather than on the marker. So this suite is a regression net over the journey, not a proof of any one control; the controls are proven where they can be broken and observed, in the unit suites, and the spec's header says so. The nav assertion turned up a real gap while it was failing: the avatar block still linked into the account settings from inside a shared record. Those routes refuse under a switch, so the link led only to an explanation of why it did not work. It renders as a plain identity block now, and the mobile user menu drops the same utilities. Setup gains a second account, a marker weight in each record, a grant reset, and a clear of the login bucket — this setup signs in twice now, and the ceiling is five attempts a quarter-hour, so three local runs used to end with a 429 from the fixture rather than a failure from the product.
This commit is contained in:
1 parent
883fd2d047
commit
cacf802b8d
4 files changed
+182
-57
No files matched your search
@@ -59,12 +59,51 @@ function getInitials(name: string): string {
|
||||
.join("");
|
||||
}
|
||||
|
||||
/**
|
||||
* The sidebar footer's name-and-avatar block.
|
||||
*
|
||||
* A link into the account settings normally, and a plain block while this
|
||||
* browser is acting on somebody else's record — the settings routes refuse
|
||||
* under a switch, so the link would lead only to an explanation of why it did
|
||||
* not work. It keeps naming the person at the keyboard either way, which is
|
||||
* what a switched session needs it to do.
|
||||
*/
|
||||
function FooterIdentity({
|
||||
sharedRecord,
|
||||
label,
|
||||
children,
|
||||
}: {
|
||||
sharedRecord: boolean;
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const className =
|
||||
"flex min-w-0 flex-1 items-center gap-3 rounded-md transition-colors";
|
||||
if (sharedRecord) {
|
||||
return <div className={className}>{children}</div>;
|
||||
}
|
||||
return (
|
||||
<Link
|
||||
href="/settings/account"
|
||||
aria-label={label}
|
||||
className={`hover:bg-accent ${className}`}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarUserSection({ collapsed }: { collapsed: boolean }) {
|
||||
const { user } = useAuth();
|
||||
const logout = useLogout();
|
||||
const { theme, setTheme } = useTheme();
|
||||
const { t } = useTranslations();
|
||||
const avatarUrl = user?.avatarUrl ?? null;
|
||||
// v1.36.0 — while this browser is acting on somebody else's record the
|
||||
// account surfaces are unreachable, so the avatar stops being a door into
|
||||
// them. It still names the person at the keyboard, which is exactly what a
|
||||
// switched session needs it to do.
|
||||
const sharedRecord = user?.accountAccess?.active != null;
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
@@ -77,6 +116,23 @@ function SidebarUserSection({ collapsed }: { collapsed: boolean }) {
|
||||
<Sun className="h-4 w-4" />
|
||||
);
|
||||
|
||||
const identityBlock = (
|
||||
<>
|
||||
<Avatar className="h-9 w-9 shrink-0">
|
||||
{avatarUrl && <AvatarImage src={avatarUrl} alt={user.username} />}
|
||||
<AvatarFallback className="bg-primary/15 text-primary text-xs font-medium">
|
||||
{getInitials(user.username)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-semibold">{user.username}</p>
|
||||
{user.email && (
|
||||
<p className="text-muted-foreground truncate text-xs">{user.email}</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const dropdownContent = (
|
||||
<DropdownMenuContent
|
||||
side="right"
|
||||
@@ -88,34 +144,27 @@ function SidebarUserSection({ collapsed }: { collapsed: boolean }) {
|
||||
settings. Clicking the avatar / name jumps straight to
|
||||
`/settings/account` rather than making the user hunt for a
|
||||
separate "Settings" entry. */}
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
href="/settings/account"
|
||||
className="flex cursor-pointer items-center gap-3 px-2 py-2"
|
||||
>
|
||||
<Avatar className="h-9 w-9 shrink-0">
|
||||
{avatarUrl && <AvatarImage src={avatarUrl} alt={user.username} />}
|
||||
<AvatarFallback className="bg-primary/15 text-primary text-xs font-medium">
|
||||
{getInitials(user.username)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-semibold">{user.username}</p>
|
||||
{user.email && (
|
||||
<p className="text-muted-foreground truncate text-xs">
|
||||
{user.email}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
{sharedRecord ? (
|
||||
<div className="flex items-center gap-3 px-2 py-2">{identityBlock}</div>
|
||||
) : (
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
href="/settings/account"
|
||||
className="flex cursor-pointer items-center gap-3 px-2 py-2"
|
||||
>
|
||||
{identityBlock}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/notifications" className="cursor-pointer">
|
||||
<Bell className="mr-2 h-4 w-4" />
|
||||
{t("nav.notifications")}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
{!sharedRecord && (
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/notifications" className="cursor-pointer">
|
||||
<Bell className="mr-2 h-4 w-4" />
|
||||
{t("nav.notifications")}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{/* The about section lives at the end of the settings shell nav;
|
||||
the avatar menu stays focused on account-level actions. */}
|
||||
<DropdownMenuSub>
|
||||
@@ -193,10 +242,9 @@ function SidebarUserSection({ collapsed }: { collapsed: boolean }) {
|
||||
{/* R20 — the avatar + name in the expanded footer routes straight
|
||||
into the account settings; the kebab to the right still opens
|
||||
the rest of the user menu. */}
|
||||
<Link
|
||||
href="/settings/account"
|
||||
aria-label={t("nav.accountSettings")}
|
||||
className="hover:bg-accent flex min-w-0 flex-1 items-center gap-3 rounded-md transition-colors"
|
||||
<FooterIdentity
|
||||
sharedRecord={sharedRecord}
|
||||
label={t("nav.accountSettings")}
|
||||
>
|
||||
<Avatar className="h-8 w-8 shrink-0">
|
||||
{avatarUrl && <AvatarImage src={avatarUrl} alt={user.username} />}
|
||||
@@ -212,7 +260,7 @@ function SidebarUserSection({ collapsed }: { collapsed: boolean }) {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
</FooterIdentity>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
aria-label={t("nav.userMenu")}
|
||||
|
||||
@@ -42,6 +42,8 @@ function getInitials(name: string): string {
|
||||
|
||||
export function TopBar() {
|
||||
const { user, isLoading } = useAuth();
|
||||
// v1.36.0 — acting on somebody else's record; see the menu below.
|
||||
const sharedRecord = user?.accountAccess?.active != null;
|
||||
const logout = useLogout();
|
||||
const { theme, setTheme } = useTheme();
|
||||
const { t } = useTranslations();
|
||||
@@ -113,25 +115,34 @@ export function TopBar() {
|
||||
<ChevronDown className="h-3 w-3 opacity-60" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-60">
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/settings/account" className="cursor-pointer">
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
{t("nav.settings")}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/notifications" className="cursor-pointer">
|
||||
<Bell className="mr-2 h-4 w-4" />
|
||||
{t("nav.notifications")}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/admin" className="cursor-pointer">
|
||||
<Shield className="mr-2 h-4 w-4" />
|
||||
{t("nav.admin")}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
{/* v1.36.0 — the account utilities drop out while this browser is
|
||||
acting on somebody else's record. Every route behind them
|
||||
refuses under a switch, and a control that leads only to an
|
||||
explanation of why it does not work is worse than no control.
|
||||
The switcher below is how the person gets back. */}
|
||||
{!sharedRecord && (
|
||||
<>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/settings/account" className="cursor-pointer">
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
{t("nav.settings")}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/notifications" className="cursor-pointer">
|
||||
<Bell className="mr-2 h-4 w-4" />
|
||||
{t("nav.notifications")}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/admin" className="cursor-pointer">
|
||||
<Shield className="mr-2 h-4 w-4" />
|
||||
{t("nav.admin")}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{/* v1.4.36 W4e — About moved into the Admin Console
|
||||
(`/admin/about`). The dropdown entry was redundant
|
||||
|
||||
Reference in new issue
Block a user