11import { render , screen , waitFor , cleanup } from '@testing-library/preact' ;
22import { afterEach , beforeEach , describe , expect , it , vi , type Mock } from 'vitest' ;
33import { DeviceList } from '../Devices' ;
4- import { createRef } from 'preact' ;
4+ import { createRef , type RefObject } from 'preact' ;
55import { fetchClient , get_decrypted_secret } from '../../utils' ;
66import { Base64 } from 'js-base64' ;
77import sodium from 'libsodium-wrappers' ;
88import { showAlert } from '../../components/Alert' ;
99
1010describe ( 'Devices.tsx - DeviceList' , ( ) => {
11+ // Helper to safely access the component instance without using non-null assertions
12+ const getRef = ( ref : RefObject < DeviceList > ) : DeviceList => {
13+ const current = ref . current ;
14+ if ( ! current ) {
15+ throw new Error ( 'DeviceList ref not set' ) ;
16+ }
17+ return current ;
18+ } ;
19+
1120 beforeEach ( ( ) => {
1221 vi . clearAllMocks ( ) ;
1322 } ) ;
@@ -138,36 +147,36 @@ describe('Devices.tsx - DeviceList', () => {
138147 { id : 'a' , uid : 2 , name : 'Bravo' , status : 'Connected' , note : '' , port : 0 , valid : true , last_state_change : null , firmware_version : '1' } ,
139148 { id : 'b' , uid : 1 , name : 'Alpha' , status : 'Connected' , note : '' , port : 0 , valid : true , last_state_change : null , firmware_version : '1' } ,
140149 ] ;
141- ref . current ! . setState ( { devices, sortColumn : 'none' , sortSequence : 'asc' , showDeleteModal : false , showEditNoteModal : false , editNote : '' , editChargerIdx : 0 } ) ;
142- await waitFor ( ( ) => expect ( ref . current ! . state . devices . length ) . toBe ( 2 ) ) ;
150+ getRef ( ref ) . setState ( { devices, sortColumn : 'none' , sortSequence : 'asc' , showDeleteModal : false , showEditNoteModal : false , editNote : '' , editChargerIdx : 0 } ) ;
151+ await waitFor ( ( ) => expect ( getRef ( ref ) . state . devices . length ) . toBe ( 2 ) ) ;
143152
144153 // First click -> sort by name asc
145- ref . current ! . setSort ( 'name' ) ;
146- await waitFor ( ( ) => expect ( ref . current ! . state . sortColumn ) . toBe ( 'name' ) ) ;
147- expect ( ref . current ! . state . sortSequence ) . toBe ( 'asc' ) ;
148- expect ( ref . current ! . state . devices . map ( d => d . name ) ) . toEqual ( [ 'Alpha' , 'Bravo' ] ) ;
154+ getRef ( ref ) . setSort ( 'name' ) ;
155+ await waitFor ( ( ) => expect ( getRef ( ref ) . state . sortColumn ) . toBe ( 'name' ) ) ;
156+ expect ( getRef ( ref ) . state . sortSequence ) . toBe ( 'asc' ) ;
157+ expect ( getRef ( ref ) . state . devices . map ( d => d . name ) ) . toEqual ( [ 'Alpha' , 'Bravo' ] ) ;
149158
150159 // Second click -> name desc
151- ref . current ! . setSort ( 'name' ) ;
152- await waitFor ( ( ) => expect ( ref . current ! . state . sortSequence ) . toBe ( 'desc' ) ) ;
153- expect ( ref . current ! . state . devices . map ( d => d . name ) ) . toEqual ( [ 'Bravo' , 'Alpha' ] ) ;
160+ getRef ( ref ) . setSort ( 'name' ) ;
161+ await waitFor ( ( ) => expect ( getRef ( ref ) . state . sortSequence ) . toBe ( 'desc' ) ) ;
162+ expect ( getRef ( ref ) . state . devices . map ( d => d . name ) ) . toEqual ( [ 'Bravo' , 'Alpha' ] ) ;
154163
155164 // Third click -> none (defaults to name asc)
156- ref . current ! . setSort ( 'name' ) ;
157- await waitFor ( ( ) => expect ( ref . current ! . state . sortColumn ) . toBe ( 'none' ) ) ;
158- expect ( ref . current ! . state . devices . map ( d => d . name ) ) . toEqual ( [ 'Alpha' , 'Bravo' ] ) ;
165+ getRef ( ref ) . setSort ( 'name' ) ;
166+ await waitFor ( ( ) => expect ( getRef ( ref ) . state . sortColumn ) . toBe ( 'none' ) ) ;
167+ expect ( getRef ( ref ) . state . devices . map ( d => d . name ) ) . toEqual ( [ 'Alpha' , 'Bravo' ] ) ;
159168 } ) ;
160169
161170 it ( 'setMobileSort toggles between selected and none' , async ( ) => {
162171 const ref = createRef < DeviceList > ( ) ;
163172 render ( < DeviceList ref = { ref } /> ) ;
164- ref . current ! . setState ( { devices : [ ] , sortColumn : 'none' , sortSequence : 'asc' , showDeleteModal : false , showEditNoteModal : false , editNote : '' , editChargerIdx : 0 } ) ;
173+ getRef ( ref ) . setState ( { devices : [ ] , sortColumn : 'none' , sortSequence : 'asc' , showDeleteModal : false , showEditNoteModal : false , editNote : '' , editChargerIdx : 0 } ) ;
165174
166- ref . current ! . setMobileSort ( 'uid' ) ;
167- await waitFor ( ( ) => expect ( ref . current ! . state . sortColumn ) . toBe ( 'uid' ) ) ;
175+ getRef ( ref ) . setMobileSort ( 'uid' ) ;
176+ await waitFor ( ( ) => expect ( getRef ( ref ) . state . sortColumn ) . toBe ( 'uid' ) ) ;
168177
169- ref . current ! . setMobileSort ( 'uid' ) ;
170- await waitFor ( ( ) => expect ( ref . current ! . state . sortColumn ) . toBe ( 'none' ) ) ;
178+ getRef ( ref ) . setMobileSort ( 'uid' ) ;
179+ await waitFor ( ( ) => expect ( getRef ( ref ) . state . sortColumn ) . toBe ( 'none' ) ) ;
171180 } ) ;
172181
173182 it ( 'handleDelete and handleDeleteConfirm remove device on success' , async ( ) => {
@@ -179,15 +188,15 @@ describe('Devices.tsx - DeviceList', () => {
179188 { id : 'x' , uid : 10 , name : 'X' , status : 'Connected' , note : '' , port : 0 , valid : true , last_state_change : null , firmware_version : '1' } ,
180189 { id : 'y' , uid : 11 , name : 'Y' , status : 'Connected' , note : '' , port : 0 , valid : true , last_state_change : null , firmware_version : '1' } ,
181190 ] ;
182- ref . current ! . setState ( { devices, sortColumn : 'none' , sortSequence : 'asc' , showDeleteModal : false , showEditNoteModal : false , editNote : '' , editChargerIdx : 0 } ) ;
191+ getRef ( ref ) . setState ( { devices, sortColumn : 'none' , sortSequence : 'asc' , showDeleteModal : false , showEditNoteModal : false , editNote : '' , editChargerIdx : 0 } ) ;
183192
184- ref . current ! . handleDelete ( devices [ 0 ] ) ;
185- await waitFor ( ( ) => expect ( ref . current ! . state . showDeleteModal ) . toBe ( true ) ) ;
193+ getRef ( ref ) . handleDelete ( devices [ 0 ] ) ;
194+ await waitFor ( ( ) => expect ( getRef ( ref ) . state . showDeleteModal ) . toBe ( true ) ) ;
186195
187196 ( fetchClient . DELETE as unknown as Mock ) . mockResolvedValue ( { response : { status : 200 } } ) ;
188- await ref . current ! . handleDeleteConfirm ( ) ;
189- await waitFor ( ( ) => expect ( ref . current ! . state . showDeleteModal ) . toBe ( false ) ) ;
190- expect ( ref . current ! . state . devices . map ( d => d . id ) ) . toEqual ( [ 'y' ] ) ;
197+ await getRef ( ref ) . handleDeleteConfirm ( ) ;
198+ await waitFor ( ( ) => expect ( getRef ( ref ) . state . showDeleteModal ) . toBe ( false ) ) ;
199+ expect ( getRef ( ref ) . state . devices . map ( d => d . id ) ) . toEqual ( [ 'y' ] ) ;
191200 } ) ;
192201
193202 it ( 'handleEditNote flows: submit updates note and cancel resets' , async ( ) => {
@@ -198,26 +207,26 @@ describe('Devices.tsx - DeviceList', () => {
198207 const devices = [
199208 { id : 'z' , uid : 5 , name : 'Z' , status : 'Connected' , note : 'old' , port : 0 , valid : true , last_state_change : null , firmware_version : '1' } ,
200209 ] ;
201- ref . current ! . setState ( { devices, sortColumn : 'none' , sortSequence : 'asc' , showDeleteModal : false , showEditNoteModal : false , editNote : '' , editChargerIdx : 0 } ) ;
202- await waitFor ( ( ) => expect ( ref . current ! . state . devices . length ) . toBe ( 1 ) ) ;
210+ getRef ( ref ) . setState ( { devices, sortColumn : 'none' , sortSequence : 'asc' , showDeleteModal : false , showEditNoteModal : false , editNote : '' , editChargerIdx : 0 } ) ;
211+ await waitFor ( ( ) => expect ( getRef ( ref ) . state . devices . length ) . toBe ( 1 ) ) ;
203212
204- ref . current ! . handleEditNote ( devices [ 0 ] , 0 ) ;
205- await waitFor ( ( ) => expect ( ref . current ! . state . showEditNoteModal ) . toBe ( true ) ) ;
213+ getRef ( ref ) . handleEditNote ( devices [ 0 ] , 0 ) ;
214+ await waitFor ( ( ) => expect ( getRef ( ref ) . state . showEditNoteModal ) . toBe ( true ) ) ;
206215
207216 ( sodium . crypto_box_seal as unknown as Mock ) . mockReturnValue ( new Uint8Array ( [ 9 , 9 ] ) ) ;
208217 ( fetchClient . POST as unknown as Mock ) . mockResolvedValue ( { error : undefined } ) ;
209218 const evt = { preventDefault : vi . fn ( ) } as unknown as Event ;
210- ref . current ! . setState ( { editNote : 'new' , editChargerIdx : 0 } ) ;
211- await ref . current ! . handleEditNoteSubmit ( evt ) ;
212- await waitFor ( ( ) => expect ( ref . current ! . state . devices [ 0 ] . note ) . toBe ( 'new' ) ) ;
213- expect ( ref . current ! . state . showEditNoteModal ) . toBe ( false ) ;
214-
215- ref . current ! . handleEditNote ( devices [ 0 ] , 0 ) ;
216- await waitFor ( ( ) => expect ( ref . current ! . state . showEditNoteModal ) . toBe ( true ) ) ;
217- ref . current ! . handleEditNoteCancel ( ) ;
218- await waitFor ( ( ) => expect ( ref . current ! . state . showEditNoteModal ) . toBe ( false ) ) ;
219- expect ( ref . current ! . state . editNote ) . toBe ( '' ) ;
220- expect ( ref . current ! . state . editChargerIdx ) . toBe ( - 1 ) ;
219+ getRef ( ref ) . setState ( { editNote : 'new' , editChargerIdx : 0 } ) ;
220+ await getRef ( ref ) . handleEditNoteSubmit ( evt ) ;
221+ await waitFor ( ( ) => expect ( getRef ( ref ) . state . devices [ 0 ] . note ) . toBe ( 'new' ) ) ;
222+ expect ( getRef ( ref ) . state . showEditNoteModal ) . toBe ( false ) ;
223+
224+ getRef ( ref ) . handleEditNote ( devices [ 0 ] , 0 ) ;
225+ await waitFor ( ( ) => expect ( getRef ( ref ) . state . showEditNoteModal ) . toBe ( true ) ) ;
226+ getRef ( ref ) . handleEditNoteCancel ( ) ;
227+ await waitFor ( ( ) => expect ( getRef ( ref ) . state . showEditNoteModal ) . toBe ( false ) ) ;
228+ expect ( getRef ( ref ) . state . editNote ) . toBe ( '' ) ;
229+ expect ( getRef ( ref ) . state . editChargerIdx ) . toBe ( - 1 ) ;
221230 } ) ;
222231
223232 it ( 'handleEditNoteSubmit shows alert on error' , async ( ) => {
@@ -228,13 +237,13 @@ describe('Devices.tsx - DeviceList', () => {
228237 const devices = [
229238 { id : 'n1' , uid : 1 , name : 'Name' , status : 'Connected' , note : 'old' , port : 0 , valid : true , last_state_change : null , firmware_version : '1' } ,
230239 ] ;
231- ref . current ! . setState ( { devices, sortColumn : 'none' , sortSequence : 'asc' , showDeleteModal : false , showEditNoteModal : true , editNote : 'upd' , editChargerIdx : 0 } ) ;
232- await waitFor ( ( ) => expect ( ref . current ! . state . devices . length ) . toBe ( 1 ) ) ;
233- await waitFor ( ( ) => expect ( ref . current ! . state . showEditNoteModal ) . toBe ( true ) ) ;
240+ getRef ( ref ) . setState ( { devices, sortColumn : 'none' , sortSequence : 'asc' , showDeleteModal : false , showEditNoteModal : true , editNote : 'upd' , editChargerIdx : 0 } ) ;
241+ await waitFor ( ( ) => expect ( getRef ( ref ) . state . devices . length ) . toBe ( 1 ) ) ;
242+ await waitFor ( ( ) => expect ( getRef ( ref ) . state . showEditNoteModal ) . toBe ( true ) ) ;
234243 ( sodium . crypto_box_seal as unknown as Mock ) . mockReturnValue ( new Uint8Array ( [ 1 ] ) ) ;
235244 ( fetchClient . POST as unknown as Mock ) . mockResolvedValue ( { error : 'err' } ) ;
236245 const evt = { preventDefault : vi . fn ( ) } as unknown as Event ;
237- await ref . current ! . handleEditNoteSubmit ( evt ) ;
246+ await getRef ( ref ) . handleEditNoteSubmit ( evt ) ;
238247 await waitFor ( ( ) => expect ( ( showAlert as unknown as Mock ) ) . toHaveBeenCalled ( ) ) ;
239248 } ) ;
240249
@@ -246,12 +255,12 @@ describe('Devices.tsx - DeviceList', () => {
246255 const devices = [
247256 { id : 'u' , uid : 7 , name : 'U' , status : 'Connected' , note : '' , port : 0 , valid : true , last_state_change : null , firmware_version : '1' } ,
248257 ] ;
249- ref . current ! . setState ( { devices, sortColumn : 'none' , sortSequence : 'asc' , showDeleteModal : false , showEditNoteModal : false , editNote : '' , editChargerIdx : 0 } ) ;
250- await waitFor ( ( ) => expect ( ref . current ! . state . devices . length ) . toBe ( 1 ) ) ;
258+ getRef ( ref ) . setState ( { devices, sortColumn : 'none' , sortSequence : 'asc' , showDeleteModal : false , showEditNoteModal : false , editNote : '' , editChargerIdx : 0 } ) ;
259+ await waitFor ( ( ) => expect ( getRef ( ref ) . state . devices . length ) . toBe ( 1 ) ) ;
251260
252261 ( fetchClient . GET as unknown as Mock ) . mockImplementation ( ( ) => { throw new Error ( 'Network fail' ) ; } ) ;
253- await ref . current ! . updateChargers ( ) ;
254- expect ( ref . current ! . state . devices [ 0 ] . status ) . toBe ( 'Disconnected' ) ;
262+ await getRef ( ref ) . updateChargers ( ) ;
263+ expect ( getRef ( ref ) . state . devices [ 0 ] . status ) . toBe ( 'Disconnected' ) ;
255264 } ) ;
256265
257266 it ( 'updateChargers marks device invalid when decryption fails' , async ( ) => {
@@ -264,17 +273,17 @@ describe('Devices.tsx - DeviceList', () => {
264273 error : undefined ,
265274 response : { status : 200 } ,
266275 } ) ;
267- await ref . current ! . updateChargers ( ) ;
268- expect ( ref . current ! . state . devices [ 0 ] . valid ) . toBe ( false ) ;
269- expect ( ref . current ! . state . devices [ 0 ] . name ) . toBe ( '' ) ;
270- expect ( typeof ref . current ! . state . devices [ 0 ] . note ) . toBe ( 'string' ) ;
276+ await getRef ( ref ) . updateChargers ( ) ;
277+ expect ( getRef ( ref ) . state . devices [ 0 ] . valid ) . toBe ( false ) ;
278+ expect ( getRef ( ref ) . state . devices [ 0 ] . name ) . toBe ( '' ) ;
279+ expect ( typeof getRef ( ref ) . state . devices [ 0 ] . note ) . toBe ( 'string' ) ;
271280 } ) ;
272281
273282 it ( 'componentWillUnmount clears the interval' , ( ) => {
274283 const ref = createRef < DeviceList > ( ) ;
275284 render ( < DeviceList ref = { ref } /> ) ;
276285 const spy = vi . spyOn ( global , 'clearInterval' ) ;
277- ref . current ! . componentWillUnmount ( ) ;
286+ getRef ( ref ) . componentWillUnmount ( ) ;
278287 expect ( spy ) . toHaveBeenCalled ( ) ;
279288 } ) ;
280289} ) ;
0 commit comments