Skip to content

Commit aa58c75

Browse files
authored
Fix settings e2e menu navigation (#543)
1 parent f0cc427 commit aa58c75

4 files changed

Lines changed: 54 additions & 16 deletions

File tree

web/src/mocks/handlers/roles.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
// under the License.
1717

1818
import { http, HttpResponse } from "msw";
19+
import settingsFixture from "@/features/core/identity/__fixtures__/settings.json";
1920
import type { PrivilegeKey, Role, UserRole } from "@/generated/core/types.gen";
2021

2122
const PRIVILEGES: PrivilegeKey[] = [
@@ -79,6 +80,13 @@ const initialRoles: MockRole[] = [
7980
{ user_id: "u3", role_id: "role-auditor", granted_at: "2026-01-15T09:00:00Z" },
8081
],
8182
},
83+
...Object.values(settingsFixture.roleDetails).map((detail) => ({
84+
...detail.role,
85+
privileges: detail.privileges as PrivilegeKey[],
86+
holders: (settingsFixture.roles as UserRole[]).filter(
87+
(grant) => grant.role_id === detail.role.id,
88+
),
89+
})),
8290
];
8391

8492
const roles = new Map<string, MockRole>(initialRoles.map((role) => [role.id ?? "", role]));

web/src/shared/layout/UserPill.tsx

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@
1818
"use client";
1919

2020
import { LogOut, Settings } from "lucide-react";
21-
import Link from "next/link";
2221
import { useSession } from "next-auth/react";
2322
import { useSignOut } from "@/shared/auth/useSignOut";
2423
import { Avatar, AvatarFallback } from "@/shared/ui/avatar";
2524
import {
2625
DropdownMenu,
2726
DropdownMenuContent,
2827
DropdownMenuItem,
28+
DropdownMenuLinkItem,
2929
DropdownMenuSeparator,
3030
DropdownMenuTrigger,
3131
} from "@/shared/ui/dropdown-menu";
@@ -66,14 +66,10 @@ export function UserPill() {
6666
<div className="truncate text-xs text-muted-foreground">{email}</div>
6767
</div>
6868
<DropdownMenuSeparator />
69-
<DropdownMenuItem
70-
render={(props) => (
71-
<Link {...props} href="/settings">
72-
<Settings className="mr-2 h-4 w-4" />
73-
Settings
74-
</Link>
75-
)}
76-
/>
69+
<DropdownMenuLinkItem href="/settings" closeOnClick>
70+
<Settings className="mr-2 h-4 w-4" />
71+
Settings
72+
</DropdownMenuLinkItem>
7773
<DropdownMenuSeparator />
7874
<DropdownMenuItem
7975
variant="destructive"

web/src/shared/ui/dropdown-menu.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,26 @@ function DropdownMenuItem({
113113
);
114114
}
115115

116+
function DropdownMenuLinkItem({
117+
className,
118+
inset,
119+
...props
120+
}: MenuPrimitive.LinkItem.Props & {
121+
inset?: boolean;
122+
}) {
123+
return (
124+
<MenuPrimitive.LinkItem
125+
data-slot="dropdown-menu-link-item"
126+
data-inset={inset}
127+
className={cn(
128+
"group/dropdown-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
129+
className,
130+
)}
131+
{...props}
132+
/>
133+
);
134+
}
135+
116136
function DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) {
117137
return <MenuPrimitive.SubmenuRoot data-slot="dropdown-menu-sub" {...props} />;
118138
}
@@ -264,6 +284,7 @@ export {
264284
DropdownMenuGroup,
265285
DropdownMenuLabel,
266286
DropdownMenuItem,
287+
DropdownMenuLinkItem,
267288
DropdownMenuCheckboxItem,
268289
DropdownMenuRadioGroup,
269290
DropdownMenuRadioItem,

web/tests/settings.e2e.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,28 +19,41 @@ import { expect, test } from "@playwright/test";
1919
import { signInAs } from "./fixtures/auth";
2020

2121
test.describe("settings", () => {
22+
test.setTimeout(90_000);
23+
2224
test("account menu opens the settings page and theme persists across reload", async ({
2325
page,
2426
}) => {
2527
await signInAs(page, "admin");
26-
await page.goto("/");
28+
await page.goto("/", { waitUntil: "domcontentloaded" });
29+
await expect(page.getByRole("button", { name: /account menu/i })).toBeVisible({
30+
timeout: 30_000,
31+
});
2732

2833
// Open the account menu and navigate to Settings.
2934
await page.getByRole("button", { name: /account menu/i }).click();
30-
await page.getByRole("menuitem", { name: /settings/i }).click();
31-
await expect(page).toHaveURL(/\/settings$/);
32-
await expect(page.getByRole("heading", { name: /^Settings$/ })).toBeVisible();
35+
await Promise.all([
36+
page.waitForURL(/\/settings$/, { waitUntil: "domcontentloaded" }),
37+
page.getByRole("menuitem", { name: /settings/i }).click(),
38+
]);
39+
await expect(page.getByRole("heading", { name: /^Settings$/ })).toBeVisible({
40+
timeout: 30_000,
41+
});
3342

3443
// The identity cards render against the seeded MSW fixtures.
35-
await expect(page.getByRole("heading", { name: "Roles" })).toBeVisible();
36-
await expect(page.getByRole("heading", { name: "Effective privileges" })).toBeVisible();
44+
await expect(page.getByRole("heading", { name: "Roles" })).toBeVisible({
45+
timeout: 30_000,
46+
});
47+
await expect(page.getByRole("heading", { name: "Effective privileges" })).toBeVisible({
48+
timeout: 30_000,
49+
});
3750

3851
// Switch to the dark theme and confirm it applies.
3952
await page.getByRole("button", { name: /^dark$/i }).click();
4053
await expect(page.locator("html")).toHaveClass(/dark/);
4154

4255
// Reload and confirm the choice persisted (next-themes localStorage).
43-
await page.reload();
56+
await page.reload({ waitUntil: "domcontentloaded" });
4457
await expect(page.getByRole("button", { name: /^dark$/i })).toHaveAttribute(
4558
"aria-pressed",
4659
"true",

0 commit comments

Comments
 (0)