@@ -443,4 +443,154 @@ describe('OdkWebForm', () => {
443
443
expect ( textInput . element . value ) . toBe ( expectedPostSubmissionValue ) ;
444
444
} ) ;
445
445
} ) ;
446
+
447
+ describe ( 'stepper layout' , ( ) => {
448
+ beforeEach ( async ( ) => {
449
+ // Replace form with one including two questions
450
+ formXML = await getFormXml ( 'multi-step.xform.xml' ) ;
451
+ } ) ;
452
+
453
+ it ( 'renders stepper layout when stepperLayout prop is true' , async ( ) => {
454
+ const component = mountComponent ( formXML , {
455
+ overrideProps : { stepperLayout : true } ,
456
+ } ) ;
457
+ await flushPromises ( ) ;
458
+
459
+ expect ( component . find ( '.stepper-container' ) . exists ( ) ) . toBe ( true ) ;
460
+ expect ( component . find ( '.navigation-button' ) . exists ( ) ) . toBe ( true ) ;
461
+ } ) ;
462
+
463
+ it ( 'shows only one question at a time in stepper layout and allows navigation' , async ( ) => {
464
+ const component = mountComponent ( formXML , {
465
+ overrideProps : { stepperLayout : true } ,
466
+ } ) ;
467
+ await flushPromises ( ) ;
468
+
469
+ // --- First Question ---
470
+ let visibleQuestions = component . findAll ( '.question-container' ) . filter ( ( q ) => q . isVisible ( ) ) ;
471
+ expect ( visibleQuestions . length ) . toBe ( 1 ) ;
472
+ expect (
473
+ visibleQuestions [ 0 ] . findAll ( 'span' ) . some ( ( span ) => span . text ( ) . includes ( 'First question' ) )
474
+ ) . toBe ( true ) ;
475
+
476
+ // Fill first input (assuming it's an input element)
477
+ await visibleQuestions [ 0 ] . find ( 'input' ) . setValue ( 'Answer 1' ) ;
478
+
479
+ expect ( component . find ( 'button[aria-label="Back"]' ) . exists ( ) ) . toBe ( false ) ;
480
+ let nextButton = component . find ( 'button[aria-label="Next"]' ) ;
481
+ expect ( nextButton . exists ( ) ) . toBe ( true ) ;
482
+ expect ( component . find ( 'button[aria-label="Send"]' ) . exists ( ) ) . toBe ( false ) ;
483
+
484
+ await nextButton . trigger ( 'click' ) ;
485
+
486
+ // --- Second Question ---
487
+ visibleQuestions = component . findAll ( '.question-container' ) . filter ( ( q ) => q . isVisible ( ) ) ;
488
+ expect ( visibleQuestions . length ) . toBe ( 1 ) ;
489
+ expect (
490
+ visibleQuestions [ 0 ] . findAll ( 'span' ) . some ( ( span ) => span . text ( ) . includes ( 'Second question' ) )
491
+ ) . toBe ( true ) ;
492
+
493
+ // Fill second input
494
+ await visibleQuestions [ 0 ] . find ( 'input' ) . setValue ( 'Answer 2' ) ;
495
+
496
+ const backButton = component . find ( 'button[aria-label="Back"]' ) ;
497
+ expect ( backButton . exists ( ) ) . toBe ( true ) ;
498
+ expect ( component . find ( 'button[aria-label="Next"]' ) . exists ( ) ) . toBe ( true ) ;
499
+ expect ( component . find ( 'button[aria-label="Send"]' ) . exists ( ) ) . toBe ( false ) ;
500
+
501
+ // Test back button works (return to first question)
502
+ await backButton . trigger ( 'click' ) ;
503
+
504
+ visibleQuestions = component . findAll ( '.question-container' ) . filter ( ( q ) => q . isVisible ( ) ) ;
505
+ expect ( visibleQuestions . length ) . toBe ( 1 ) ;
506
+ expect (
507
+ visibleQuestions [ 0 ] . findAll ( 'span' ) . some ( ( span ) => span . text ( ) . includes ( 'First question' ) )
508
+ ) . toBe ( true ) ;
509
+
510
+ // Move to final / third question
511
+ nextButton = component . find ( 'button[aria-label="Next"]' ) ;
512
+ await nextButton . trigger ( 'click' ) ;
513
+ nextButton = component . find ( 'button[aria-label="Next"]' ) ;
514
+ await nextButton . trigger ( 'click' ) ;
515
+
516
+ // --- Third Question ---
517
+ visibleQuestions = component . findAll ( '.question-container' ) . filter ( ( q ) => q . isVisible ( ) ) ;
518
+ expect ( visibleQuestions . length ) . toBe ( 1 ) ;
519
+ expect (
520
+ visibleQuestions [ 0 ] . findAll ( 'span' ) . some ( ( span ) => span . text ( ) . includes ( 'Third question' ) )
521
+ ) . toBe ( true ) ;
522
+
523
+ // Fill third input
524
+ await visibleQuestions [ 0 ] . find ( 'input' ) . setValue ( 'Answer 3' ) ;
525
+
526
+ expect ( component . find ( 'button[aria-label="Back"]' ) . exists ( ) ) . toBe ( true ) ;
527
+ expect ( component . find ( 'button[aria-label="Next"]' ) . exists ( ) ) . toBe ( false ) ;
528
+ expect ( component . find ( 'button[aria-label="Send"]' ) . exists ( ) ) . toBe ( true ) ;
529
+
530
+ // Submit form
531
+ const sendButton = component . find ( 'button[aria-label="Send"]' ) ;
532
+ await sendButton . trigger ( 'click' ) ;
533
+
534
+ // Ensure no form error messages are shown
535
+ const errorMessages = component . findAll ( '.form-error-message' ) . filter ( ( el ) => el . isVisible ( ) ) ;
536
+ expect ( errorMessages . length ) . toBe ( 0 ) ;
537
+ } ) ;
538
+
539
+ it ( 'blocks navigation and send in stepper layout if current step is invalid' , async ( ) => {
540
+ const component = mountComponent ( formXML , {
541
+ overrideProps : { stepperLayout : true } ,
542
+ } ) ;
543
+ await flushPromises ( ) ;
544
+
545
+ let nextButton = component . find ( 'button[aria-label="Next"]' ) ;
546
+ await nextButton . trigger ( 'click' ) ;
547
+
548
+ // Displays an error, remains on first question
549
+ expect ( component . get ( '.validation-message' ) . isVisible ( ) ) . toBe ( true ) ;
550
+ expect ( component . get ( '.validation-message' ) . text ( ) ) . toBe ( 'Condition not satisfied: required' ) ;
551
+ let visibleQuestions = component . findAll ( '.question-container' ) . filter ( ( q ) => q . isVisible ( ) ) ;
552
+ expect ( visibleQuestions . length ) . toBe ( 1 ) ;
553
+ expect (
554
+ visibleQuestions [ 0 ] . findAll ( 'span' ) . some ( ( span ) => span . text ( ) . includes ( 'First question' ) )
555
+ ) . toBe ( true ) ;
556
+ } ) ;
557
+
558
+ it ( 'allows submission once all steps are valid in stepper layout' , async ( ) => {
559
+ const mockSubmit = vi . fn ( ) ;
560
+ const component = mountComponent ( formXML , {
561
+ overrideProps : { stepperLayout : true } ,
562
+ onSubmit : mockSubmit ,
563
+ } ) ;
564
+ await flushPromises ( ) ;
565
+
566
+ // Progress to end of form
567
+ let visibleQuestions = component . findAll ( '.question-container' ) . filter ( ( q ) => q . isVisible ( ) ) ;
568
+ await visibleQuestions [ 0 ] . find ( 'input' ) . setValue ( 'Answer 1' ) ;
569
+ let nextButton = component . find ( 'button[aria-label="Next"]' ) ;
570
+ nextButton = component . find ( 'button[aria-label="Next"]' ) ;
571
+ await nextButton . trigger ( 'click' ) ;
572
+ visibleQuestions = component . findAll ( '.question-container' ) . filter ( ( q ) => q . isVisible ( ) ) ;
573
+ await visibleQuestions [ 0 ] . find ( 'input' ) . setValue ( 'Answer 2' ) ;
574
+ nextButton = component . find ( 'button[aria-label="Next"]' ) ;
575
+ await nextButton . trigger ( 'click' ) ;
576
+
577
+ // Check entire form validation works
578
+ expect ( component . get ( '.form-error-message' ) . isVisible ( ) ) . toBe ( false ) ;
579
+ const sendButton = component . find ( 'button[aria-label="Send"]' ) ;
580
+ await sendButton . trigger ( 'click' ) ;
581
+ expect ( component . get ( '.form-error-message' ) . isVisible ( ) ) . toBe ( true ) ;
582
+
583
+ // Fill final question to make form valid, and check submission sends
584
+ visibleQuestions = component . findAll ( '.question-container' ) . filter ( ( q ) => q . isVisible ( ) ) ;
585
+ await visibleQuestions [ 0 ] . find ( 'input' ) . setValue ( 'Answer 3' ) ;
586
+ await sendButton . trigger ( 'click' ) ;
587
+ expect ( component . get ( '.form-error-message' ) . isVisible ( ) ) . toBe ( false ) ;
588
+ expect ( mockSubmit ) . toHaveBeenCalled ( ) ;
589
+ } ) ;
590
+
591
+ it ( 'always shows the stepper next and back buttons at the bottom of the screen' , async ( ) => {
592
+ // TODO
593
+ console . log ( 'here' ) ;
594
+ } ) ;
595
+ } ) ;
446
596
} ) ;
0 commit comments