Skip to content

Commit 03c5af7

Browse files
committed
test: add tests for stepper layout OdkWebForm logic
1 parent 16e84b1 commit 03c5af7

File tree

2 files changed

+188
-0
lines changed

2 files changed

+188
-0
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?xml version="1.0"?>
2+
<h:html xmlns="http://www.w3.org/2002/xforms"
3+
xmlns:ev="http://www.w3.org/2001/xml-events"
4+
xmlns:h="http://www.w3.org/1999/xhtml"
5+
xmlns:jr="http://openrosa.org/javarosa"
6+
xmlns:orx="http://openrosa.org/xforms/"
7+
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
8+
<h:head>
9+
<h:title>Multi Step XForm</h:title>
10+
<model>
11+
<instance>
12+
<root id="minimal">
13+
<first-question/>
14+
<second-question/>
15+
<third-question/>
16+
<meta>
17+
<instanceID/>
18+
</meta>
19+
</root>
20+
</instance>
21+
<bind nodeset="/root/first-question" type="string" required="true()"/>
22+
<bind nodeset="/root/second-question" type="string" required="true()"/>
23+
<bind nodeset="/root/third-question" type="string" required="true()"/>
24+
<bind nodeset="/root/meta/instanceID" type="string"/>
25+
</model>
26+
</h:head>
27+
<h:body>
28+
<input ref="/root/first-question">
29+
<label>First question</label>
30+
</input>
31+
<input ref="/root/second-question">
32+
<label>Second question</label>
33+
</input>
34+
<input ref="/root/third-question">
35+
<label>Third question</label>
36+
</input>
37+
</h:body>
38+
</h:html>

packages/web-forms/tests/components/OdkWebForm.test.ts

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)