From fff02b78ea6ab675c2e477de5b20f9867b0fbd3e Mon Sep 17 00:00:00 2001
From: Kalle <38327916+Sendouc@users.noreply.github.com>
Date: Fri, 25 Sep 2026 07:20:16 +0300
Subject: [PATCH] Fix user card send friend request disabled when it should not
be
---
.../user-card/components/UserCard.tsx | 31 +++++++++++++---
...-09-25-user-card-friend-request-pending.md | 5 +++
e2e/pages/user/user-card.ts | 11 ++++++
e2e/pages/user/user-page.ts | 9 +++++
e2e/user-card.spec.ts | 35 ++++++++++++++++++-
5 files changed, 85 insertions(+), 6 deletions(-)
create mode 100644 changelog/2026-09-25-user-card-friend-request-pending.md
diff --git a/app/features/user-card/components/UserCard.tsx b/app/features/user-card/components/UserCard.tsx
index 4bd3b6341..648d121aa 100644
--- a/app/features/user-card/components/UserCard.tsx
+++ b/app/features/user-card/components/UserCard.tsx
@@ -95,15 +95,39 @@ export function UserCard({
withMutualFriends?: boolean;
children: React.ReactNode;
}) {
- const { t } = useTranslation(["common", "q"]);
const lookedUpData = useUserCardData(userId);
const data = dataProp ?? lookedUpData;
+ if (!data) return <>{children}>;
+
+ // keyed so friendship state doesn't carry over to another user e.g. when navigating between user pages
+ return (
+
+ {children}
+
+ );
+}
+
+function UserCardPopover({
+ data,
+ withMutualFriends,
+ children,
+}: {
+ data: UserCardData;
+ withMutualFriends: boolean;
+ children: React.ReactNode;
+}) {
+ const { t } = useTranslation(["common", "q"]);
+
// on narrow viewports the card is placed vertically so React Aria can shift it to stay on-screen
const placement = useLayoutSize() === "mobile" ? "bottom" : "right";
const user = useUser();
- const isOwnCard = user?.id === data?.id;
+ const isOwnCard = user?.id === data.id;
const [isOpen, setIsOpen] = React.useState(false);
// outside the popover so the modals survive it closing when they take focus
@@ -120,7 +144,6 @@ export function UserCard({
if (!nextIsOpen) return;
if (friendshipLoadedRef.current) return;
if (isOwnCard) return;
- if (typeof data?.id !== "number") return;
friendshipLoadedRef.current = true;
fetcher.load(
@@ -148,8 +171,6 @@ export function UserCard({
setIsReportDialogOpen(true);
};
- if (!data) return <>{children}>;
-
return (
<>
+ this.locators.sendFriendRequestButton.click(),
+ );
+ }
+
+ async close() {
+ await this.page.keyboard.press("Escape");
+ await expect(this.locators.banner).not.toBeVisible();
+ }
+
async openReportDialog() {
await this.locators.reportUserButton.click();
return new ReportUserDialog(this.page);
diff --git a/e2e/pages/user/user-page.ts b/e2e/pages/user/user-page.ts
index 2454441b8..736d13ebc 100644
--- a/e2e/pages/user/user-page.ts
+++ b/e2e/pages/user/user-page.ts
@@ -3,6 +3,7 @@ import { userPage } from "~/utils/urls";
import { navigate } from "../../helpers/playwright";
import { TeamPage } from "../team/team-page";
import { TopSearchPlayerPage } from "../top-search/top-search-player-page";
+import { UserCard } from "./user-card";
import { UserEditProfilePage } from "./user-edit-profile-page";
import { UserEditWidgetsPage } from "./user-edit-widgets-page";
import { UserResultsPage } from "./user-results-page";
@@ -76,6 +77,14 @@ export class UserPage {
return this.page.getByRole("heading", { name: username });
}
+ /** Opens the user card from the username heading. */
+ openUserCard(username: string) {
+ return UserCard.open(
+ this.page,
+ this.usernameHeading(username).getByRole("button"),
+ );
+ }
+
async openEditProfile() {
await this.locators.editProfileButton.click();
return new UserEditProfilePage(this.page);
diff --git a/e2e/user-card.spec.ts b/e2e/user-card.spec.ts
index a0cc0a922..c343a0b0a 100644
--- a/e2e/user-card.spec.ts
+++ b/e2e/user-card.spec.ts
@@ -1,10 +1,12 @@
-import { NZAP_TEST_ID } from "~/db/seed/constants";
+import { NZAP_TEST_DISCORD_ID, NZAP_TEST_ID } from "~/db/seed/constants";
import { ADMIN_ID } from "~/features/admin/admin-constants";
import { SENDOUQ_LOOKING_PAGE } from "~/utils/urls";
import { expect, impersonate, test } from "./helpers/playwright";
import { FriendsPage } from "./pages/friends/friends-page";
import { LFGPage } from "./pages/lfg/lfg-page";
+import { GlobalSearchDialog } from "./pages/search/global-search-dialog";
import { SendouQLookingPage } from "./pages/sendouq/sendouq-looking-page";
+import { UserPage } from "./pages/user/user-page";
test.describe("User card", () => {
test("edits banner and bio from the looking page", async ({
@@ -126,4 +128,35 @@ test.describe("User card friend request", () => {
await expect(card.locators.pendingFriendRequestButton).toBeDisabled();
await expect(card.locators.acceptFriendRequestButton).not.toBeVisible();
});
+
+ test("sending a request does not mark another user's card pending after navigating between user pages", async ({
+ page,
+ factories,
+ }) => {
+ const otherUsername = "FriendTarget";
+ await factories.UserFactory.create({ discordName: otherUsername });
+
+ await impersonate(page);
+
+ const userPage = new UserPage(page);
+ await userPage.goto(NZAP_TEST_DISCORD_ID);
+
+ const nzapCard = await userPage.openUserCard("N-ZAP");
+ await nzapCard.sendFriendRequest();
+ await expect(nzapCard.locators.pendingFriendRequestButton).toBeVisible();
+ await nzapCard.close();
+
+ const search = new GlobalSearchDialog(page);
+ await search.open();
+ await search.selectType("users");
+ await search.search(otherUsername);
+ await search.selectOption(otherUsername);
+ await expect(userPage.usernameHeading(otherUsername)).toBeVisible();
+
+ const otherCard = await userPage.openUserCard(otherUsername);
+ await expect(otherCard.locators.sendFriendRequestButton).toBeVisible();
+ await expect(
+ otherCard.locators.pendingFriendRequestButton,
+ ).not.toBeVisible();
+ });
});