@@ -206,6 +206,232 @@ test.describe('Streaming Settings', () => {
206206 } )
207207} )
208208
209+ test . describe ( 'IOT Settings' , ( ) => {
210+ test . beforeEach ( async ( { page } ) => {
211+ await page . goto ( '/' )
212+ await login ( page )
213+ await navigateToAppSettings ( page )
214+ await page . waitForSelector ( 'text=OMDB Settings' )
215+
216+ // Navigate to IOT settings
217+ const iotMenuItem = page . getByText ( 'Device Availability' )
218+ await iotMenuItem . click ( )
219+ await page . waitForSelector ( 'text=📡 IOT Device Availability' )
220+ } )
221+
222+ test ( 'should display IOT settings form' , async ( { page } ) => {
223+ // Verify IOT settings heading (using text selector to pierce shadow DOM)
224+ await expect ( page . locator ( 'text=📡 IOT Device Availability' ) . first ( ) ) . toBeVisible ( )
225+
226+ const iotPage = page . locator ( 'iot-settings-page' )
227+
228+ // Verify form fields are present
229+ const pingIntervalInput = iotPage . locator ( 'input[name="pingIntervalMs"]' )
230+ await expect ( pingIntervalInput ) . toBeVisible ( )
231+
232+ const pingTimeoutInput = iotPage . locator ( 'input[name="pingTimeoutMs"]' )
233+ await expect ( pingTimeoutInput ) . toBeVisible ( )
234+
235+ const saveButton = iotPage . getByRole ( 'button' , { name : / s a v e s e t t i n g s / i } )
236+ await expect ( saveButton ) . toBeVisible ( )
237+ } )
238+
239+ test ( 'should validate ping interval input constraints' , async ( { page } ) => {
240+ const iotPage = page . locator ( 'iot-settings-page' )
241+ const pingIntervalInput = iotPage . locator ( 'input[name="pingIntervalMs"]' )
242+
243+ // Verify min/max attributes
244+ await expect ( pingIntervalInput ) . toHaveAttribute ( 'min' , '1000' )
245+ await expect ( pingIntervalInput ) . toHaveAttribute ( 'max' , '3600000' )
246+ await expect ( pingIntervalInput ) . toHaveAttribute ( 'type' , 'number' )
247+ } )
248+
249+ test ( 'should validate ping timeout input constraints' , async ( { page } ) => {
250+ const iotPage = page . locator ( 'iot-settings-page' )
251+ const pingTimeoutInput = iotPage . locator ( 'input[name="pingTimeoutMs"]' )
252+
253+ // Verify min/max attributes
254+ await expect ( pingTimeoutInput ) . toHaveAttribute ( 'min' , '100' )
255+ await expect ( pingTimeoutInput ) . toHaveAttribute ( 'max' , '60000' )
256+ await expect ( pingTimeoutInput ) . toHaveAttribute ( 'type' , 'number' )
257+ } )
258+
259+ test ( 'should save IOT settings successfully' , async ( { page } ) => {
260+ const iotPage = page . locator ( 'iot-settings-page' )
261+ const pingIntervalInput = iotPage . locator ( 'input[name="pingIntervalMs"]' )
262+ const pingTimeoutInput = iotPage . locator ( 'input[name="pingTimeoutMs"]' )
263+
264+ // Set valid values
265+ await pingIntervalInput . fill ( '60000' )
266+ await pingTimeoutInput . fill ( '5000' )
267+
268+ // Submit the form
269+ const saveButton = iotPage . getByRole ( 'button' , { name : / s a v e s e t t i n g s / i } )
270+ await saveButton . click ( )
271+
272+ // Verify success notification
273+ await assertAndDismissNoty ( page , 'IOT settings saved successfully' )
274+ } )
275+
276+ test ( 'should persist IOT settings after save' , async ( { page } ) => {
277+ const iotPage = page . locator ( 'iot-settings-page' )
278+ const pingIntervalInput = iotPage . locator ( 'input[name="pingIntervalMs"]' )
279+ const pingTimeoutInput = iotPage . locator ( 'input[name="pingTimeoutMs"]' )
280+
281+ // Set specific values
282+ await pingIntervalInput . fill ( '45000' )
283+ await pingTimeoutInput . fill ( '4500' )
284+
285+ // Submit the form
286+ const saveButton = iotPage . getByRole ( 'button' , { name : / s a v e s e t t i n g s / i } )
287+ await saveButton . click ( )
288+ await assertAndDismissNoty ( page , 'IOT settings saved successfully' )
289+
290+ // Navigate away and back
291+ await page . goto ( '/' )
292+ await page . waitForSelector ( 'text=Apps' )
293+
294+ // Navigate back to IOT settings
295+ await navigateToAppSettings ( page )
296+ await page . waitForSelector ( 'text=OMDB Settings' )
297+
298+ const iotMenuItemAfter = page . getByText ( 'Device Availability' )
299+ await iotMenuItemAfter . click ( )
300+ await page . waitForSelector ( 'text=📡 IOT Device Availability' )
301+
302+ // Verify the values are persisted
303+ const pingIntervalInputAfter = page . locator ( 'iot-settings-page' ) . locator ( 'input[name="pingIntervalMs"]' )
304+ const pingTimeoutInputAfter = page . locator ( 'iot-settings-page' ) . locator ( 'input[name="pingTimeoutMs"]' )
305+ await expect ( pingIntervalInputAfter ) . toHaveValue ( '45000' )
306+ await expect ( pingTimeoutInputAfter ) . toHaveValue ( '4500' )
307+ } )
308+
309+ test ( 'should show validation error when timeout is greater than interval' , async ( { page } ) => {
310+ const iotPage = page . locator ( 'iot-settings-page' )
311+ const pingIntervalInput = iotPage . locator ( 'input[name="pingIntervalMs"]' )
312+ const pingTimeoutInput = iotPage . locator ( 'input[name="pingTimeoutMs"]' )
313+
314+ // Set invalid values (timeout >= interval)
315+ await pingIntervalInput . fill ( '5000' )
316+ await pingTimeoutInput . fill ( '5000' )
317+
318+ // Submit the form
319+ const saveButton = iotPage . getByRole ( 'button' , { name : / s a v e s e t t i n g s / i } )
320+ await saveButton . click ( )
321+
322+ // Verify validation error is shown
323+ const validationError = iotPage . locator ( '[data-testid="validation-error"]' )
324+ await expect ( validationError ) . toBeVisible ( )
325+ await expect ( validationError ) . toContainText ( 'Ping timeout must be less than ping interval' )
326+ } )
327+ } )
328+
329+ test . describe ( 'AI Settings' , ( ) => {
330+ test . beforeEach ( async ( { page } ) => {
331+ await page . goto ( '/' )
332+ await login ( page )
333+ await navigateToAppSettings ( page )
334+ await page . waitForSelector ( 'text=OMDB Settings' )
335+
336+ // Navigate to AI settings
337+ const aiMenuItem = page . getByText ( 'Ollama Settings' )
338+ await aiMenuItem . click ( )
339+ await page . waitForSelector ( 'text=🤖 Ollama Integration' )
340+ } )
341+
342+ test ( 'should display AI settings form' , async ( { page } ) => {
343+ // Verify AI settings heading (using text selector to pierce shadow DOM)
344+ await expect ( page . locator ( 'text=🤖 Ollama Integration' ) . first ( ) ) . toBeVisible ( )
345+
346+ const aiPage = page . locator ( 'ai-settings-page' )
347+
348+ // Verify form fields are present
349+ const hostInput = aiPage . locator ( 'input[name="host"]' )
350+ await expect ( hostInput ) . toBeVisible ( )
351+ await expect ( hostInput ) . toHaveAttribute ( 'type' , 'url' )
352+
353+ const saveButton = aiPage . getByRole ( 'button' , { name : / s a v e s e t t i n g s / i } )
354+ await expect ( saveButton ) . toBeVisible ( )
355+ } )
356+
357+ test ( 'should save AI settings successfully with valid URL' , async ( { page } ) => {
358+ const aiPage = page . locator ( 'ai-settings-page' )
359+ const hostInput = aiPage . locator ( 'input[name="host"]' )
360+
361+ // Set a valid URL
362+ await hostInput . fill ( 'http://localhost:11434' )
363+
364+ // Submit the form
365+ const saveButton = aiPage . getByRole ( 'button' , { name : / s a v e s e t t i n g s / i } )
366+ await saveButton . click ( )
367+
368+ // Verify success notification
369+ await assertAndDismissNoty ( page , 'AI settings saved successfully' )
370+ } )
371+
372+ test ( 'should save AI settings successfully with empty URL (disable AI)' , async ( { page } ) => {
373+ const aiPage = page . locator ( 'ai-settings-page' )
374+ const hostInput = aiPage . locator ( 'input[name="host"]' )
375+
376+ // Clear the URL to disable AI features
377+ await hostInput . fill ( '' )
378+
379+ // Submit the form
380+ const saveButton = aiPage . getByRole ( 'button' , { name : / s a v e s e t t i n g s / i } )
381+ await saveButton . click ( )
382+
383+ // Verify success notification
384+ await assertAndDismissNoty ( page , 'AI settings saved successfully' )
385+ } )
386+
387+ test ( 'should persist AI settings after save' , async ( { page } ) => {
388+ const aiPage = page . locator ( 'ai-settings-page' )
389+ const hostInput = aiPage . locator ( 'input[name="host"]' )
390+
391+ // Set a specific URL
392+ const testUrl = 'http://my-ollama-server:8080'
393+ await hostInput . fill ( testUrl )
394+
395+ // Submit the form
396+ const saveButton = aiPage . getByRole ( 'button' , { name : / s a v e s e t t i n g s / i } )
397+ await saveButton . click ( )
398+ await assertAndDismissNoty ( page , 'AI settings saved successfully' )
399+
400+ // Navigate away and back
401+ await page . goto ( '/' )
402+ await page . waitForSelector ( 'text=Apps' )
403+
404+ // Navigate back to AI settings
405+ await navigateToAppSettings ( page )
406+ await page . waitForSelector ( 'text=OMDB Settings' )
407+
408+ const aiMenuItemAfter = page . getByText ( 'Ollama Settings' )
409+ await aiMenuItemAfter . click ( )
410+ await page . waitForSelector ( 'text=🤖 Ollama Integration' )
411+
412+ // Verify the value is persisted
413+ const hostInputAfter = page . locator ( 'ai-settings-page' ) . locator ( 'input[name="host"]' )
414+ await expect ( hostInputAfter ) . toHaveValue ( testUrl )
415+ } )
416+
417+ test ( 'should show validation error for invalid URL' , async ( { page } ) => {
418+ const aiPage = page . locator ( 'ai-settings-page' )
419+ const hostInput = aiPage . locator ( 'input[name="host"]' )
420+
421+ // Set an invalid URL
422+ await hostInput . fill ( 'not-a-valid-url' )
423+
424+ // Submit the form
425+ const saveButton = aiPage . getByRole ( 'button' , { name : / s a v e s e t t i n g s / i } )
426+ await saveButton . click ( )
427+
428+ // Verify validation error is shown
429+ const validationError = aiPage . locator ( '[data-testid="validation-error"]' )
430+ await expect ( validationError ) . toBeVisible ( )
431+ await expect ( validationError ) . toContainText ( 'Please enter a valid URL' )
432+ } )
433+ } )
434+
209435test . describe ( 'Settings Navigation' , ( ) => {
210436 test . beforeEach ( async ( { page } ) => {
211437 await page . goto ( '/' )
@@ -234,6 +460,29 @@ test.describe('Settings Navigation', () => {
234460 await expect ( page . locator ( 'text=🎬 OMDB Settings' ) . first ( ) ) . toBeVisible ( )
235461 } )
236462
463+ test ( 'should navigate to all settings sections' , async ( { page } ) => {
464+ await navigateToAppSettings ( page )
465+ await page . waitForSelector ( 'text=OMDB Settings' )
466+
467+ // Navigate to IOT settings
468+ const iotMenuItem = page . getByText ( 'Device Availability' )
469+ await iotMenuItem . click ( )
470+ await expect ( page ) . toHaveURL ( / \/ a p p - s e t t i n g s \/ i o t / )
471+ await expect ( page . locator ( 'text=📡 IOT Device Availability' ) . first ( ) ) . toBeVisible ( )
472+
473+ // Navigate to AI settings
474+ const aiMenuItem = page . getByText ( 'Ollama Settings' )
475+ await aiMenuItem . click ( )
476+ await expect ( page ) . toHaveURL ( / \/ a p p - s e t t i n g s \/ a i / )
477+ await expect ( page . locator ( 'text=🤖 Ollama Integration' ) . first ( ) ) . toBeVisible ( )
478+
479+ // Navigate back to OMDB
480+ const omdbMenuItem = page . getByText ( 'OMDB Settings' )
481+ await omdbMenuItem . click ( )
482+ await expect ( page ) . toHaveURL ( / \/ a p p - s e t t i n g s \/ o m d b / )
483+ await expect ( page . locator ( 'text=🎬 OMDB Settings' ) . first ( ) ) . toBeVisible ( )
484+ } )
485+
237486 test ( 'should highlight active menu item' , async ( { page } ) => {
238487 await navigateToAppSettings ( page )
239488 await page . waitForSelector ( 'text=OMDB Settings' )
0 commit comments