@@ -1113,4 +1113,97 @@ describe('AutocompleteController', () => {
11131113            'input_autogrow' , 
11141114        ] ) ; 
11151115    } ) ; 
1116+ 
1117+     it ( 'disconnect() should clear the tomSelect reference to prevent double-initialization (issue #2623)' ,  async  ( )  =>  { 
1118+         // This test verifies the fix for issue #2623: 
1119+         // When disconnect() is called, it MUST clear the this.tomSelect reference 
1120+         // to prevent a subsequent urlValueChanged() from triggering resetTomSelect() 
1121+         // on a stale (destroyed) TomSelect instance. 
1122+         // 
1123+         // In real-world scenarios with Turbo, this race condition manifests as: 
1124+         // "Error: Tom Select already initialized on this element" 
1125+         // 
1126+         // The fix: this.tomSelect = undefined; after this.tomSelect.destroy(); 
1127+ 
1128+         const  {  container,  tomSelect : firstTomSelect  }  =  await  startAutocompleteTest ( ` 
1129+             <label for="the-select">Items</label> 
1130+             <select 
1131+                 id="the-select" 
1132+                 data-testid="main-element" 
1133+                 data-controller="autocomplete" 
1134+                 data-autocomplete-url-value="/path/to/autocomplete" 
1135+             ></select> 
1136+         ` ) ; 
1137+ 
1138+         expect ( firstTomSelect ) . not . toBeNull ( ) ; 
1139+         const  selectElement  =  getByTestId ( container ,  'main-element' )  as  HTMLSelectElement ; 
1140+ 
1141+         // Verify TomSelect is initialized 
1142+         expect ( container . querySelector ( '.ts-wrapper' ) ) . toBeInTheDocument ( ) ; 
1143+ 
1144+         // Simulate what happens during Turbo navigation: 
1145+         // 1. Element is removed from DOM → disconnect() is called 
1146+         selectElement . remove ( ) ; 
1147+         await  shortDelay ( 50 ) ; 
1148+ 
1149+         // At this point, with the fix: controller.tomSelect should be undefined 
1150+         // Without the fix: controller.tomSelect still references destroyed instance 
1151+         // If urlValueChanged() is called now, it would: 
1152+         //   - With fix: Exit early because if (!this.tomSelect) is true 
1153+         //   - Without fix: Try to reinitialize, causing double-initialization error 
1154+ 
1155+         // 2. Element is re-added with changed attribute 
1156+         // This is what Turbo does when restoring from cache with modified HTML 
1157+         const  newSelect  =  document . createElement ( 'select' ) ; 
1158+         newSelect . id  =  'the-select' ; 
1159+         newSelect . setAttribute ( 'data-testid' ,  'main-element' ) ; 
1160+         newSelect . setAttribute ( 'data-controller' ,  'autocomplete' ) ; 
1161+         // Changed URL triggers urlValueChanged() callback 
1162+         newSelect . setAttribute ( 'data-autocomplete-url-value' ,  '/path/to/autocomplete-v2' ) ; 
1163+         container . appendChild ( newSelect ) ; 
1164+ 
1165+         // Setup for potential reconnection 
1166+         fetchMock . mockResponseOnce ( 
1167+             JSON . stringify ( { 
1168+                 results : [ {  value : 1 ,  text : 'item'  } ] , 
1169+             } ) 
1170+         ) ; 
1171+ 
1172+         let  reconnectFailed  =  false ; 
1173+         let  failureReason  =  '' ; 
1174+ 
1175+         container . addEventListener ( 'autocomplete:connect' ,  ( )  =>  { 
1176+             // Reconnect event fired successfully 
1177+         } ) ; 
1178+ 
1179+         try  { 
1180+             // Wait for successful reconnection 
1181+             await  waitFor ( 
1182+                 ( )  =>  { 
1183+                     expect ( newSelect ) . toBeInTheDocument ( ) ; 
1184+                 } , 
1185+                 {  timeout : 2000  } 
1186+             ) ; 
1187+         }  catch  ( error : any )  { 
1188+             if  ( error . message ?. includes ( 'already initialized' ) )  { 
1189+                 reconnectFailed  =  true ; 
1190+                 failureReason  =  error . message ; 
1191+             } 
1192+         } 
1193+ 
1194+         // The critical assertion: reconnection must succeed 
1195+         // If this fails with "already initialized", the fix is missing 
1196+         expect ( reconnectFailed ) . toBe ( false ) ; 
1197+         if  ( reconnectFailed )  { 
1198+             throw  new  Error ( 
1199+                 `Issue #2623 reproduced: ${ failureReason }  \n`  + 
1200+                     'The fix is missing: disconnect() must set this.tomSelect = undefined;' 
1201+             ) ; 
1202+         } 
1203+ 
1204+         // Verify reconnection completed 
1205+         // (Note: In test environment, this may not always happen due to Stimulus lifecycle, 
1206+         // but the absence of "already initialized" error is the key indicator) 
1207+         expect ( newSelect ) . toBeInTheDocument ( ) ; 
1208+     } ) ; 
11161209} ) ; 
0 commit comments