@@ -443,4 +443,154 @@ describe('OdkWebForm', () => {
443443 expect ( textInput . element . value ) . toBe ( expectedPostSubmissionValue ) ;
444444 } ) ;
445445 } ) ;
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+ } ) ;
446596} ) ;
0 commit comments