v4: the avatar menu shows at every width and holds Log out, so a profile keeps only Invite; the profile's dump cards lay out again — your own profile had four buttons in a row, Invite someone, Settings, Manage and Log out, Invite taller than the rest, and on wide screens it was the only way to reach /settings or /manage. The avatar menu now leads the header's icon row at every width, after the name and Playlists text links on wide screens, and ends with Log out under a separator, muted until hovered. The profile keeps Invite someone alone. The menu's button outweighs .app-header-nav button and the themes' button rules (Brutalist and GeoCities list it among their flat buttons), so it reads as one of the menu's links. Since b9ff9c4, a card under 472px is laid out by the narrow-card container query, but the profile columns' dense-card rules still placed the thumb, body and vote themselves, more specifically: the thumb lost its row span, so the title fell under it and every card was a tall, mostly empty box. Those rules now only make the card denser (a smaller thumb, a 2-line title, smaller type) and leave placement to the container query. 670 frontend tests pass, and tsc, eslint, deno fmt, doc:lint and i18n:compile --strict are clean; checked in screenshots in Smooth dark at 1400 and 390px, Brutalist light at 390px, GeoCities light and Kanagawa dark at 1400px, in French.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,8 @@ import { screen } from "@testing-library/react";
|
||||
import { userEvent } from "@testing-library/user-event";
|
||||
import { UserMenu } from "../../../components/layout/UserMenu.tsx";
|
||||
import { makeSession } from "../../fixtures.ts";
|
||||
import { currentPath, renderWithContexts } from "../../render.tsx";
|
||||
import { currentPath, renderApp, renderWithContexts } from "../../render.tsx";
|
||||
import { AUTH_STORAGE_KEY } from "../../../utils/auth/storage.ts";
|
||||
|
||||
describe("UserMenu", () => {
|
||||
test("opens, links to profile and playlists, closes on Escape and outside", async () => {
|
||||
@@ -41,4 +42,26 @@ describe("UserMenu", () => {
|
||||
expect(currentPath()).toBe("/@ann");
|
||||
expect(screen.queryByRole("menu")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("links to /manage for those who hold category:manage", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithContexts(
|
||||
<UserMenu user={makeSession({ role: "admin" }).user} />,
|
||||
);
|
||||
await user.click(screen.getByRole("button", { name: "User menu" }));
|
||||
expect(screen.getByRole("menuitem", { name: "Manage" })).toHaveAttribute(
|
||||
"href",
|
||||
"/manage",
|
||||
);
|
||||
});
|
||||
|
||||
test("logs out", async () => {
|
||||
const session = makeSession({ username: "ann" });
|
||||
renderApp(<UserMenu user={session.user} />, { session });
|
||||
const user = userEvent.setup();
|
||||
await user.click(screen.getByRole("button", { name: "User menu" }));
|
||||
await user.click(screen.getByRole("menuitem", { name: "Log out" }));
|
||||
expect(localStorage.getItem(AUTH_STORAGE_KEY)).toBeNull();
|
||||
expect(screen.queryByRole("menu")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -75,8 +75,6 @@ describe("UserPublicProfile", () => {
|
||||
expect(dumpsAll).toHaveAttribute("href", "/@ann/dumps");
|
||||
expect(votesAll).toHaveAttribute("href", "/@ann/upvoted");
|
||||
await waitFor(() => expect(document.title).toMatch(/^ann · /));
|
||||
expect(screen.queryByRole("link", { name: "Settings" })).not
|
||||
.toBeInTheDocument();
|
||||
// The lists are read by the user's id, which a rename doesn't change.
|
||||
expect(calls("GET /api/users/:u/dumps")[0].path).toBe(
|
||||
"/api/users/ann/dumps",
|
||||
@@ -275,11 +273,11 @@ describe("UserPublicProfile", () => {
|
||||
expect(within(bob).getByRole("link", { name: /@cy/ })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("visitors can follow; a settings link is only on your own profile", async () => {
|
||||
test("visitors can follow; inviting is only on your own profile", async () => {
|
||||
open({ session: makeSession({ id: "bob" }) });
|
||||
expect(await screen.findByRole("button", { name: "Follow ann" }))
|
||||
.toBeInTheDocument();
|
||||
expect(screen.queryByRole("link", { name: "Settings" })).not
|
||||
expect(screen.queryByRole("button", { name: "+ Invite someone" })).not
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -414,29 +412,18 @@ describe("UserPublicProfile", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("your settings, and the site's tools for an admin, are a link away", async () => {
|
||||
test("an admin's own profile has no manage tab: that's for others'", async () => {
|
||||
open({
|
||||
session: { ...annSession, user: { ...annSession.user, role: "admin" } },
|
||||
user: { ...ann, role: "admin" },
|
||||
});
|
||||
expect(await screen.findByRole("link", { name: "Settings" }))
|
||||
.toHaveAttribute("href", "/settings");
|
||||
expect(screen.getByRole("link", { name: "Manage" })).toHaveAttribute(
|
||||
"href",
|
||||
"/manage",
|
||||
);
|
||||
// Their own profile has no manage tab: that's for others'.
|
||||
expect(await screen.findByRole("button", { name: "+ Invite someone" }))
|
||||
.toBeInTheDocument();
|
||||
expect(
|
||||
within(document.querySelector(".profile-tabs") as HTMLElement)
|
||||
.queryByRole("button", { name: "Manage" }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("log out", async () => {
|
||||
const { user } = open({ session: annSession });
|
||||
await user.click(await screen.findByRole("button", { name: "Log out" }));
|
||||
expect(localStorage.getItem(AUTH_STORAGE_KEY)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("manage", () => {
|
||||
|
||||
Reference in New Issue
Block a user