refactor: split the questionnaire into teamer and bus feedback
This commit is contained in:
1 parent
2f4f9800dd
commit
a0170f5952
35 files changed
+1114
-554
No files matched your search
@@ -14,11 +14,11 @@ use App\Model\Questionnaire\Section;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
|
||||
/**
|
||||
* Runs against the shipped teamer_assignment definition, so a definition that parses but cannot be
|
||||
* turned into a working form fails here.
|
||||
* Runs against the shipped definitions - teamer feedback alone, and with the bus feedback on the
|
||||
* same page as a bus escort gets them - so a definition that parses but cannot be turned into a
|
||||
* working form fails here.
|
||||
*
|
||||
* Every submit goes through Form::submit() with clearMissing, as a browser request does: an
|
||||
* unchecked radio group is simply absent from the request, and without clearMissing it would keep
|
||||
@@ -28,7 +28,7 @@ class QuestionnaireTypeTest extends KernelTestCase
|
||||
{
|
||||
public function testEmptyDraftIsValid(): void
|
||||
{
|
||||
$form = $this->createForm($this->teamerAssignment());
|
||||
$form = $this->createForm($this->both());
|
||||
$form->submit([QuestionnaireType::DRAFT => '']);
|
||||
|
||||
$this->assertTrue($form->isValid());
|
||||
@@ -40,85 +40,92 @@ class QuestionnaireTypeTest extends KernelTestCase
|
||||
*/
|
||||
public function testFormSentWithoutAButtonIsADraft(): void
|
||||
{
|
||||
$form = $this->createForm($this->teamerAssignment());
|
||||
$form = $this->createForm($this->both());
|
||||
$form->submit([]);
|
||||
|
||||
$this->assertTrue($form->isValid());
|
||||
$this->assertFalse(QuestionnaireType::isFinal($form));
|
||||
}
|
||||
|
||||
public function testCompleteSubmitIsValidAndFlat(): void
|
||||
public function testCompleteSubmitIsValidAndKeyedByQuestionnaire(): void
|
||||
{
|
||||
$definition = $this->teamerAssignment();
|
||||
$form = $this->createForm($definition);
|
||||
$form->submit($this->complete($definition) + [QuestionnaireType::SUBMIT => '']);
|
||||
$definitions = $this->both();
|
||||
$form = $this->createForm($definitions);
|
||||
$form->submit($this->complete($definitions) + [QuestionnaireType::SUBMIT => '']);
|
||||
|
||||
$this->assertTrue($form->isValid(), (string) $form->getErrors(true));
|
||||
$this->assertTrue(QuestionnaireType::isFinal($form));
|
||||
|
||||
$answers = $form->getData();
|
||||
// keyed by question, not by section, and a rating comes back as an int
|
||||
$this->assertSame(5, $answers['office_briefing_rating']);
|
||||
$this->assertSame('very_helpful', $answers['team_week_seminar']);
|
||||
$this->assertArrayNotHasKey('organisation', $answers);
|
||||
}
|
||||
|
||||
public function testEmptySubmitDemandsTheVisibleRequiredQuestionsOnly(): void
|
||||
{
|
||||
$definition = $this->teamerAssignment();
|
||||
$form = $this->createForm($definition);
|
||||
$form->submit([QuestionnaireType::SUBMIT => '']);
|
||||
|
||||
$this->assertFalse($form->isValid());
|
||||
$this->assertCount(1, $this->field($form, 'organisation', 'office_briefing_rating')->getErrors());
|
||||
$this->assertCount(1, $this->field($form, 'bus_equipment', 'wc')->getErrors());
|
||||
// required, but shown only after a rating of 3 or less, or for another bus company
|
||||
$this->assertCount(0, $this->field($form, 'organisation', 'office_briefing_low_reason')->getErrors());
|
||||
$this->assertCount(0, $this->field($form, 'bus_trip', 'bus_company_other')->getErrors());
|
||||
// optional
|
||||
$this->assertCount(0, $this->field($form, 'communication', 'support_wished')->getErrors());
|
||||
$data = $form->getData();
|
||||
$this->assertSame([QuestionnaireCatalog::TEAMER_FEEDBACK, QuestionnaireCatalog::BUS_FEEDBACK], array_keys($data));
|
||||
// keyed by question within each questionnaire, not by section, and a rating is an int
|
||||
$this->assertSame(5, $data[QuestionnaireCatalog::TEAMER_FEEDBACK]['office_briefing_rating']);
|
||||
$this->assertSame('outbound', $data[QuestionnaireCatalog::BUS_FEEDBACK]['direction']);
|
||||
$this->assertArrayNotHasKey('organisation', $data[QuestionnaireCatalog::TEAMER_FEEDBACK]);
|
||||
}
|
||||
|
||||
/**
|
||||
* A placement without bus escort gets the questionnaire without the bus sections, and can
|
||||
* submit it without them.
|
||||
* Every required question of either questionnaire is demanded, as far as it is shown.
|
||||
*/
|
||||
public function testWithoutBusEscortTheBusPartIsNeitherShownNorDemanded(): void
|
||||
public function testEmptySubmitDemandsTheVisibleRequiredQuestionsOfBoth(): void
|
||||
{
|
||||
$definition = $this->teamerAssignment()->forFlags([]);
|
||||
$form = $this->createForm($definition);
|
||||
$form = $this->createForm($this->both());
|
||||
$form->submit([QuestionnaireType::SUBMIT => '']);
|
||||
|
||||
$this->assertFalse($form->has('bus_trip'));
|
||||
$this->assertFalse($form->isValid());
|
||||
|
||||
$form->submit($this->complete($definition) + [QuestionnaireType::SUBMIT => '']);
|
||||
foreach ($this->both() as $definition) {
|
||||
foreach ($definition->sections as $section) {
|
||||
foreach ($section->questions as $question) {
|
||||
$this->assertCount(
|
||||
$question->required && null === $question->showIf ? 1 : 0,
|
||||
$form->get($definition->key)->get($section->key)->get($question->key)->getErrors(),
|
||||
$definition->key.'.'.$question->key,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A placement without bus escort gets the teamer feedback only, and can submit it alone.
|
||||
*/
|
||||
public function testTheTeamerFeedbackAloneCanBeSubmitted(): void
|
||||
{
|
||||
$definitions = [$this->teamerFeedback()];
|
||||
$form = $this->createForm($definitions);
|
||||
|
||||
$this->assertFalse($form->has(QuestionnaireCatalog::BUS_FEEDBACK));
|
||||
|
||||
$form->submit($this->complete($definitions) + [QuestionnaireType::SUBMIT => '']);
|
||||
|
||||
$this->assertTrue($form->isValid(), (string) $form->getErrors(true));
|
||||
}
|
||||
|
||||
public function testLowRatingDemandsAReason(): void
|
||||
{
|
||||
$definition = $this->teamerAssignment();
|
||||
$data = $this->complete($definition);
|
||||
$data['organisation']['office_briefing_rating'] = '3';
|
||||
$definitions = $this->both();
|
||||
$data = $this->complete($definitions);
|
||||
$data[QuestionnaireCatalog::TEAMER_FEEDBACK]['organisation']['office_briefing_rating'] = '3';
|
||||
|
||||
$form = $this->createForm($definition);
|
||||
$form = $this->createForm($definitions);
|
||||
$form->submit($data + [QuestionnaireType::SUBMIT => '']);
|
||||
|
||||
$this->assertFalse($form->isValid());
|
||||
$this->assertCount(1, $this->field($form, 'organisation', 'office_briefing_low_reason')->getErrors());
|
||||
$this->assertCount(1, $form->get(QuestionnaireCatalog::TEAMER_FEEDBACK)->get('organisation')->get('office_briefing_low_reason')->getErrors());
|
||||
$this->assertCount(1, $form->getErrors(true));
|
||||
}
|
||||
|
||||
public function testOtherBusCompanyDemandsItsName(): void
|
||||
{
|
||||
$definition = $this->teamerAssignment();
|
||||
$data = $this->complete($definition);
|
||||
$data['bus_trip']['bus_company'] = 'other';
|
||||
$definitions = $this->both();
|
||||
$data = $this->complete($definitions);
|
||||
$data[QuestionnaireCatalog::BUS_FEEDBACK]['bus_trip']['bus_company'] = 'other';
|
||||
|
||||
$form = $this->createForm($definition);
|
||||
$form = $this->createForm($definitions);
|
||||
$form->submit($data + [QuestionnaireType::SUBMIT => '']);
|
||||
|
||||
$this->assertCount(1, $this->field($form, 'bus_trip', 'bus_company_other')->getErrors());
|
||||
$this->assertCount(1, $form->get(QuestionnaireCatalog::BUS_FEEDBACK)->get('bus_trip')->get('bus_company_other')->getErrors());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,25 +134,37 @@ class QuestionnaireTypeTest extends KernelTestCase
|
||||
*/
|
||||
public function testNoFieldIsHtmlRequired(): void
|
||||
{
|
||||
$view = $this->createForm($this->teamerAssignment())->createView();
|
||||
$view = $this->createForm($this->both())->createView();
|
||||
|
||||
foreach ($view as $section) {
|
||||
foreach ($section as $field) {
|
||||
$this->assertFalse($field->vars['required'], $field->vars['full_name']);
|
||||
foreach ($view as $questionnaire) {
|
||||
foreach ($questionnaire as $section) {
|
||||
foreach ($section as $field) {
|
||||
$this->assertFalse($field->vars['required'], $field->vars['full_name']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Question keys are only unique within one questionnaire, so the browser sees them prefixed.
|
||||
*/
|
||||
public function testConditionalQuestionCarriesItsConditionForTheBrowser(): void
|
||||
{
|
||||
$view = $this->createForm($this->teamerAssignment())->createView();
|
||||
$view = $this->createForm($this->both())->createView();
|
||||
|
||||
$row = $this->view($view, 'organisation', 'office_briefing_low_reason');
|
||||
$row = $view->children[QuestionnaireCatalog::TEAMER_FEEDBACK]->children['organisation']->children['office_briefing_low_reason'];
|
||||
$this->assertSame('teamer_feedback.office_briefing_low_reason', $row->vars['row_attr']['data-question-key']);
|
||||
$this->assertSame(
|
||||
['question' => 'office_briefing_rating', 'operator' => 'lte', 'value' => 3],
|
||||
['question' => 'teamer_feedback.office_briefing_rating', 'operator' => 'lte', 'value' => 3],
|
||||
json_decode($row->vars['row_attr']['data-show-if'], true),
|
||||
);
|
||||
$this->assertArrayNotHasKey('data-show-if', $this->view($view, 'organisation', 'office_briefing_rating')->vars['row_attr']);
|
||||
}
|
||||
|
||||
public function testTheSameQuestionnaireCannotBeAddedTwice(): void
|
||||
{
|
||||
$this->expectException(\Symfony\Component\OptionsResolver\Exception\InvalidOptionsException::class);
|
||||
|
||||
$this->createForm([$this->teamerFeedback(), $this->teamerFeedback()]);
|
||||
}
|
||||
|
||||
public function testMultiAndDateAnswersRoundTrip(): void
|
||||
@@ -160,50 +179,69 @@ class QuestionnaireTypeTest extends KernelTestCase
|
||||
]),
|
||||
]);
|
||||
|
||||
$form = $this->createForm($definition, ['extras' => ['ski_box'], 'travel_date' => '2026-01-10']);
|
||||
$form = $this->createForm([$definition], ['sample' => ['extras' => ['ski_box'], 'travel_date' => '2026-01-10']]);
|
||||
$view = $form->createView();
|
||||
$this->assertTrue($this->view($view, 'main', 'extras')->children[1]->vars['checked']);
|
||||
$main = $view->children['sample']->children['main'];
|
||||
$this->assertTrue($main->children['extras']->children[1]->vars['checked']);
|
||||
|
||||
$form = $this->createForm($definition);
|
||||
$form = $this->createForm([$definition]);
|
||||
$form->submit([
|
||||
'main' => ['extras' => ['trailer', 'ski_box'], 'travel_date' => $this->view($view, 'main', 'travel_date')->vars['value']],
|
||||
'sample' => ['main' => ['extras' => ['trailer', 'ski_box'], 'travel_date' => $main->children['travel_date']->vars['value']]],
|
||||
QuestionnaireType::SUBMIT => '',
|
||||
]);
|
||||
|
||||
$this->assertTrue($form->isValid(), (string) $form->getErrors(true));
|
||||
$this->assertSame(['trailer', 'ski_box'], $form->getData()['extras']);
|
||||
$this->assertSame('2026-01-10', $form->getData()['travel_date']);
|
||||
$this->assertSame(['trailer', 'ski_box'], $form->getData()['sample']['extras']);
|
||||
$this->assertSame('2026-01-10', $form->getData()['sample']['travel_date']);
|
||||
}
|
||||
|
||||
private function teamerAssignment(): Definition
|
||||
private function teamerFeedback(): Definition
|
||||
{
|
||||
return new QuestionnaireCatalog(__DIR__.'/../../config/questionnaires')->get('teamer_assignment');
|
||||
return $this->catalog()->get(QuestionnaireCatalog::TEAMER_FEEDBACK);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<Definition> what a bus escort gets
|
||||
*/
|
||||
private function both(): array
|
||||
{
|
||||
return [$this->teamerFeedback(), $this->catalog()->get(QuestionnaireCatalog::BUS_FEEDBACK)];
|
||||
}
|
||||
|
||||
private function catalog(): QuestionnaireCatalog
|
||||
{
|
||||
return new QuestionnaireCatalog(__DIR__.'/../../config/questionnaires');
|
||||
}
|
||||
|
||||
/**
|
||||
* A submission answering every question that is shown when each choice takes its first option
|
||||
* and each rating is 5.
|
||||
*
|
||||
* @return array<string, array<string, mixed>>
|
||||
* @param list<Definition> $definitions
|
||||
*
|
||||
* @return array<string, array<string, array<string, mixed>>>
|
||||
*/
|
||||
private function complete(Definition $definition): array
|
||||
private function complete(array $definitions): array
|
||||
{
|
||||
$data = [];
|
||||
$answers = [];
|
||||
|
||||
foreach ($definition->sections as $section) {
|
||||
foreach ($section->questions as $question) {
|
||||
$answer = match ($question->type) {
|
||||
QuestionType::CHOICE => $question->options[0]->key,
|
||||
QuestionType::MULTI => [$question->options[0]->key],
|
||||
QuestionType::RATING => 5,
|
||||
QuestionType::DATE => '01.01.2026',
|
||||
QuestionType::TEXT, QuestionType::TEXTAREA => 'Antwort',
|
||||
};
|
||||
$answers[$question->key] = $answer;
|
||||
foreach ($definitions as $definition) {
|
||||
$answers = [];
|
||||
|
||||
if ($definition->isVisible($question, $answers)) {
|
||||
$data[$section->key][$question->key] = is_int($answer) ? (string) $answer : $answer;
|
||||
foreach ($definition->sections as $section) {
|
||||
foreach ($section->questions as $question) {
|
||||
$answer = match ($question->type) {
|
||||
QuestionType::CHOICE => $question->options[0]->key,
|
||||
QuestionType::MULTI => [$question->options[0]->key],
|
||||
QuestionType::RATING => 5,
|
||||
QuestionType::DATE => '01.01.2026',
|
||||
QuestionType::TEXT, QuestionType::TEXTAREA => 'Antwort',
|
||||
};
|
||||
$answers[$question->key] = $answer;
|
||||
|
||||
if ($definition->isVisible($question, $answers)) {
|
||||
$data[$definition->key][$section->key][$question->key] = is_int($answer) ? (string) $answer : $answer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,26 +249,19 @@ class QuestionnaireTypeTest extends KernelTestCase
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function createForm(Definition $definition, array $answers = []): FormInterface
|
||||
/**
|
||||
* @param list<Definition> $definitions
|
||||
*/
|
||||
private function createForm(array $definitions, array $data = []): FormInterface
|
||||
{
|
||||
self::bootKernel();
|
||||
|
||||
/** @var FormFactoryInterface $formFactory */
|
||||
$formFactory = self::getContainer()->get(FormFactoryInterface::class);
|
||||
|
||||
return $formFactory->create(QuestionnaireType::class, $answers, [
|
||||
'definition' => $definition,
|
||||
return $formFactory->create(QuestionnaireType::class, $data, [
|
||||
'definitions' => $definitions,
|
||||
'csrf_protection' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
private function field(FormInterface $form, string $section, string $question): FormInterface
|
||||
{
|
||||
return $form->get($section)->get($question);
|
||||
}
|
||||
|
||||
private function view(FormView $view, string $section, string $question): FormView
|
||||
{
|
||||
return $view->children[$section]->children[$question];
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user