Skip to content

Commit

Permalink
Merge pull request #683 from cabinetoffice/feature/dp-543-exclusions-…
Browse files Browse the repository at this point in the history
…single-choice-question-type

Feature/dp 543 exclusions single choice question type
  • Loading branch information
JBaigGoaco authored Oct 3, 2024
2 parents 62dc666 + dfbd315 commit 3e580a3
Show file tree
Hide file tree
Showing 14 changed files with 2,243 additions and 34 deletions.
113 changes: 82 additions & 31 deletions Frontend/CO.CDP.OrganisationApp.Tests/FormsEngineTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,37 +56,56 @@ private static WebApiClient.SectionQuestionsResponse CreateApiSectionQuestionsRe
id: sectionId,
title: "SectionTitle"
),
questions: new List<WebApiClient.FormQuestion>
{
new WebApiClient.FormQuestion(
id: questionId,
title: "Question1",
description: "Description1",
caption: "Caption1",
summaryTitle: "Question1 Title",
type: WebApiClient.FormQuestionType.Text,
isRequired: true,
nextQuestion: nextQuestionId,
nextQuestionAlternative: null,
options: new WebApiClient.FormQuestionOptions(
choiceProviderStrategy: choiceProviderStrategy,
choices: new List<WebApiClient.FormQuestionChoice>
{
new WebApiClient.FormQuestionChoice(
id: Guid.NewGuid(),
title: "Option1",
groupName: null,
hint: new WebApiClient.FormQuestionChoiceHint(
title: null,
description: "Hint Description"
)
questions: new List<WebApiClient.FormQuestion>
{
new WebApiClient.FormQuestion(
id: questionId,
title: "Question1",
description: "Description1",
caption: "Caption1",
summaryTitle: "Question1 Title",
type: WebApiClient.FormQuestionType.Text,
isRequired: true,
nextQuestion: nextQuestionId,
nextQuestionAlternative: null,
options: new WebApiClient.FormQuestionOptions(
choiceProviderStrategy: choiceProviderStrategy,
choices: new List<WebApiClient.FormQuestionChoice>
{
new WebApiClient.FormQuestionChoice(
id: Guid.NewGuid(),
title: "Option1",
groupName: null,
hint: new WebApiClient.FormQuestionChoiceHint(
title: null,
description: "Hint Description"
)
)
},
groups: new List<WebApiClient.FormQuestionGroup>
{
new WebApiClient.FormQuestionGroup(
name: "Group 1",
hint: "Group 1 Hint",
caption: "Group 1 Caption",
choices: new List<WebApiClient.FormQuestionGroupChoice>
{
new WebApiClient.FormQuestionGroupChoice(
title: "Group Choice 1",
value: "group_choice_1"
),
new WebApiClient.FormQuestionGroupChoice(
title: "Group Choice 2",
value: "group_choice_2"
)
}
)
}
)
}
)
)
},
answerSets: new List<WebApiClient.FormAnswerSet>()
);
)
},
answerSets: new List<WebApiClient.FormAnswerSet>()
);
}

private static SectionQuestionsResponse CreateModelSectionQuestionsResponse(Guid sectionId, Guid questionId, Guid nextQuestionId, string? choiceProviderStrategy = null, Dictionary<string, string>? options = null)
Expand Down Expand Up @@ -140,9 +159,26 @@ public async Task GetFormSectionAsync_ShouldFetchAndCacheResponse_WhenCachedResp
var (organisationId, formId, sectionId, sessionKey) = CreateTestGuids();
var questionId = Guid.NewGuid();
var nextQuestionId = Guid.NewGuid();

var apiResponse = CreateApiSectionQuestionsResponse(sectionId, questionId, nextQuestionId);

var expectedResponse = CreateModelSectionQuestionsResponse(sectionId, questionId, nextQuestionId);

expectedResponse.Questions[0].Options.Groups = new List<FormQuestionGroup>
{
new FormQuestionGroup
{
Name = "Group 1",
Hint = "Group 1 Hint",
Caption = "Group 1 Caption",
Choices = new List<FormQuestionGroupChoice>
{
new FormQuestionGroupChoice { Title = "Group Choice 1", Value = "group_choice_1" },
new FormQuestionGroupChoice { Title = "Group Choice 2", Value = "group_choice_2" }
}
}
};

_tempDataServiceMock.Setup(t => t.Peek<SectionQuestionsResponse>(sessionKey))
.Returns((SectionQuestionsResponse?)null);
_choiceProviderServiceMock.Setup(t => t.GetStrategy(It.IsAny<string>()))
Expand Down Expand Up @@ -264,13 +300,28 @@ public async Task GetFormSectionAsync_ShouldFetchChoicesFromCustomChoiceProvider

expectedResponse.Questions[0].Options.ChoiceAnswerFieldName = "JsonValue";

expectedResponse.Questions[0].Options.Groups = new List<FormQuestionGroup>
{
new FormQuestionGroup
{
Name = "Group 1",
Hint = "Group 1 Hint",
Caption = "Group 1 Caption",
Choices = new List<FormQuestionGroupChoice>
{
new FormQuestionGroupChoice { Title = "Group Choice 1", Value = "group_choice_1" },
new FormQuestionGroupChoice { Title = "Group Choice 2", Value = "group_choice_2" }
}
}
};

_organisationClientMock.Setup(c => c.GetConnectedEntitiesAsync(It.IsAny<Guid>()))
.ReturnsAsync([
new ConnectedEntityLookup(new Guid("e4bdd7ef-8200-4257-9892-b16f43d1803e"), ConnectedEntityType.Individual, "Connected person", new Uri("http://whatever")),
new ConnectedEntityLookup(new Guid("4c8dccba-df39-4997-814b-7599ed9b5bed"), ConnectedEntityType.Organisation, "Connected organisation", new Uri("http://whatever"))
]);
_organisationClientMock.Setup(c => c.GetOrganisationAsync(organisationId))
.ReturnsAsync(new Organisation.WebApiClient.Organisation([], [],null, null, organisationId, null, "User's current organisation", []));
.ReturnsAsync(new Organisation.WebApiClient.Organisation([], [], null, null, organisationId, null, "User's current organisation", []));
_userInfoServiceMock.Setup(u => u.GetOrganisationId()).Returns(organisationId);
_tempDataServiceMock.Setup(t => t.Peek<SectionQuestionsResponse>(sessionKey))
.Returns((SectionQuestionsResponse?)null);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using CO.CDP.OrganisationApp.Models;
using CO.CDP.OrganisationApp.Pages.Forms;
using FluentAssertions;
using System.ComponentModel.DataAnnotations;

namespace CO.CDP.OrganisationApp.Tests.Pages.Forms;
public class FormElementGroupedSingleChoiceModelTests
{
private readonly FormElementGroupedSingleChoiceModel _model;

public FormElementGroupedSingleChoiceModelTests()
{
_model = new FormElementGroupedSingleChoiceModel
{
Options = new FormQuestionOptions
{
Groups = new List<FormQuestionGroup>
{
new FormQuestionGroup
{
Name = "Group 1",
Hint = "Group 1 Hint",
Caption = "Group 1 Caption",
Choices = new List<FormQuestionGroupChoice>
{
new FormQuestionGroupChoice { Title = "Group Choice 1", Value = "group_choice_1" },
new FormQuestionGroupChoice { Title = "Group Choice 2", Value = "group_choice_2" }
}
}
}
}
};
}

[Fact]
public void GetAnswer_ShouldReturnNull_WhenNoOptionIsSelected()
{
_model.SelectedOption = null;
var answer = _model.GetAnswer();
answer.Should().BeNull();
}

[Fact]
public void GetAnswer_ShouldReturnFormAnswer_WhenValidOptionIsSelected()
{
_model.SelectedOption = "group_choice_1";
var answer = _model.GetAnswer();

answer.Should().NotBeNull();
answer!.OptionValue.Should().Be("group_choice_1");
}

[Fact]
public void GetAnswer_ShouldReturnNull_WhenInvalidOptionIsSelected()
{
_model.SelectedOption = "invalid_choice";

var answer = _model.GetAnswer();
answer.Should().BeNull();
}

[Fact]
public void SetAnswer_ShouldSetSelectedOption_WhenValidOptionIsProvided()
{
var answer = new FormAnswer { OptionValue = "group_choice_2" };

_model.SetAnswer(answer);
_model.SelectedOption.Should().Be("group_choice_2");
}

[Fact]
public void SetAnswer_ShouldNotSetSelectedOption_WhenInvalidOptionIsProvided()
{
var answer = new FormAnswer { OptionValue = "invalid_choice" };

_model.SetAnswer(answer);
_model.SelectedOption.Should().BeNull();
}

[Fact]
public void Validate_ShouldReturnValidationError_WhenNoOptionIsSelected()
{
_model.SelectedOption = null;

var validationResults = _model.Validate(new ValidationContext(_model));
validationResults.Should().ContainSingle(v => v.ErrorMessage == "Select an option");
}

[Fact]
public void Validate_ShouldReturnValidationError_WhenInvalidOptionIsSelected()
{
_model.SelectedOption = "invalid_choice";

var validationResults = _model.Validate(new ValidationContext(_model));
validationResults.Should().ContainSingle(v => v.ErrorMessage == "Invalid option selected");
}

[Fact]
public void Validate_ShouldReturnNoError_WhenValidOptionIsSelected()
{
_model.SelectedOption = "group_choice_1";

var validationResults = _model.Validate(new ValidationContext(_model));
validationResults.Should().BeEmpty();
}
}
11 changes: 11 additions & 0 deletions Frontend/CO.CDP.OrganisationApp/FormsEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ public async Task<SectionQuestionsResponse> GetFormSectionAsync(Guid organisatio
{
Choices = await choiceProviderStrategy.Execute(q.Options),
ChoiceProviderStrategy = q.Options.ChoiceProviderStrategy,
Groups = q.Options.Groups?.Select(g => new Models.FormQuestionGroup
{
Name = g.Name,
Hint = g.Hint,
Caption = g.Caption,
Choices = g.Choices?.Select(c => new Models.FormQuestionGroupChoice
{
Title = c.Title,
Value = c.Value
}).ToList()
}).ToList(),
ChoiceAnswerFieldName = choiceProviderStrategy.AnswerFieldName
}
};
Expand Down
16 changes: 16 additions & 0 deletions Frontend/CO.CDP.OrganisationApp/Models/DynamicForms.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public class FormQuestionOptions
{
public Dictionary<string, string>? Choices { get; set; }
public string? ChoiceProviderStrategy { get; set; }
public List<FormQuestionGroup>? Groups { get; set; }
public string? ChoiceAnswerFieldName { get; set; }
}

Expand Down Expand Up @@ -68,6 +69,20 @@ public class FormAnswer
public string? JsonValue { get; init; }
}

public class FormQuestionGroup
{
public string? Name { get; set; }
public string? Hint { get; set; }
public string? Caption { get; set; }
public List<FormQuestionGroupChoice>? Choices { get; set; }
}

public class FormQuestionGroupChoice
{
public string? Title { get; set; }
public string? Value { get; set; } = null;
}

public enum FormQuestionType
{
NoInput,
Expand All @@ -81,5 +96,6 @@ public enum FormQuestionType
CheckBox,
Address,
MultiLine,
GroupedSingleChoice,
Url
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using CO.CDP.OrganisationApp.Models;
using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;

namespace CO.CDP.OrganisationApp.Pages.Forms;

public class FormElementGroupedSingleChoiceModel : FormElementModel, IValidatableObject
{
[BindProperty]
public string? SelectedOption { get; set; }

public override FormAnswer? GetAnswer()
{
if (SelectedOption != null && Options?.Groups != null)
{
var isValidOption = Options.Groups.Any(group => group.Choices != null && group.Choices.Any(choice => choice.Value == SelectedOption));

if (isValidOption)
{
return new FormAnswer { OptionValue = SelectedOption };
}
}

return null;
}

public override void SetAnswer(FormAnswer? answer)
{
if (answer?.OptionValue != null && Options?.Groups != null)
{
var isValidOption = Options.Groups.Any(group => group.Choices != null && group.Choices.Any(choice => choice.Value == answer.OptionValue));

if (isValidOption)
{
SelectedOption = answer.OptionValue;
}
}
}

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (string.IsNullOrWhiteSpace(SelectedOption))
{
yield return new ValidationResult("Select an option", new[] { nameof(SelectedOption) });
yield break;
}

if (Options?.Groups == null || !Options.Groups.Any(group => group.Choices != null && group.Choices.Any(choice => choice.Value == SelectedOption)))
{
yield return new ValidationResult("Invalid option selected", new[] { nameof(SelectedOption) });
}
}
}
Loading

0 comments on commit 3e580a3

Please sign in to comment.