@@ -1072,6 +1072,47 @@ describe("E2E: RoomError rejoin policy", () => {
10721072 } , 8000 ) ;
10731073} ) ;
10741074
1075+ describe ( "React strict-mode: join + immediate close" , ( ) => {
1076+ let server : SimpleServer ;
1077+ let port : number ;
1078+
1079+ beforeAll ( async ( ) => {
1080+ port = await getPort ( ) ;
1081+ server = new SimpleServer ( { port } ) ;
1082+ await server . start ( ) ;
1083+ } ) ;
1084+
1085+ afterAll ( async ( ) => {
1086+ await server . stop ( ) ;
1087+ } , 15000 ) ;
1088+
1089+ it ( "close() before auth microtask settles does not resurrect the client" , async ( ) => {
1090+ const statuses : ClientStatus [ ] = [ ] ;
1091+ const client = new LoroWebsocketClient ( {
1092+ url : `ws://localhost:${ port } ` ,
1093+ } ) ;
1094+ await client . waitConnected ( ) ;
1095+
1096+ client . onStatusChange ( s => statuses . push ( s ) ) ;
1097+
1098+ // Simulate React strict-mode: effect fires join(), cleanup fires close()
1099+ const adaptor = new LoroAdaptor ( ) ;
1100+ client . join ( { roomId : "strict-mode-zombie" , crdtAdaptor : adaptor } ) ;
1101+ client . close ( ) ; // shouldReconnect = false
1102+
1103+ // Let the resolveAuth().then(sendJoinPayload) microtask fire
1104+ await new Promise ( r => setTimeout ( r , 500 ) ) ;
1105+
1106+ // The client must stay dead — no Connecting/Connected after Disconnected
1107+ expect ( client . getStatus ( ) ) . toBe ( ClientStatus . Disconnected ) ;
1108+ expect (
1109+ statuses . filter ( s => s === ClientStatus . Connecting )
1110+ ) . toHaveLength ( 0 ) ;
1111+
1112+ client . destroy ( ) ;
1113+ } , 5000 ) ;
1114+ } ) ;
1115+
10751116function installMockWindow ( initialOnline = true ) {
10761117 const originalWindowDescriptor = Object . getOwnPropertyDescriptor (
10771118 globalThis ,
0 commit comments