Skip to content

Commit b2de345

Browse files
fix: E2E member test flake from stale test data and unclosed dialog (#215) (#219)
Co-authored-by: Ona <no-reply@ona.com>
1 parent 1b2e8bf commit b2de345

3 files changed

Lines changed: 55 additions & 1 deletion

File tree

e2e/fixtures/supabase-admin.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,34 @@ export async function cleanupInvitesForEmail(email: string): Promise<void> {
150150
.ilike("email", email)
151151
.is("accepted_at", null);
152152
}
153+
154+
/**
155+
* Removes stale test users that match a given display name.
156+
* Used in beforeAll to clean up leftovers from previous test runs whose
157+
* afterAll cleanup was interrupted (e.g. process killed, timeout).
158+
* Skips the provided excludeUserId (the current test owner) to avoid
159+
* accidentally deleting the primary test user.
160+
*/
161+
export async function cleanupStaleTestUsers(
162+
displayName: string,
163+
excludeUserId?: string
164+
): Promise<void> {
165+
const admin = getAdminClient();
166+
167+
const query = admin
168+
.from("profiles")
169+
.select("id")
170+
.eq("display_name", displayName);
171+
172+
const { data: staleProfiles } = excludeUserId
173+
? await query.neq("id", excludeUserId)
174+
: await query;
175+
176+
if (!staleProfiles || staleProfiles.length === 0) return;
177+
178+
for (const profile of staleProfiles) {
179+
await deleteTestUser(profile.id).catch((err) => {
180+
console.warn(`Failed to clean up stale test user ${profile.id}: ${err}`);
181+
});
182+
}
183+
}

e2e/members.spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
deleteTestUser,
55
getInviteToken,
66
cleanupInvitesForEmail,
7+
cleanupStaleTestUsers,
78
} from "./fixtures/supabase-admin";
89
import { createClient } from "@supabase/supabase-js";
910
import { type Browser, type Page } from "@playwright/test";
@@ -72,11 +73,21 @@ test.describe("Workspace member management", () => {
7273
let invitedUserId: string | undefined;
7374
let workspaceSlug: string;
7475

76+
// Remove stale test users from previous runs whose cleanup was interrupted.
77+
// Without this, a leftover member with display_name "E2E Member" causes
78+
// the re-invite test to find 2 matching elements (strict mode violation).
79+
test.beforeAll(async () => {
80+
await cleanupStaleTestUsers(INVITE_DISPLAY_NAME).catch(() => {});
81+
await cleanupInvitesForEmail(INVITE_EMAIL).catch(() => {});
82+
});
83+
7584
test.afterAll(async () => {
7685
await cleanupInvitesForEmail(INVITE_EMAIL).catch(() => {});
7786
if (invitedUserId) {
7887
await deleteTestUser(invitedUserId).catch(() => {});
7988
}
89+
// Fallback: clean up by display name in case invitedUserId was never set
90+
await cleanupStaleTestUsers(INVITE_DISPLAY_NAME).catch(() => {});
8091
});
8192

8293
test("owner can invite a user by email", async ({

src/components/members/member-list.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,18 @@ export function MemberList({
5555
onRemove,
5656
}: MemberListProps) {
5757
const [removingId, setRemovingId] = useState<string | null>(null);
58+
// Track which member's remove dialog is open so we can close it after the
59+
// async delete completes. AlertDialogAction is a plain Button in Base UI
60+
// and does not auto-close the dialog.
61+
const [openDialogMemberId, setOpenDialogMemberId] = useState<string | null>(
62+
null
63+
);
5864
const isAdmin = currentUserRole === "owner" || currentUserRole === "admin";
5965

6066
async function handleRemove(memberId: string) {
6167
setRemovingId(memberId);
6268
await onRemove(memberId);
69+
setOpenDialogMemberId(null);
6370
setRemovingId(null);
6471
}
6572

@@ -130,7 +137,12 @@ export function MemberList({
130137
{isAdmin && (
131138
<TableCell>
132139
{canRemove(member) && (
133-
<AlertDialog>
140+
<AlertDialog
141+
open={openDialogMemberId === member.id}
142+
onOpenChange={(open) =>
143+
setOpenDialogMemberId(open ? member.id : null)
144+
}
145+
>
134146
<AlertDialogTrigger
135147
render={
136148
<Button

0 commit comments

Comments
 (0)