File tree Expand file tree Collapse file tree
Expand file tree Collapse file tree Original file line number Diff line number Diff 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+ }
Original file line number Diff line number Diff line change 44 deleteTestUser ,
55 getInviteToken ,
66 cleanupInvitesForEmail ,
7+ cleanupStaleTestUsers ,
78} from "./fixtures/supabase-admin" ;
89import { createClient } from "@supabase/supabase-js" ;
910import { 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 ( {
Original file line number Diff line number Diff 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
You can’t perform that action at this time.
0 commit comments