Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: "Who Was Ill?" page and validation #263

Merged
merged 8 commits into from
Aug 4, 2020
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

### Merge instructions

We are commited to keeping commit history clean, consistent and linear. To achive this commit should be structured as follows:
We are committed to keeping commit history clean, consistent and linear. To achieve this commit should be structured as follows:

```
<type>[optional scope]: <description>
Expand Down
86 changes: 79 additions & 7 deletions src/controllers/IllPersonController.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,89 @@
import { controller, httpGet } from 'inversify-express-utils';
import { SessionMiddleware } from 'ch-node-session-handler';
import { controller } from 'inversify-express-utils';
import { FormValidator } from './validators/FormValidator';

import { BaseAsyncHttpController } from 'app/controllers/BaseAsyncHttpController';
import { BaseController } from 'app/controllers/BaseController';
import { AuthMiddleware } from 'app/middleware/AuthMiddleware';
import { FeatureToggleMiddleware } from 'app/middleware/FeatureToggleMiddleware';
import { loggerInstance } from 'app/middleware/Logger';
import { Appeal } from 'app/models/Appeal';
import { Illness } from 'app/models/Illness';
import { OtherReason } from 'app/models/OtherReason.ts';
import { IllPerson } from 'app/models/fields/IllPerson';
import { schema } from 'app/models/fields/IllPerson.schema';
import { Feature } from 'app/utils/Feature';
import { ILL_PERSON_PAGE_URI } from 'app/utils/Paths';
import { Navigation } from 'app/utils/navigation/navigation';

const template = 'illness/ill-person';

@controller(ILL_PERSON_PAGE_URI, FeatureToggleMiddleware(Feature.ILLNESS_REASON))
export class IllPersonController extends BaseAsyncHttpController {
const navigation : Navigation = {
previous(): string {
return '';
},
next(): string {
return '';
},
actions: (_: boolean) => {
return {
continue:'action=continue'
};
}
};

interface FormBody {
illPerson: IllPerson;
otherPerson: string;
}

@controller(ILL_PERSON_PAGE_URI, FeatureToggleMiddleware(Feature.ILLNESS_REASON),
SessionMiddleware, AuthMiddleware)
export class IllPersonController extends BaseController<FormBody> {

constructor() {
super(
template,
navigation,
new FormValidator(schema)
);
}

protected prepareViewModelFromAppeal(appeal: Appeal): any {
const illness: Illness | undefined = appeal.reasons?.illness;
if (!illness) {
return {};
}

return {
illPerson: illness.illPerson,
otherPerson: illness.otherPerson
akaJSaunders marked this conversation as resolved.
Show resolved Hide resolved
};
}

protected prepareSessionModelPriorSave(appeal: Appeal, value: FormBody): Appeal {

/* const dummyOther: OtherReason = {
title: 'Dummy Reason',
description: 'The current Appeal data model requires an Other-type reason, ' +
'and reworking the data model is outside of the scope of this feature. ' +
'Until the Appeal object has been remodelled, this dummy reason must ' +
'be included.'
}; */
rhysbarrett marked this conversation as resolved.
Show resolved Hide resolved

if (appeal.reasons?.illness != null) {
appeal.reasons.illness.illPerson = value.illPerson;
if (value.illPerson !== IllPerson.someoneElse) {
appeal.reasons.illness.otherPerson = undefined;
}
} else {
appeal.reasons = {
illness: value as Illness,
other: {} as OtherReason
};
}

@httpGet('')
public async redirectView (): Promise<void> {
return this.render(template);
loggerInstance()
.debug(`${IllPersonController.name} - prepareSessionModelPriorSave: ${JSON.stringify(appeal)}`);
akaJSaunders marked this conversation as resolved.
Show resolved Hide resolved
return appeal;
}
}
1 change: 0 additions & 1 deletion src/controllers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,3 @@ import 'app/controllers/OtherReasonDisclaimerController';
import 'app/controllers/PenaltyDetailsController';
import 'app/controllers/ReviewPenaltyController';
import 'app/controllers/SelectPenaltyController';

5 changes: 4 additions & 1 deletion src/models/Illness.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@

import { YesNo } from './fields/YesNo';
import { IllPerson } from 'app/models/fields/IllPerson';
import { YesNo } from 'app/models/fields/YesNo';
akaJSaunders marked this conversation as resolved.
Show resolved Hide resolved

export interface Illness {
illPerson: IllPerson;
otherPerson?: string;
akaJSaunders marked this conversation as resolved.
Show resolved Hide resolved
illnessStart: string;
continuedIllness: YesNo;
illnessEnd?: string;
Expand Down
25 changes: 25 additions & 0 deletions src/models/fields/IllPerson.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Joi from '@hapi/joi';

import { IllPerson } from 'app/models/fields/IllPerson';

export const emptySelectionErrorMessage = 'You must select a person';
export const emptyOtherPersonErrorMessage = 'You must tell us more information';

export const schema = Joi.object({
illPerson: Joi.string()
.required()
.valid(...Object.values(IllPerson).filter(x => typeof x === 'string'))
.messages({
'any.required': emptySelectionErrorMessage,
'any.only': emptySelectionErrorMessage,
}),
otherPerson: Joi.when('illPerson', {
is: IllPerson.someoneElse,
then: Joi.string().required().pattern(/\w+/).messages({
'any.required': emptyOtherPersonErrorMessage,
'string.base': emptyOtherPersonErrorMessage,
'string.empty': emptyOtherPersonErrorMessage,
'string.pattern.base': emptyOtherPersonErrorMessage
})
})
}).options({ abortEarly: true });
7 changes: 7 additions & 0 deletions src/models/fields/IllPerson.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export enum IllPerson {
director = 'director',
accountant = 'accountant',
family = 'family',
employee = 'employee',
someoneElse = 'someoneElse'
akaJSaunders marked this conversation as resolved.
Show resolved Hide resolved
}
3 changes: 3 additions & 0 deletions src/utils/Paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@ export const PENALTY_DETAILS_PAGE_URI = `${ROOT_URI}/penalty-reference`;
export const SELECT_THE_PENALTY_PAGE_URI = `${ROOT_URI}/select-the-penalty`;
export const REVIEW_PENALTY_PAGE_URI =`${ROOT_URI}/review-penalty`;
export const CHOOSE_REASON_PAGE_URI =`${ROOT_URI}/choose-reason`;

export const ILL_PERSON_PAGE_URI = `${CATEGORY_PREFIXES.ILLNESS}/who-was-ill`;
export const ILLNESS_START_DATE_PAGE_URI = `${CATEGORY_PREFIXES.ILLNESS}/illness-start-date`;
export const CONTINUED_ILLNESS_PAGE_URI = `${CATEGORY_PREFIXES.ILLNESS}/continued-illness`;

export const OTHER_REASON_DISCLAIMER_PAGE_URI = `${CATEGORY_PREFIXES.OTHER_REASON}/other-reason-entry`;
export const OTHER_REASON_PAGE_URI = `${CATEGORY_PREFIXES.OTHER_REASON}/reason-other`;

export const EVIDENCE_QUESTION_URI = `${ROOT_URI}/evidence`;
export const EVIDENCE_UPLOAD_PAGE_URI = `${ROOT_URI}/evidence-upload`;
export const EVIDENCE_REMOVAL_PAGE_URI = `${ROOT_URI}/remove-document`;
Expand Down
87 changes: 83 additions & 4 deletions src/views/illness/ill-person.njk
Original file line number Diff line number Diff line change
@@ -1,16 +1,95 @@
{% extends 'layout.njk' %}

{% from 'govuk/components/back-link/macro.njk' import govukBackLink %}
{% from 'govuk/components/error-summary/macro.njk' import govukErrorSummary %}
{% from 'govuk/components/input/macro.njk' import govukInput %}
{% from 'govuk/components/button/macro.njk' import govukButton %}
{% from 'govuk/components/radios/macro.njk' import govukRadios %}

{% block pageTitle %}
Appeal due to ill health: Who was ill?
{% endblock %}

{% block content %}
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<h1 class="govuk-heading-xl">
Who was ill?
</h1>
<p class="govuk-body">This page is currently in development.</p>
{{
govukErrorSummary ({
titleText: 'There is a problem with the details you gave us',
errorList: validationResult.errors
}) if validationResult and validationResult.errors.length > 0
}}

<form method="post">
{% set otherPersonHtml %}
{{
govukInput({
id: 'other-person',
name: 'otherPerson',
type: 'text',
classes: 'govuk-!-width-one-third',
label: {
text: 'Their relationship to the company'
},
errorMessage: validationResult.getErrorForField('otherPerson') if validationResult
akaJSaunders marked this conversation as resolved.
Show resolved Hide resolved
})
}}
{% endset -%}

{{
govukRadios({
idPrefix: 'ill-person',
name: 'illPerson',
fieldset: {
legend: {
text: 'Who was ill?',
isPageHeading: true,
classes: 'govuk-fieldset__legend--xl'
}
},
errorMessage: validationResult.getErrorForField('illPerson') if validationResult,
items: [
{
value: 'director',
text: 'A company director',
checked: true if illPerson === 'director'
},
{
value: 'accountant',
text: 'A company accountant or agent',
checked: true if illPerson === 'accountant'
},
{
value: 'family',
text: 'A family member of a director',
checked: true if illPerson === 'family'
},
{
value: 'employee',
text: 'A company employee',
checked: true if illPerson === 'employee'
},
{
value: 'someoneElse',
text: 'Someone else',
checked: true if illPerson === 'someoneElse',
conditional: {
html: otherPersonHtml
akaJSaunders marked this conversation as resolved.
Show resolved Hide resolved
}
}
]
})
}}

{{
govukButton({
text: 'Continue',
attributes: {
id: 'submit'
}
})
}}
</form>
</div>
</div>
{% endblock %}
69 changes: 69 additions & 0 deletions test/controllers/IllPersonController.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { expect } from 'chai';
import { UNPROCESSABLE_ENTITY } from 'http-status-codes';
import request from 'supertest';
import { createApp } from '../ApplicationFactory';

import { Appeal } from 'app/models/Appeal';
import { ApplicationData } from 'app/models/ApplicationData';
import { IllPerson } from 'app/models/fields/IllPerson';
import { ILL_PERSON_PAGE_URI } from 'app/utils/Paths';

describe('IllPersonController', () => {

const applicationData: Partial<ApplicationData> = {
appeal: {
penaltyIdentifier: {
companyNumber: 'NI000000'
}
} as Appeal,
navigation: { permissions: [ILL_PERSON_PAGE_URI] }
};

describe('on GET', () => {
it('should show radio buttons for available Ill Person options', async () => {
const app = createApp(applicationData);

await request(app).get(ILL_PERSON_PAGE_URI).expect(res => {
expect(res.text).to.include('type="radio"');
expect(res.text).to.include('value="director"');
expect(res.text).to.include('value="accountant"');
expect(res.text).to.include('value="family"');
expect(res.text).to.include('value="employee"');
expect(res.text).to.include('value="someoneElse"');
const radioCount = (res.text.match(/type="radio"/g) || []).length;
expect(radioCount).to.equal(5);
});
});

it('should hide the Other Person conditional input by default', async () => {
const app = createApp(applicationData);

await request(app).get(ILL_PERSON_PAGE_URI).expect(res => {
expect(res.text).to.include('class="govuk-radios__conditional govuk-radios__conditional--hidden"');
expect(res.text).to.include('name="otherPerson"');
});
});
});

describe('on POST', () => {
it('should show a validation error if no person is selected', async () => {
const app = createApp(applicationData);

await request(app).post(ILL_PERSON_PAGE_URI).expect(res => {
expect(res.status).to.equal(UNPROCESSABLE_ENTITY);
expect(res.text).to.contain('You must select a person');
});
});


it('should show a validation error if "Someone else" is chosen but not specified', async () => {
const app = createApp(applicationData);

await request(app).post(ILL_PERSON_PAGE_URI).send({ illPerson: IllPerson.someoneElse }).expect(res => {
expect(res.text).to.include('class="govuk-radios__conditional"');
expect(res.status).to.equal(UNPROCESSABLE_ENTITY);
expect(res.text).to.contain('You must tell us more information');
});
});
});
});
Loading