@@ -183,30 +184,32 @@ export function UserDetailsOverview({ user }: { user: AdminUserDetail }) {
},
]}
/>
-
{
@@ -67,7 +68,7 @@ const fixture = createServer((req, res) => {
else if (path.endsWith("/settings"))
body = { appearance: { theme: "light" }, defaults: { refresh_seconds: 0 } };
else if (path.endsWith("/notifications/config")) body = { enabled: false };
- else if (path === `/v1/admin/users/${userId}`) body = user;
+ else if (path === `/v1/admin/users/${userId}`) body = { ...user, username: configuredUsername };
else if (path === `/v1/admin/users/${userId}/interests`)
body = { items: [], total: 0, limit: 5, offset: 0 };
else if (path === `/v1/admin/users/${userId}/must-reads`) {
@@ -194,6 +195,7 @@ try {
}
await page.goto(`${origin}/users/${userId}`);
await page.getByText("Account", { exact: true }).waitFor();
+ await page.getByText("Reader profile", { exact: true }).waitFor();
for (const title of ["Dev Card styling", "Reader preferences", "Technical identifiers"]) {
const disclosure = page
.locator("details")
@@ -217,6 +219,55 @@ try {
await page.screenshot({ path: `${output}/details-${theme}-${width}.png`, fullPage: true });
}
}
+ configuredUsername = null;
+ await page.reload();
+ await page.getByText("Account", { exact: true }).waitFor();
+ for (const title of ["Reader profile", "Dev Card styling", "Dev Card"]) {
+ assert.equal(await page.getByText(title, { exact: true }).count(), 0);
+ }
+ for (const title of ["Reading activity", "Recommendation refresh", "Personalization"]) {
+ assert.equal(await page.getByText(title, { exact: true }).count(), 1);
+ }
+ for (const title of ["Reader preferences", "Technical identifiers"]) {
+ const disclosure = page
+ .locator("details")
+ .filter({ has: page.locator("summary", { hasText: title }) });
+ assert.equal(await disclosure.evaluate((node) => node.open), false);
+ await disclosure.locator("summary").focus();
+ await page.keyboard.press("Enter");
+ assert.equal(await disclosure.evaluate((node) => node.open), true);
+ await disclosure.locator("summary").click();
+ }
+ for (const theme of ["light", "dark"]) {
+ await page
+ .locator("html")
+ .evaluate((node, theme) => node.classList.toggle("dark", theme === "dark"), theme);
+ for (const width of [1440, 390]) {
+ await page.setViewportSize({ width, height: 1000 });
+ assert.equal(
+ await page.evaluate(() => document.documentElement.scrollWidth > innerWidth),
+ false,
+ );
+ if (width === 1440) {
+ const refreshPanel = await page
+ .getByText("Recommendation refresh", { exact: true })
+ .locator('xpath=ancestor::div[@data-slot="card"]')
+ .boundingBox();
+ const activityPanel = await page
+ .getByText("Personalization", { exact: true })
+ .locator('xpath=ancestor::div[@data-slot="card"]')
+ .boundingBox();
+ assert.ok(refreshPanel && activityPanel);
+ assert.ok(Math.abs(refreshPanel.y - activityPanel.y) < 1);
+ assert.ok(refreshPanel.x < activityPanel.x);
+ }
+ await page.screenshot({ path: `${output}/unclaimed-${theme}-${width}.png`, fullPage: true });
+ }
+ }
+ configuredUsername = user.username;
+ await page.reload();
+ await page.getByText("Reader profile", { exact: true }).waitFor();
+ assert.equal(await page.getByText("Dev Card styling", { exact: true }).count(), 1);
await page.goto(`${origin}/users/${userId}/analysis`);
mode = "empty";
await page.reload();
@@ -240,7 +291,7 @@ try {
);
assert.deepEqual(errors, []);
console.log(
- "Admin Must Reads: saved picks, reasons, timezone, read progress, empty states, retry, and responsive themes passed.",
+ "Admin users: claimed/unclaimed profile visibility, keyboard disclosures, responsive themes, and Must Reads passed.",
);
} catch (error) {
console.error(logs);
diff --git a/apps/admin/tests/users.test.tsx b/apps/admin/tests/users.test.tsx
index 966986e5..67ef0d25 100644
--- a/apps/admin/tests/users.test.tsx
+++ b/apps/admin/tests/users.test.tsx
@@ -1,10 +1,15 @@
// @vitest-environment jsdom
import { afterEach, beforeEach, expect, it, vi } from "vitest";
-import { act, cleanup, fireEvent, screen, waitFor, within } from "@testing-library/react";
+import { act, cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { renderAdmin } from "./render-admin";
import { ResourceList } from "@/components/organisms/resource-list";
import { ResourceDetail } from "@/components/organisms/resource-detail";
-import { UserRecords, UserAnalysis, UserAnalysisAction } from "@/components/organisms/user-details";
+import {
+ UserRecords,
+ UserAnalysis,
+ UserAnalysisAction,
+ UserDetailsOverview,
+} from "@/components/organisms/user-details";
import { getRecord, listRecords, listUserRecords } from "@/lib/resource-api";
import { adminRouteTitle } from "@/lib/page-titles";
import type { AdminUserDetail } from "@/lib/api/generated/models";
@@ -135,6 +140,46 @@ it("shows account information and links to existing-style detail sections", asyn
expect(screen.queryByRole("link", { name: "Logs" })).toBeNull();
expect(screen.queryByRole("link", { name: "Open in graph" })).toBeNull();
});
+
+it.each([null, undefined, "", " "])(
+ "omits profile and Dev Card details when the username is %j",
+ async (username) => {
+ vi.mocked(getRecord).mockResolvedValue({ ...user, username });
+ renderAdmin();
+ await screen.findByText("Account");
+ expect(screen.queryByText("Reader profile")).toBeNull();
+ expect(screen.queryByText("Dev Card styling")).toBeNull();
+ expect(screen.queryByText("Dev Card")).toBeNull();
+ expect(screen.queryByRole("link", { name: "Website" })).toBeNull();
+ expect(screen.queryByText("London")).toBeNull();
+ for (const title of [
+ "Reading activity",
+ "Recommendation refresh",
+ "Personalization",
+ "Reader preferences",
+ "Technical identifiers",
+ ]) {
+ expect(screen.getByText(title)).toBeTruthy();
+ }
+ expect(screen.getByText("Longest streak")).toBeTruthy();
+ expect(screen.getByRole("link", { name: "Reading days" })).toBeTruthy();
+ },
+);
+
+it("shows claimed profile details for private profiles and updates when the username changes", () => {
+ const view = render();
+ expect(screen.getByText("Reader profile")).toBeTruthy();
+ expect(screen.getByText("Hidden")).toBeTruthy();
+ expect(screen.getByText("Dev Card styling")).toBeTruthy();
+ view.rerender();
+ expect(screen.queryByText("Reader profile")).toBeNull();
+ expect(screen.queryByText("Dev Card styling")).toBeNull();
+ view.rerender();
+ expect(screen.getByText("Reader profile")).toBeTruthy();
+ expect(screen.getByText("Dev Card styling")).toBeTruthy();
+ expect(screen.getByRole("link", { name: "Website" })).toBeTruthy();
+});
+
it("labels stale prepared results and fetches only the selected user section", async () => {
renderAdmin(
,