diff --git a/assets/js/add-another-detail.js b/assets/js/add-another-detail.js new file mode 100644 index 00000000..42359660 --- /dev/null +++ b/assets/js/add-another-detail.js @@ -0,0 +1,2 @@ +new AddAnother($('.add-another-body-worn-camera'), '.remove-button-container') +new AddAnother($('.add-another-weapons-observed'), '.remove-button-container') diff --git a/assets/js/add-another-evidence.js b/assets/js/add-another-evidence.js index 7723d5e8..013ffe04 100644 --- a/assets/js/add-another-evidence.js +++ b/assets/js/add-another-evidence.js @@ -1,2 +1 @@ new AddAnother($('.add-another-evidence'), '.remove-button-container') -new AddAnother($('.add-another-input'), '.remove-button-container') diff --git a/integration-tests/integration/createIncident/form-submit.cy.js b/integration-tests/integration/createIncident/form-submit.cy.js index 66033b49..1dfbacaa 100644 --- a/integration-tests/integration/createIncident/form-submit.cy.js +++ b/integration-tests/integration/createIncident/form-submit.cy.js @@ -84,6 +84,14 @@ context('Submit the incident report', () => { useOfForceDetailsPage.bodyWornCameraNumber(1).type('789') useOfForceDetailsPage.addAnotherBodyWornCamera() useOfForceDetailsPage.bodyWornCameraNumber(2).type('456') + + useOfForceDetailsPage.weaponsObserved().check('YES') + useOfForceDetailsPage.weaponTypes(0).type('gun') + useOfForceDetailsPage.addAnotherWeapon() + useOfForceDetailsPage.weaponTypes(1).type('knife') + useOfForceDetailsPage.addAnotherWeapon() + useOfForceDetailsPage.weaponTypes(2).type('fork') + const relocationAndInjuriesPage = useOfForceDetailsPage.save() relocationAndInjuriesPage.fillForm() const evidencePage = relocationAndInjuriesPage.save() diff --git a/integration-tests/integration/reportPages/enter-use-of-force-details.cy.js b/integration-tests/integration/reportPages/enter-use-of-force-details.cy.js index 89455977..3b117c5f 100644 --- a/integration-tests/integration/reportPages/enter-use-of-force-details.cy.js +++ b/integration-tests/integration/reportPages/enter-use-of-force-details.cy.js @@ -1,4 +1,3 @@ -const { use } = require('passport') const { offender } = require('../../mockApis/data') const ReportUseOfForcePage = require('../../pages/createReport/reportUseOfForcePage') @@ -31,6 +30,7 @@ context('Enter use of force details page', () => { useOfForceDetailsPage.batonDrawn().check('true') useOfForceDetailsPage.batonUsed().check('true') useOfForceDetailsPage.pavaDrawn().check('true') + useOfForceDetailsPage.weaponsObserved().check('NO') useOfForceDetailsPage.pavaUsed().check('true') useOfForceDetailsPage.guidingHold().check('true') useOfForceDetailsPage.guidingHoldOfficersInvolved.check('2') @@ -59,6 +59,7 @@ context('Enter use of force details page', () => { handcuffsApplied: true, pavaDrawn: true, pavaUsed: true, + weaponsObserved: 'NO', personalProtectionTechniques: true, positiveCommunication: true, restraintPositions: ['STANDING', 'ON_BACK', 'FACE_DOWN', 'KNEELING'], @@ -83,6 +84,7 @@ context('Enter use of force details page', () => { escortingHold: true, handcuffsApplied: true, pavaDrawn: true, + weaponsObserved: 'NO', pavaUsed: true, personalProtectionTechniques: true, positiveCommunication: true, @@ -127,12 +129,39 @@ context('Enter use of force details page', () => { selectUofReasonsPage.checkReason('FIGHT_BETWEEN_PRISONERS') selectUofReasonsPage.clickSaveAndContinue() + const useOfForceDetailsPage = UseOfForceDetailsPage.verifyOnPage() + useOfForceDetailsPage.positiveCommunication().check('true') + useOfForceDetailsPage.personalProtectionTechniques().check('true') + useOfForceDetailsPage.pavaDrawn().check('true') + useOfForceDetailsPage.pavaUsed().check('true') + useOfForceDetailsPage.guidingHold().check('true') + useOfForceDetailsPage.guidingHoldOfficersInvolved.check('2') + useOfForceDetailsPage.escortingHold().check('true') + useOfForceDetailsPage.handcuffsApplied().check('true') + useOfForceDetailsPage.clickSaveAndContinue() + useOfForceDetailsPage.errorSummary().contains('Select yes if a baton was drawn') + useOfForceDetailsPage + .errorSummary() + .contains('Select yes if any part of the incident was captured on a body-worn camera') + useOfForceDetailsPage.errorSummary().contains('Select if any pain inducing techniques were used') + useOfForceDetailsPage.errorSummary().contains('Select yes if any weapons were observed') + }) + + it('Displays secondary validation messages', () => { + cy.login() + + const reportUseOfForcePage = ReportUseOfForcePage.visit(offender.bookingId) + const selectUofReasonsPage = reportUseOfForcePage.goToSelectUofReasonsPage() + selectUofReasonsPage.checkReason('FIGHT_BETWEEN_PRISONERS') + selectUofReasonsPage.clickSaveAndContinue() + const useOfForceDetailsPage = UseOfForceDetailsPage.verifyOnPage() useOfForceDetailsPage.positiveCommunication().check('true') useOfForceDetailsPage.bodyWornCamera().check('YES') useOfForceDetailsPage.personalProtectionTechniques().check('true') useOfForceDetailsPage.pavaDrawn().check('true') useOfForceDetailsPage.pavaUsed().check('true') + useOfForceDetailsPage.weaponsObserved().check('YES') useOfForceDetailsPage.guidingHold().check('true') useOfForceDetailsPage.guidingHoldOfficersInvolved.check('2') useOfForceDetailsPage.escortingHold().check('true') @@ -141,5 +170,23 @@ context('Enter use of force details page', () => { useOfForceDetailsPage.errorSummary().contains('Select yes if a baton was drawn') useOfForceDetailsPage.errorSummary().contains('Enter the body-worn camera number') useOfForceDetailsPage.errorSummary().contains('Select if any pain inducing techniques were used') + useOfForceDetailsPage.errorSummary().contains('Enter the type of weapon observed') + }) + + it('Displays validation messages when multiple inputs are not unique', () => { + cy.login() + + const reportUseOfForcePage = ReportUseOfForcePage.visit(offender.bookingId) + const selectUofReasonsPage = reportUseOfForcePage.goToSelectUofReasonsPage() + selectUofReasonsPage.checkReason('FIGHT_BETWEEN_PRISONERS') + selectUofReasonsPage.clickSaveAndContinue() + + const useOfForceDetailsPage = UseOfForceDetailsPage.verifyOnPage() + useOfForceDetailsPage.bodyWornCamera().check('YES') + useOfForceDetailsPage.bodyWornCameraNumber(0).type('1') + useOfForceDetailsPage.addAnotherBodyWornCamera() + useOfForceDetailsPage.bodyWornCameraNumber(1).type('1') + useOfForceDetailsPage.clickSaveAndContinue() + useOfForceDetailsPage.errorSummary().contains("Camera '1' has already been added - remove this camera") }) }) diff --git a/integration-tests/integration/seedData.js b/integration-tests/integration/seedData.js index f1c22811..4658b42c 100644 --- a/integration-tests/integration/seedData.js +++ b/integration-tests/integration/seedData.js @@ -28,6 +28,8 @@ const expectedPayload = { pavaUsed: true, batonUsed: true, pavaDrawn: true, + weaponsObserved: 'YES', + weaponTypes: [{ weaponType: 'gun' }, { weaponType: 'knife' }, { weaponType: 'fork' }], batonDrawn: true, guidingHold: true, escortingHold: true, diff --git a/integration-tests/pages/createReport/useOfForceDetailsPage.js b/integration-tests/pages/createReport/useOfForceDetailsPage.js index 8e12ce99..2b233194 100644 --- a/integration-tests/pages/createReport/useOfForceDetailsPage.js +++ b/integration-tests/pages/createReport/useOfForceDetailsPage.js @@ -7,13 +7,17 @@ const useOfForceDetailsPage = () => bodyWornCamera: () => cy.get('[name="bodyWornCamera"]'), bodyWornCameraNumber: index => cy.get(`[name="bodyWornCameraNumbers[${index}][cameraNum]"]`), - addAnotherBodyWornCamera: () => cy.get('[data-qa-add-another-input = true]').click(), - removeBodyWornCamera: index => cy.get('.add-another-input .add-another__remove-button').eq(index).click(), + addAnotherBodyWornCamera: () => cy.get('[dataqa=add-another-body-worn-camera]').click(), + removeBodyWornCamera: index => + cy.get('.add-another-body-worn-camera .add-another__remove-button').eq(index).click(), personalProtectionTechniques: () => cy.get('[name="personalProtectionTechniques"]'), batonDrawn: () => cy.get('[name="batonDrawn"]'), batonUsed: () => cy.get('[name="batonUsed"]'), pavaDrawn: () => cy.get('[name="pavaDrawn"]'), + weaponsObserved: () => cy.get('[name="weaponsObserved"]'), + weaponTypes: index => cy.get(`[name="weaponTypes[${index}][weaponType]"]`), + addAnotherWeapon: () => cy.get('[dataqa=add-another-weapons-observed]').click(), pavaUsed: () => cy.get('[name="pavaUsed"]'), guidingHold: () => cy.get('[name="guidingHold"]'), guidingHoldOfficersInvolved: { @@ -57,6 +61,7 @@ const useOfForceDetailsPage = () => this.batonUsed().check('true') this.pavaDrawn().check('true') this.pavaUsed().check('true') + this.weaponsObserved().check('NO') this.guidingHold().check('true') this.guidingHoldOfficersInvolved.check('2') this.escortingHold().check('true') diff --git a/server/config/forms/useOfForceDetailsForm.js b/server/config/forms/useOfForceDetailsForm.js index d3d11617..56ca7b14 100644 --- a/server/config/forms/useOfForceDetailsForm.js +++ b/server/config/forms/useOfForceDetailsForm.js @@ -53,6 +53,25 @@ const completeSchema = joi.object({ pavaDrawn: requiredBooleanMsg('Select yes if PAVA was drawn').alter(optionalForPartialValidation), + weaponsObserved: requiredOneOfMsg( + 'YES', + 'NO' + )('Select yes if any weapons were observed').alter(optionalForPartialValidation), + + weaponTypes: joi + .when('weaponsObserved', { + is: 'YES', + then: arrayOfObjects({ + weaponType: requiredStringMsg('Enter the type of weapon observed').alter(optionalForPartialValidation), + }) + .min(1) + .message('Enter the type of weapon observed') + .required() + .alter(minZeroForPartialValidation), + otherwise: joi.any().strip(), + }) + .meta({ firstFieldName: 'weaponTypes[0]' }), + pavaUsed: joi.when('pavaDrawn', { is: true, then: requiredBooleanMsg('Select yes if PAVA was used').alter(optionalForPartialValidation), diff --git a/server/config/forms/useOfForceDetailsValidation.test.js b/server/config/forms/useOfForceDetailsValidation.test.js index 0f7093cf..9a0a4cf9 100644 --- a/server/config/forms/useOfForceDetailsValidation.test.js +++ b/server/config/forms/useOfForceDetailsValidation.test.js @@ -1,5 +1,6 @@ const { complete, partial } = require('./useOfForceDetailsForm') const { processInput } = require('../../services/validation') +const { BodyWornCameras } = require('../types') const checkFactory = schema => input => { const { payloadFields: formResponse, errors } = processInput({ validationSpec: schema, input }) @@ -16,6 +17,7 @@ beforeEach(() => { batonDrawn: 'true', batonUsed: 'true', pavaDrawn: 'true', + weaponsObserved: 'NO', pavaUsed: 'true', guidingHold: 'true', guidingHoldOfficersInvolved: '2', @@ -44,6 +46,7 @@ describe('complete schema', () => { batonUsed: true, pavaDrawn: true, pavaUsed: true, + weaponsObserved: 'NO', guidingHold: true, guidingHoldOfficersInvolved: 2, escortingHold: true, @@ -53,7 +56,7 @@ describe('complete schema', () => { }) }) - it('Should return 9 error messages if no input field is completed', () => { + it('Should return 10 error messages if no input field is completed', () => { const input = {} const { errors, formResponse } = check(input) @@ -78,6 +81,7 @@ describe('complete schema', () => { href: '#pavaDrawn', text: 'Select yes if PAVA was drawn', }, + { href: '#weaponsObserved', text: 'Select yes if any weapons were observed' }, { href: '#guidingHold', text: 'Select yes if a guiding hold was used', @@ -100,7 +104,7 @@ describe('complete schema', () => { }, ]) - expect(errors.length).toEqual(10) + expect(errors.length).toEqual(11) expect(formResponse).toEqual({}) }) @@ -190,6 +194,7 @@ describe('complete schema', () => { handcuffsApplied: true, painInducingTechniquesUsed: ['FINAL_LOCK_FLEXION', 'THUMB_LOCK'], pavaDrawn: true, + weaponsObserved: 'NO', pavaUsed: true, personalProtectionTechniques: true, positiveCommunication: true, @@ -198,7 +203,7 @@ describe('complete schema', () => { }) it('Body-worn camera identifiers are not required when bodyWornCamera is NO', () => { - validInput.bodyWornCamer = 'NO' + validInput.bodyWornCamera = 'NO' validInput.bodyWornCameraNumbers = [{ cameraNum: 'AAA' }, { cameraNum: '' }, { cameraNum: 'AAA' }] const { errors, formResponse } = check(validInput) @@ -215,6 +220,7 @@ describe('complete schema', () => { handcuffsApplied: true, painInducingTechniquesUsed: ['FINAL_LOCK_FLEXION', 'THUMB_LOCK'], pavaDrawn: true, + weaponsObserved: 'NO', pavaUsed: true, personalProtectionTechniques: true, positiveCommunication: true, @@ -244,6 +250,7 @@ describe('complete schema', () => { handcuffsApplied: true, painInducingTechniquesUsed: ['FINAL_LOCK_FLEXION', 'THUMB_LOCK'], pavaDrawn: true, + weaponsObserved: 'NO', pavaUsed: true, personalProtectionTechniques: true, positiveCommunication: true, @@ -332,6 +339,108 @@ describe('complete schema', () => { expect(formResponse.pavaUsed).toBe(undefined) }) + it("Not selecting an option for 'weapons observed' returns a validation error message", () => { + const input = { + ...validInput, + weaponsObserved: undefined, + } + const { errors } = check(input) + expect(errors).toEqual([ + { + href: '#weaponsObserved', + text: 'Select yes if any weapons were observed', + }, + ]) + }) + + it('Selecting YES for weapons observed but not adding weapon types generates validation error', () => { + validInput.weaponsObserved = 'YES' + const { errors } = check(validInput) + + expect(errors).toEqual([{ href: '#weaponTypes[0]', text: '"weaponTypes" is required' }]) + }) + + it('Should not return validation error if all weapon observed identifiers are unique', () => { + validInput.weaponsObserved = 'YES' + validInput.weaponTypes = [{ weaponType: 'Gun' }, { weaponType: 'Knife' }] + const { errors } = check(validInput) + + expect(errors).toEqual([]) + }) + + it('Should trim empty-string weapons observed identifiers', () => { + validInput.weaponsObserved = 'YES' + validInput.weaponTypes = [{ weaponType: ' gun ', age: 'knife' }, { weaponType: '' }, { weaponType: 'GUN' }] + + const { errors, formResponse } = check(validInput) + + expect(errors).toEqual([]) + + expect(formResponse).toEqual({ + batonDrawn: true, + batonUsed: true, + bodyWornCamera: 'NO', + escortingHold: true, + guidingHold: true, + guidingHoldOfficersInvolved: 2, + handcuffsApplied: true, + painInducingTechniquesUsed: ['FINAL_LOCK_FLEXION', 'THUMB_LOCK'], + pavaDrawn: true, + pavaUsed: true, + personalProtectionTechniques: true, + positiveCommunication: true, + restraintPositions: ['STANDING', 'FACE_DOWN'], + weaponTypes: [ + { + weaponType: 'gun', + }, + { + weaponType: 'GUN', + }, + ], + weaponsObserved: 'YES', + }) + }) + + it('Weapon types identifiers are not required when weaponsObserved is NO', () => { + validInput.weaponsObserved = 'NO' + validInput.weaponTypes = [{ weaponType: 'Gun' }] + + const { errors, formResponse } = check(validInput) + + expect(errors).toEqual([]) + + expect(formResponse).toEqual({ + batonDrawn: true, + batonUsed: true, + bodyWornCamera: 'NO', + weaponsObserved: 'NO', + escortingHold: true, + guidingHold: true, + guidingHoldOfficersInvolved: 2, + handcuffsApplied: true, + painInducingTechniquesUsed: ['FINAL_LOCK_FLEXION', 'THUMB_LOCK'], + pavaDrawn: true, + pavaUsed: true, + personalProtectionTechniques: true, + positiveCommunication: true, + restraintPositions: ['STANDING', 'FACE_DOWN'], + }) + }) + + it('Weapons observed field must be one of allowed values', () => { + validInput.weaponsObserved = 'SOMETHING_RANDOM' + validInput.weaponTypes = [{ weaponType: 'gun' }] + const { errors } = check(validInput) + + expect(errors).toEqual([ + { + href: '#weaponsObserved', + text: 'Select yes if any weapons were observed', + }, + ]) + }) + it("Not selecting an option for 'guiding hold' returns validation error message plus 'how many officers involved' is undefined", () => { const input = { ...validInput, @@ -515,6 +624,7 @@ describe('partial schema', () => { batonUsed: true, pavaDrawn: true, pavaUsed: true, + weaponsObserved: 'NO', guidingHold: true, guidingHoldOfficersInvolved: 2, escortingHold: true, @@ -536,19 +646,23 @@ describe('partial schema', () => { it('Should return no error messages when dependent answers are absent', () => { const { errors, formResponse } = check({ batonDrawn: 'true', + bodyWornCamera: 'YES', pavaDrawn: 'true', guidingHold: 'true', escortingHold: 'true', restraintPositions: 'NONE', + weaponsObserved: 'YES', }) expect(errors).toEqual([]) expect(formResponse).toEqual({ batonDrawn: true, + bodyWornCamera: 'YES', guidingHold: true, escortingHold: true, pavaDrawn: true, restraintPositions: 'NONE', + weaponsObserved: 'YES', }) }) it('Selecting only a child control technique returns a validation error message', () => { diff --git a/server/config/types.ts b/server/config/types.ts index b566540f..0980a3a3 100644 --- a/server/config/types.ts +++ b/server/config/types.ts @@ -27,6 +27,11 @@ export const BodyWornCameras = toEnum({ NOT_KNOWN: { value: 'NOT_KNOWN', label: 'Not Known' }, }) +export const WeaponsObserved = toEnum({ + YES: { value: 'YES', label: 'Yes' }, + NO: { value: 'NO', label: 'No' }, +}) + export const Cctv = toEnum({ YES: { value: 'YES', label: 'Yes' }, NO: { value: 'NO', label: 'No' }, diff --git a/server/data/UseOfForceReport.ts b/server/data/UseOfForceReport.ts index d76e9e6a..83b75e5e 100644 --- a/server/data/UseOfForceReport.ts +++ b/server/data/UseOfForceReport.ts @@ -14,6 +14,8 @@ export type UseOfForceDetails = { batonUsed: boolean pavaDrawn: boolean pavaUsed: boolean + weaponsObserved: string + weaponTypes: { weaponType: string }[] guidingHold: boolean guidingHoldOfficersInvolved: number escortingHold?: boolean diff --git a/server/routes/creatingReports/createReport.test.ts b/server/routes/creatingReports/createReport.test.ts index ce395ee1..6a5efe21 100644 --- a/server/routes/creatingReports/createReport.test.ts +++ b/server/routes/creatingReports/createReport.test.ts @@ -44,6 +44,7 @@ const validUseOfForceDetailsRequest = { personalProtectionTechniques: 'false', batonDrawn: 'false', pavaDrawn: 'false', + weaponsObserved: 'NO', guidingHold: 'false', escortingHold: 'false', painInducingTechniquesUsed: 'NONE', @@ -65,6 +66,7 @@ const validUseOfForceDetailUpdate = [ handcuffsApplied: false, painInducingTechniquesUsed: 'NONE', pavaDrawn: false, + weaponsObserved: 'NO', personalProtectionTechniques: false, positiveCommunication: false, restraintPositions: 'NONE', @@ -122,6 +124,7 @@ describe('POST save and return to tasklist', () => { handcuffsApplied: false, painInducingTechniquesUsed: 'NONE', pavaDrawn: false, + weaponsObserved: 'NO', personalProtectionTechniques: false, positiveCommunication: false, restraintPositions: 'NONE', diff --git a/server/services/drafts/reportStatusChecker.test.ts b/server/services/drafts/reportStatusChecker.test.ts index 6a093428..d9fbec3f 100644 --- a/server/services/drafts/reportStatusChecker.test.ts +++ b/server/services/drafts/reportStatusChecker.test.ts @@ -24,6 +24,8 @@ describe('statusCheck', () => { restraintPositions: 'NONE', bodyWornCamera: 'YES', bodyWornCameraNumbers: [{ cameraNum: '1111' }, { cameraNum: '2222' }], + weaponsObserved: 'YES', + weaponTypes: [{ weaponType: 'gun' }], }, evidence: { cctvRecording: 'NO', diff --git a/server/services/reportDetailBuilder.test.ts b/server/services/reportDetailBuilder.test.ts index e038832a..08131ffe 100644 --- a/server/services/reportDetailBuilder.test.ts +++ b/server/services/reportDetailBuilder.test.ts @@ -114,6 +114,7 @@ describe('Build details', () => { handcuffsApplied: undefined, painInducingTechniques: undefined, pavaDrawn: undefined, + weaponsObserved: undefined, personalProtectionTechniques: undefined, positiveCommunicationUsed: undefined, primaryReason: undefined, diff --git a/server/services/reportSummary.ts b/server/services/reportSummary.ts index ca5df25d..8f22f722 100644 --- a/server/services/reportSummary.ts +++ b/server/services/reportSummary.ts @@ -8,6 +8,7 @@ import { toLabel, UofReasons, findEnum, + WeaponsObserved, } from '../config/types' import { Prison } from '../data/prisonClientTypes' import { @@ -81,6 +82,12 @@ const createUseOfForceDetails = ( ? `${YES} - ${extractCommaSeparatedList('cameraNum', bodyWornCameraNumbers)}` || YES : toLabel(BodyWornCameras, value) ), + + weaponsObserved: whenPresent(details.weaponsObserved, value => + value === WeaponsObserved.YES.value + ? `${YES} - ${extractCommaSeparatedList('weaponType', details.weaponTypes)}` || YES + : toLabel(WeaponsObserved, value) + ), } } diff --git a/server/views/formPages/incident/useOfForceDetails.html b/server/views/formPages/incident/useOfForceDetails.html index 1d103f7c..1d7dd500 100644 --- a/server/views/formPages/incident/useOfForceDetails.html +++ b/server/views/formPages/incident/useOfForceDetails.html @@ -32,13 +32,13 @@