From 4326a9324752fd916c293f626242e0777dadb4c8 Mon Sep 17 00:00:00 2001 From: GCheema <50441412+GurnankCheema@users.noreply.github.com> Date: Thu, 25 Jan 2024 10:28:24 +0000 Subject: [PATCH] Map 581 move question on body worn cameras to details screen (#672) * MAP-581 moved body camera question to report details page and updated unit tests * MAP-581 update integration tests * MAP-581 fix breaking test * map-581 get body-camera details from 'evidence' and use in 'details' section * map-581 made macros to be more generic * MAP-581 remove temporary stop on integ tests --- assets/js/add-another-evidence.js | 2 +- .../createIncident/form-submit.cy.js | 6 + .../enter-use-of-force-details.cy.js | 14 ++ integration-tests/integration/seedData.js | 4 +- .../pages/createReport/evidencePage.js | 9 - .../createReport/useOfForceDetailsPage.js | 7 + server/config/forms/evidenceForm.js | 21 --- .../config/forms/evidenceValidation.test.js | 174 +----------------- server/config/forms/useOfForceDetailsForm.js | 32 +++- .../forms/useOfForceDetailsValidation.test.js | 146 ++++++++++++++- server/data/UseOfForceReport.ts | 6 +- .../creatingReports/createReport.test.ts | 11 +- .../drafts/reportStatusChecker.test.ts | 4 +- server/services/reportDetailBuilder.test.ts | 2 +- server/services/reportSummary.test.ts | 33 +++- server/services/reportSummary.ts | 20 +- server/views/formPages/incident/evidence.html | 103 +---------- .../formPages/incident/useOfForceDetails.html | 36 +++- server/views/formPages/incidentMacros.njk | 120 ++++++++++++ server/views/pages/reportDetailMacro.njk | 21 ++- 20 files changed, 424 insertions(+), 347 deletions(-) diff --git a/assets/js/add-another-evidence.js b/assets/js/add-another-evidence.js index 95e34e09..7723d5e8 100644 --- a/assets/js/add-another-evidence.js +++ b/assets/js/add-another-evidence.js @@ -1,2 +1,2 @@ new AddAnother($('.add-another-evidence'), '.remove-button-container') -new AddAnother($('.add-another-camera'), '.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 c5a46271..66033b49 100644 --- a/integration-tests/integration/createIncident/form-submit.cy.js +++ b/integration-tests/integration/createIncident/form-submit.cy.js @@ -78,6 +78,12 @@ context('Submit the incident report', () => { const useOfForceDetailsPage = UseOfForceDetailsPage.verifyOnPage() useOfForceDetailsPage.fillForm() + useOfForceDetailsPage.bodyWornCamera().check('YES') + useOfForceDetailsPage.bodyWornCameraNumber(0).type('123') + useOfForceDetailsPage.addAnotherBodyWornCamera() + useOfForceDetailsPage.bodyWornCameraNumber(1).type('789') + useOfForceDetailsPage.addAnotherBodyWornCamera() + useOfForceDetailsPage.bodyWornCameraNumber(2).type('456') 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 05dc650b..11827a83 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,3 +1,4 @@ +const { use } = require('passport') const { offender } = require('../../mockApis/data') const ReportUseOfForcePage = require('../../pages/createReport/reportUseOfForcePage') @@ -21,6 +22,11 @@ context('Enter use of force details page', () => { const useOfForceDetailsPage = UseOfForceDetailsPage.verifyOnPage() useOfForceDetailsPage.positiveCommunication().check('true') + useOfForceDetailsPage.bodyWornCamera().check('YES') + useOfForceDetailsPage.bodyWornCameraNumber(0).type('123') + useOfForceDetailsPage.addAnotherBodyWornCamera() + useOfForceDetailsPage.bodyWornCameraNumber(1).type('345') + useOfForceDetailsPage.removeBodyWornCamera(0) useOfForceDetailsPage.personalProtectionTechniques().check('true') useOfForceDetailsPage.batonDrawn().check('true') useOfForceDetailsPage.batonUsed().check('true') @@ -45,6 +51,8 @@ context('Enter use of force details page', () => { cy.task('getFormSection', { bookingId: offender.bookingId, formName: 'useOfForceDetails' }).then(({ section }) => { expect(section).to.deep.equal({ + bodyWornCamera: 'YES', + bodyWornCameraNumbers: [{ cameraNum: '345' }], batonDrawn: true, batonUsed: true, guidingHold: true, @@ -70,6 +78,8 @@ context('Enter use of force details page', () => { cy.task('getFormSection', { bookingId: offender.bookingId, formName: 'useOfForceDetails' }).then(({ section }) => { expect(section).to.deep.equal({ + bodyWornCamera: 'YES', + bodyWornCameraNumbers: [{ cameraNum: '345' }], batonDrawn: true, batonUsed: true, guidingHold: true, @@ -96,6 +106,8 @@ context('Enter use of force details page', () => { const useOfForceDetailsPage = UseOfForceDetailsPage.verifyOnPage() useOfForceDetailsPage.positiveCommunication().should('have.value', 'true') + useOfForceDetailsPage.bodyWornCamera().should('have.value', 'YES') + useOfForceDetailsPage.bodyWornCameraNumber(0).should('have.value', '345') useOfForceDetailsPage.personalProtectionTechniques().should('have.value', 'true') useOfForceDetailsPage.batonDrawn().should('have.value', 'true') useOfForceDetailsPage.batonUsed().should('have.value', 'true') @@ -123,6 +135,7 @@ context('Enter use of force details page', () => { const useOfForceDetailsPage = UseOfForceDetailsPage.verifyOnPage() useOfForceDetailsPage.positiveCommunication().check('true') + useOfForceDetailsPage.bodyWornCamera().check('YES') useOfForceDetailsPage.personalProtectionTechniques().check('true') useOfForceDetailsPage.pavaDrawn().check('true') useOfForceDetailsPage.pavaUsed().check('true') @@ -134,5 +147,6 @@ context('Enter use of force details page', () => { useOfForceDetailsPage.painInducingTechniques().check('true') useOfForceDetailsPage.clickSaveAndContinue() useOfForceDetailsPage.errorSummary().contains('Select yes if a baton was drawn') + useOfForceDetailsPage.errorSummary().contains('Enter the body-worn camera number') }) }) diff --git a/integration-tests/integration/seedData.js b/integration-tests/integration/seedData.js index 998908a0..55cb54fc 100644 --- a/integration-tests/integration/seedData.js +++ b/integration-tests/integration/seedData.js @@ -2,9 +2,7 @@ const expectedPayload = { evidence: { cctvRecording: 'NOT_KNOWN', baggedEvidence: true, - bodyWornCamera: 'YES', photographsTaken: true, - bodyWornCameraNumbers: [{ cameraNum: '123' }, { cameraNum: '789' }, { cameraNum: '456' }], evidenceTagAndDescription: [ { description: 'This evidence was collected from the prisoner 1', evidenceTagReference: 'Bagged evidence 1' }, { description: 'This evidence was collected from the prisoner 2', evidenceTagReference: 'Bagged evidence 2' }, @@ -25,6 +23,8 @@ const expectedPayload = { reasons: ['FIGHT_BETWEEN_PRISONERS'], }, useOfForceDetails: { + bodyWornCamera: 'YES', + bodyWornCameraNumbers: [{ cameraNum: '123' }, { cameraNum: '789' }, { cameraNum: '456' }], pavaUsed: true, batonUsed: true, pavaDrawn: true, diff --git a/integration-tests/pages/createReport/evidencePage.js b/integration-tests/pages/createReport/evidencePage.js index b5c6ffb6..7e0686aa 100644 --- a/integration-tests/pages/createReport/evidencePage.js +++ b/integration-tests/pages/createReport/evidencePage.js @@ -20,15 +20,6 @@ const evidencePage = () => cy.get('[name="evidenceTagAndDescription[2][description]"]').type('Clothes samples') this.photosTaken().check('true') cy.get('[name="cctvRecording"]').check('NOT_KNOWN') - cy.get('[name="bodyWornCamera"]').check('YES') - cy.get('[name="bodyWornCameraNumbers[0][cameraNum]"]').type('123') - cy.get('[data-qa-add-another-camera = true]').click() - cy.get('[name="bodyWornCameraNumbers[1][cameraNum]"]').type('456') - cy.get('[data-qa-add-another-camera = true]').click() - cy.get('[name="bodyWornCameraNumbers[2][cameraNum]"]').type('789') - cy.get('.add-another-camera .add-another__remove-button').eq(1).click() - cy.get('[data-qa-add-another-camera = true]').click() - cy.get('[name="bodyWornCameraNumbers[2][cameraNum]"]').type('456') }, save: () => { diff --git a/integration-tests/pages/createReport/useOfForceDetailsPage.js b/integration-tests/pages/createReport/useOfForceDetailsPage.js index 8006b975..604605d4 100644 --- a/integration-tests/pages/createReport/useOfForceDetailsPage.js +++ b/integration-tests/pages/createReport/useOfForceDetailsPage.js @@ -4,6 +4,12 @@ import RelocationAndInjuriesPage from './relocationAndInjuriesPage' const useOfForceDetailsPage = () => page('Use of force details', { positiveCommunication: () => cy.get('[name="positiveCommunication"]'), + + 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(), + personalProtectionTechniques: () => cy.get('[name="personalProtectionTechniques"]'), batonDrawn: () => cy.get('[name="batonDrawn"]'), batonUsed: () => cy.get('[name="batonUsed"]'), @@ -45,6 +51,7 @@ const useOfForceDetailsPage = () => fillForm() { this.positiveCommunication().check('true') + this.bodyWornCamera().check('NO') this.personalProtectionTechniques().check('true') this.batonDrawn().check('true') this.batonUsed().check('true') diff --git a/server/config/forms/evidenceForm.js b/server/config/forms/evidenceForm.js index 92ead36b..de547977 100644 --- a/server/config/forms/evidenceForm.js +++ b/server/config/forms/evidenceForm.js @@ -39,27 +39,6 @@ const completeSchema = joi.object({ 'NO', 'NOT_KNOWN' )('Select yes if any part of the incident captured on CCTV').alter(optionalForPartialValidation), - - bodyWornCamera: requiredOneOfMsg( - 'YES', - 'NO', - 'NOT_KNOWN' - )('Select yes if any part of the incident was captured on a body-worn camera').alter(optionalForPartialValidation), - bodyWornCameraNumbers: joi - .when('bodyWornCamera', { - is: 'YES', - then: arrayOfObjects({ - cameraNum: requiredStringMsg('Enter the body-worn camera number').alter(optionalForPartialValidation), - }) - .min(1) - .message('Enter the body-worn camera number') - .ruleset.unique('cameraNum') - .message("Camera '{#value.cameraNum}' has already been added - remove this camera") - .required() - .alter(minZeroForPartialValidation), - otherwise: joi.any().strip(), - }) - .meta({ firstFieldName: 'bodyWornCameraNumbers[0]' }), }) module.exports = { diff --git a/server/config/forms/evidenceValidation.test.js b/server/config/forms/evidenceValidation.test.js index 1bb5b788..0957672a 100644 --- a/server/config/forms/evidenceValidation.test.js +++ b/server/config/forms/evidenceValidation.test.js @@ -20,8 +20,6 @@ const validInput = () => ({ ], photographsTaken: 'true', cctvRecording: 'YES', - bodyWornCamera: 'YES', - bodyWornCameraNumbers: [{ cameraNum: 'ABC123' }, { cameraNum: '' }], }) describe("'complete' validation", () => { @@ -34,8 +32,6 @@ describe("'complete' validation", () => { expect(formResponse).toEqual({ baggedEvidence: true, - bodyWornCamera: 'YES', - bodyWornCameraNumbers: [{ cameraNum: 'ABC123' }], cctvRecording: 'YES', evidenceTagAndDescription: [{ description: 'A Description', evidenceTagReference: '12345' }], photographsTaken: true, @@ -58,10 +54,6 @@ describe("'complete' validation", () => { href: '#cctvRecording', text: 'Select yes if any part of the incident captured on CCTV', }, - { - href: '#bodyWornCamera', - text: 'Select yes if any part of the incident was captured on a body-worn camera', - }, ]) expect(formResponse).toEqual({}) @@ -85,8 +77,6 @@ describe("'complete' validation", () => { ]) expect(formResponse).toEqual({ - bodyWornCamera: 'YES', - bodyWornCameraNumbers: [{ cameraNum: 'ABC123' }], cctvRecording: 'YES', photographsTaken: true, }) @@ -122,8 +112,6 @@ describe("'complete' validation", () => { expect(formResponse).toEqual({ baggedEvidence: true, - bodyWornCamera: 'YES', - bodyWornCameraNumbers: [{ cameraNum: 'ABC123' }], cctvRecording: 'YES', evidenceTagAndDescription: [ { description: 'A Description', evidenceTagReference: '12345' }, @@ -152,8 +140,6 @@ describe("'complete' validation", () => { expect(formResponse).toEqual({ baggedEvidence: true, - bodyWornCamera: 'YES', - bodyWornCameraNumbers: [{ cameraNum: 'ABC123' }], cctvRecording: 'YES', photographsTaken: true, }) @@ -177,8 +163,6 @@ describe("'complete' validation", () => { expect(formResponse).toEqual({ baggedEvidence: true, - bodyWornCamera: 'YES', - bodyWornCameraNumbers: [{ cameraNum: 'ABC123' }], cctvRecording: 'YES', photographsTaken: true, evidenceTagAndDescription: [{ description: '', evidenceTagReference: 'ref-1' }], @@ -198,123 +182,16 @@ describe("'complete' validation", () => { expect(formResponse).toEqual({ baggedEvidence: false, - bodyWornCamera: 'YES', - bodyWornCameraNumbers: [{ cameraNum: 'ABC123' }], cctvRecording: 'YES', photographsTaken: true, }) }) }) - describe('Body Worn Cameras', () => { - it('Conditional field not selected - errors and filters out camera number list', () => { - const input = { ...validInput(), bodyWornCamera: undefined, bodyWornCameraNumbers: [{ cameraNum: 'AAA123' }] } - const { errors, formResponse } = check(input) - - expect(errors).toEqual([ - { - href: '#bodyWornCamera', - text: 'Select yes if any part of the incident was captured on a body-worn camera', - }, - ]) - - expect(formResponse).toEqual({ - baggedEvidence: true, - cctvRecording: 'YES', - evidenceTagAndDescription: [{ description: 'A Description', evidenceTagReference: '12345' }], - photographsTaken: true, - }) - }) - - it('Conditional field must be one of allowed values', () => { - const input = { ...validInput(), bodyWornCamera: 'BOB', bodyWornCameraNumbers: [{ cameraNum: 'AAA123' }] } - const { errors, formResponse } = check(input) - - expect(errors).toEqual([ - { - href: '#bodyWornCamera', - text: 'Select yes if any part of the incident was captured on a body-worn camera', - }, - ]) - - expect(formResponse).toEqual({ - baggedEvidence: true, - cctvRecording: 'YES', - bodyWornCamera: 'BOB', - evidenceTagAndDescription: [{ description: 'A Description', evidenceTagReference: '12345' }], - photographsTaken: true, - }) - }) - - it('Conditional field selected: Yes, check at least one number is present', () => { - const input = { ...validInput(), bodyWornCamera: 'YES', bodyWornCameraNumbers: [] } - const { errors, formResponse } = check(input) - - expect(errors).toEqual([ - { - href: '#bodyWornCameraNumbers[0]', - text: 'Enter the body-worn camera number', - }, - ]) - - expect(formResponse).toEqual({ - baggedEvidence: true, - bodyWornCamera: 'YES', - cctvRecording: 'YES', - evidenceTagAndDescription: [{ description: 'A Description', evidenceTagReference: '12345' }], - photographsTaken: true, - }) - }) - - it('Conditional field selected: Yes, empty numbers are ignored', () => { - const input = { - ...validInput(), - bodyWornCamera: 'YES', - bodyWornCameraNumbers: [{ cameraNum: 'AAA' }, { cameraNum: '' }, { cameraNum: 'BBB' }], - } - const { errors, formResponse } = check(input) - - expect(errors).toEqual([]) - - expect(formResponse).toEqual({ - baggedEvidence: true, - bodyWornCamera: 'YES', - bodyWornCameraNumbers: [{ cameraNum: 'AAA' }, { cameraNum: 'BBB' }], - cctvRecording: 'YES', - evidenceTagAndDescription: [{ description: 'A Description', evidenceTagReference: '12345' }], - photographsTaken: true, - }) - }) - - it('Duplicate camera numbers are rejected', () => { - const input = { - ...validInput(), - bodyWornCamera: 'YES', - bodyWornCameraNumbers: [{ cameraNum: 'AAA' }, { cameraNum: '' }, { cameraNum: 'AAA' }], - } - const { errors, formResponse } = check(input) - - expect(errors).toEqual([ - { - href: '#bodyWornCameraNumbers[1]', - text: "Camera 'AAA' has already been added - remove this camera", - }, - ]) - - expect(formResponse).toEqual({ - baggedEvidence: true, - bodyWornCamera: 'YES', - bodyWornCameraNumbers: [{ cameraNum: 'AAA' }, { cameraNum: 'AAA' }], - cctvRecording: 'YES', - evidenceTagAndDescription: [{ description: 'A Description', evidenceTagReference: '12345' }], - photographsTaken: true, - }) - }) - + describe('Evidence tags', () => { it('Duplicate evidence tags are rejected', () => { const input = { ...validInput(), - bodyWornCamera: 'NO', evidenceTagAndDescription: [ { description: 'D2', evidenceTagReference: '12345' }, { description: 'D1', evidenceTagReference: '12345' }, @@ -331,7 +208,6 @@ describe("'complete' validation", () => { expect(formResponse).toEqual({ baggedEvidence: true, - bodyWornCamera: 'NO', cctvRecording: 'YES', evidenceTagAndDescription: [ { description: 'D2', evidenceTagReference: '12345' }, @@ -340,45 +216,6 @@ describe("'complete' validation", () => { photographsTaken: true, }) }) - - it('Conditional field selected: Yes, unknown fields are ignored, known fields are trimmed', () => { - const input = { - ...validInput(), - bodyWornCamera: 'YES', - bodyWornCameraNumbers: [{ cameraNum: ' AAA ', age: '29' }, { cameraNum: '' }, { cameraNum: 'BBB' }], - } - const { errors, formResponse } = check(input) - - expect(errors).toEqual([]) - - expect(formResponse).toEqual({ - baggedEvidence: true, - bodyWornCamera: 'YES', - bodyWornCameraNumbers: [{ cameraNum: 'AAA' }, { cameraNum: 'BBB' }], - cctvRecording: 'YES', - evidenceTagAndDescription: [{ description: 'A Description', evidenceTagReference: '12345' }], - photographsTaken: true, - }) - }) - - it('Conditional field selected: No, numbers are not required', () => { - const input = { - ...validInput(), - bodyWornCamera: 'NO', - bodyWornCameraNumbers: [{ cameraNum: 'AAA' }, { cameraNum: '' }, { cameraNum: 'AAA' }], - } - const { errors, formResponse } = check(input) - - expect(errors).toEqual([]) - - expect(formResponse).toEqual({ - baggedEvidence: true, - bodyWornCamera: 'NO', - cctvRecording: 'YES', - evidenceTagAndDescription: [{ description: 'A Description', evidenceTagReference: '12345' }], - photographsTaken: true, - }) - }) }) }) @@ -394,12 +231,6 @@ describe("'partial' validation", () => { const { errors, formResponse } = check(validInput()) expect(formResponse).toEqual({ baggedEvidence: true, - bodyWornCamera: 'YES', - bodyWornCameraNumbers: [ - { - cameraNum: 'ABC123', - }, - ], cctvRecording: 'YES', evidenceTagAndDescription: [ { @@ -416,12 +247,9 @@ describe("'partial' validation", () => { const { errors, formResponse } = check({ baggedEvidence: 'true', evidenceTagAndDescription: [], - bodyWornCamera: 'YES', - bodyWornCameraNumbers: [], }) expect(formResponse).toEqual({ baggedEvidence: true, - bodyWornCamera: 'YES', }) expect(errors).toEqual([]) }) diff --git a/server/config/forms/useOfForceDetailsForm.js b/server/config/forms/useOfForceDetailsForm.js index 39ac0e6d..57d262be 100644 --- a/server/config/forms/useOfForceDetailsForm.js +++ b/server/config/forms/useOfForceDetailsForm.js @@ -2,13 +2,43 @@ const joi = require('@hapi/joi') const { validations } = require('./validations') const { buildValidationSpec } = require('../../services/validation') -const { requiredBooleanMsg, requiredOneOfMsg, requiredIntegerRangeMsg, optionalForPartialValidation } = validations +const { + requiredBooleanMsg, + requiredOneOfMsg, + requiredIntegerRangeMsg, + optionalForPartialValidation, + arrayOfObjects, + requiredStringMsg, + minZeroForPartialValidation, +} = validations const completeSchema = joi.object({ positiveCommunication: requiredBooleanMsg('Select yes if positive communication was used').alter( optionalForPartialValidation ), + bodyWornCamera: requiredOneOfMsg( + 'YES', + 'NO', + 'NOT_KNOWN' + )('Select yes if any part of the incident was captured on a body-worn camera').alter(optionalForPartialValidation), + + bodyWornCameraNumbers: joi + .when('bodyWornCamera', { + is: 'YES', + then: arrayOfObjects({ + cameraNum: requiredStringMsg('Enter the body-worn camera number').alter(optionalForPartialValidation), + }) + .min(1) + .message('Enter the body-worn camera number') + .ruleset.unique('cameraNum') + .message("Camera '{#value.cameraNum}' has already been added - remove this camera") + .required() + .alter(minZeroForPartialValidation), + otherwise: joi.any().strip(), + }) + .meta({ firstFieldName: 'bodyWornCameraNumbers[0]' }), + personalProtectionTechniques: requiredBooleanMsg('Select yes if any personal protection techniques were used').alter( optionalForPartialValidation ), diff --git a/server/config/forms/useOfForceDetailsValidation.test.js b/server/config/forms/useOfForceDetailsValidation.test.js index 44ff3a65..9ce9b628 100644 --- a/server/config/forms/useOfForceDetailsValidation.test.js +++ b/server/config/forms/useOfForceDetailsValidation.test.js @@ -11,6 +11,7 @@ let validInput = {} beforeEach(() => { validInput = { positiveCommunication: 'true', + bodyWornCamera: 'NO', personalProtectionTechniques: 'true', batonDrawn: 'true', batonUsed: 'true', @@ -38,6 +39,7 @@ describe('complete schema', () => { expect(formResponse).toEqual({ positiveCommunication: true, + bodyWornCamera: 'NO', personalProtectionTechniques: true, batonDrawn: true, batonUsed: true, @@ -54,7 +56,7 @@ describe('complete schema', () => { }) }) - it('Should return 9 error massages if no input field is completed', () => { + it('Should return 10 error massages if no input field is completed', () => { const input = {} const { errors, formResponse } = check(input) @@ -63,6 +65,10 @@ describe('complete schema', () => { href: '#positiveCommunication', text: 'Select yes if positive communication was used', }, + { + href: '#bodyWornCamera', + text: 'Select yes if any part of the incident was captured on a body-worn camera', + }, { href: '#personalProtectionTechniques', text: 'Select yes if any personal protection techniques were used', @@ -97,7 +103,7 @@ describe('complete schema', () => { }, ]) - expect(errors.length).toEqual(9) + expect(errors.length).toEqual(10) expect(formResponse).toEqual({}) }) @@ -118,6 +124,141 @@ describe('complete schema', () => { ]) }) + it("Not selecting an option for 'body worn cameras' returns a validation error message", () => { + const input = { + ...validInput, + bodyWornCamera: undefined, + } + const { errors } = check(input) + expect(errors).toEqual([ + { + href: '#bodyWornCamera', + text: 'Select yes if any part of the incident was captured on a body-worn camera', + }, + ]) + }) + + it('Not adding camera numbers but selecting YES for bodyWornCameras returns validation error messages', () => { + validInput.bodyWornCamera = 'YES' + const { errors } = check(validInput) + + expect(errors).toEqual([{ href: '#bodyWornCameraNumbers[0]', text: '"bodyWornCameraNumbers" is required' }]) + }) + + it('Should return validation error if more than one body-worn camera with same identifier', () => { + validInput.bodyWornCamera = 'YES' + validInput.bodyWornCameraNumbers = [{ cameraNum: '1' }, { cameraNum: '1' }] + const { errors } = check(validInput) + + expect(errors).toEqual([ + { href: '#bodyWornCameraNumbers[1]', text: "Camera '1' has already been added - remove this camera" }, + ]) + }) + + it('Should not return validation error if all body-worn camera identifiers are unique', () => { + validInput.bodyWornCamera = 'YES' + validInput.bodyWornCameraNumbers = [{ cameraNum: '1' }, { cameraNum: '2' }] + const { errors } = check(validInput) + + expect(errors).toEqual([]) + }) + + it('Should trim empty-string body-worn camera identifiers', () => { + validInput.bodyWornCamera = 'YES' + validInput.bodyWornCameraNumbers = [ + { cameraNum: ' AAA ', age: '29' }, + { cameraNum: '' }, + { cameraNum: 'BBB' }, + ] + + const { errors, formResponse } = check(validInput) + + expect(errors).toEqual([]) + + expect(formResponse).toEqual({ + batonDrawn: true, + batonUsed: true, + bodyWornCamera: 'YES', + bodyWornCameraNumbers: [ + { + cameraNum: 'AAA', + }, + { + cameraNum: 'BBB', + }, + ], + escortingHold: true, + guidingHold: true, + guidingHoldOfficersInvolved: 2, + handcuffsApplied: true, + painInducingTechniques: true, + painInducingTechniquesUsed: ['FINAL_LOCK_FLEXION', 'THUMB_LOCK'], + pavaDrawn: true, + pavaUsed: true, + personalProtectionTechniques: true, + positiveCommunication: true, + restraint: true, + restraintPositions: ['STANDING', 'FACE_DOWN'], + }) + }) + + it('Body-worn camera identifiers are not required when bodyWornCamera is NO', () => { + validInput.bodyWornCamer = 'NO' + validInput.bodyWornCameraNumbers = [{ cameraNum: 'AAA' }, { cameraNum: '' }, { cameraNum: 'AAA' }] + + 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, + painInducingTechniques: true, + painInducingTechniquesUsed: ['FINAL_LOCK_FLEXION', 'THUMB_LOCK'], + pavaDrawn: true, + pavaUsed: true, + personalProtectionTechniques: true, + positiveCommunication: true, + restraint: true, + restraintPositions: ['STANDING', 'FACE_DOWN'], + }) + }) + + it('Body worn camera field must be one of allowed values', () => { + validInput.bodyWornCamera = 'SOMETHING_RANDOM' + validInput.bodyWornCameraNumbers = [{ cameraNum: 'AAA' }, { cameraNum: '' }, { cameraNum: 'AAA' }] + const { errors, formResponse } = check(validInput) + + expect(errors).toEqual([ + { + href: '#bodyWornCamera', + text: 'Select yes if any part of the incident was captured on a body-worn camera', + }, + ]) + + expect(formResponse).toEqual({ + batonDrawn: true, + batonUsed: true, + bodyWornCamera: 'SOMETHING_RANDOM', + escortingHold: true, + guidingHold: true, + guidingHoldOfficersInvolved: 2, + handcuffsApplied: true, + painInducingTechniques: true, + painInducingTechniquesUsed: ['FINAL_LOCK_FLEXION', 'THUMB_LOCK'], + pavaDrawn: true, + pavaUsed: true, + personalProtectionTechniques: true, + positiveCommunication: true, + restraint: true, + restraintPositions: ['STANDING', 'FACE_DOWN'], + }) + }) it("Not selecting an option for 'personal protection techniques' returns a validation error message", () => { const input = { ...validInput, @@ -408,6 +549,7 @@ describe('partial schema', () => { expect(formResponse).toEqual({ positiveCommunication: true, + bodyWornCamera: 'NO', personalProtectionTechniques: true, batonDrawn: true, batonUsed: true, diff --git a/server/data/UseOfForceReport.ts b/server/data/UseOfForceReport.ts index 12cbc92c..bcbdc2b4 100644 --- a/server/data/UseOfForceReport.ts +++ b/server/data/UseOfForceReport.ts @@ -7,6 +7,8 @@ export type IncidentDetails = { export type UseOfForceDetails = { positiveCommunication: boolean + bodyWornCamera?: string + bodyWornCameraNumbers?: { cameraNum: string }[] personalProtectionTechniques: boolean batonDrawn: boolean batonUsed: boolean @@ -46,8 +48,8 @@ export type Evidence = { baggedEvidence: boolean photographsTaken: boolean cctvRecording: string - bodyWornCamera: string - bodyWornCameraNumbers: { cameraNum: string }[] + bodyWornCamera?: string + bodyWornCameraNumbers?: { cameraNum: string }[] } export type InvolvedStaff = { diff --git a/server/routes/creatingReports/createReport.test.ts b/server/routes/creatingReports/createReport.test.ts index 16a5b0ea..14de8fe6 100644 --- a/server/routes/creatingReports/createReport.test.ts +++ b/server/routes/creatingReports/createReport.test.ts @@ -38,6 +38,8 @@ describe('GET /section/form', () => { }) const validUseOfForceDetailsRequest = { + bodyWornCamera: 'YES', + bodyWornCameraNumbers: [{ cameraNum: 'ABC123' }], positiveCommunication: 'false', personalProtectionTechniques: 'false', batonDrawn: 'false', @@ -55,6 +57,8 @@ const validUseofForceDetailUpdate = [ 1, 'useOfForceDetails', { + bodyWornCamera: 'YES', + bodyWornCameraNumbers: [{ cameraNum: 'ABC123' }], batonDrawn: false, guidingHold: false, escortingHold: false, @@ -107,7 +111,7 @@ describe('POST save and return to tasklist', () => { test('Submitting invalid update is allowed', () => { return request(app) .post(`/report/1/use-of-force-details`) - .send({ ...validUseOfForceDetailsRequest, batonDrawn: null, submitType: 'save-and-return' }) + .send({ ...validUseOfForceDetailsRequest, batonDrawn: null, bodyWornCamera: null, submitType: 'save-and-return' }) .expect(302) .expect('Location', '/report/1/report-use-of-force') .expect(() => { @@ -132,6 +136,7 @@ describe('POST save and return to tasklist', () => { ...validUseOfForceDetailsRequest, restraint: 'true', restraintPositions: ['not a valid value'], + bodyWornCamera: ['another invalid input'], submitType: 'save-and-return', }) .expect(302) @@ -186,8 +191,6 @@ describe('Submitting evidence page', () => { .send({ submitType, baggedEvidence: 'true', - bodyWornCamera: 'YES', - bodyWornCameraNumbers: [{ cameraNum: 'ABC123' }], cctvRecording: 'YES', evidenceTagAndDescription: [{ description: 'A Description', evidenceTagReference: '12345' }], photographsTaken: 'true', @@ -199,8 +202,6 @@ describe('Submitting evidence page', () => { expect(draftReportService.process).toBeCalledWith(user, 1, 'evidence', { baggedEvidence: true, - bodyWornCamera: 'YES', - bodyWornCameraNumbers: [{ cameraNum: 'ABC123' }], cctvRecording: 'YES', evidenceTagAndDescription: [{ description: 'A Description', evidenceTagReference: '12345' }], photographsTaken: true, diff --git a/server/services/drafts/reportStatusChecker.test.ts b/server/services/drafts/reportStatusChecker.test.ts index 97b8366b..453c435e 100644 --- a/server/services/drafts/reportStatusChecker.test.ts +++ b/server/services/drafts/reportStatusChecker.test.ts @@ -22,13 +22,13 @@ describe('statusCheck', () => { positiveCommunication: false, personalProtectionTechniques: true, painInducingTechniques: false, + bodyWornCamera: 'YES', + bodyWornCameraNumbers: [{ cameraNum: '1111' }, { cameraNum: '2222' }], }, evidence: { cctvRecording: 'NO', baggedEvidence: true, - bodyWornCamera: 'YES', photographsTaken: false, - bodyWornCameraNumbers: [{ cameraNum: '1111' }, { cameraNum: '2222' }], evidenceTagAndDescription: [ { description: 'aaaaa', evidenceTagReference: '1111' }, { description: 'bbbb', evidenceTagReference: '2222' }, diff --git a/server/services/reportDetailBuilder.test.ts b/server/services/reportDetailBuilder.test.ts index 148903fd..e038832a 100644 --- a/server/services/reportDetailBuilder.test.ts +++ b/server/services/reportDetailBuilder.test.ts @@ -55,7 +55,6 @@ describe('Build details', () => { expect(result).toStrictEqual({ bookingId: 33, evidence: { - bodyCameras: undefined, cctv: undefined, evidenceBaggedTagged: 'No', photographs: undefined, @@ -107,6 +106,7 @@ describe('Build details', () => { reporterName: 'A User', submittedDate: new Date('2015-03-25T12:00:00.000Z'), useOfForceDetails: { + bodyCameras: undefined, batonDrawn: undefined, controlAndRestraintUsed: undefined, guidingHoldUsed: undefined, diff --git a/server/services/reportSummary.test.ts b/server/services/reportSummary.test.ts index d2dcb584..04a7404a 100755 --- a/server/services/reportSummary.test.ts +++ b/server/services/reportSummary.test.ts @@ -3,7 +3,12 @@ import { Prison } from '../data/prisonClientTypes' import { UseOfForceDraftReport } from '../data/UseOfForceReport' import reportSummary from './reportSummary' -const form: UseOfForceDraftReport = { useOfForceDetails: {}, relocationAndInjuries: {}, reasonsForUseOfForce: {} } +const form: UseOfForceDraftReport = { + useOfForceDetails: {}, + relocationAndInjuries: {}, + reasonsForUseOfForce: {}, + evidence: {}, +} const offenderDetail = {} const prison: Prison = { agencyId: 'MDI', description: 'Moorland HMP', active: true, agencyType: 'INST' } const locationDescription = '' @@ -46,7 +51,33 @@ describe('reportSummary', () => { expect(result.useOfForceDetails.painInducingTechniques).toEqual('No') }) }) + describe('Use of force details', () => { + it("should return body-camera details in 'details' even if in 'evidence' data ", () => { + form.useOfForceDetails = {} + form.evidence.bodyWornCamera = 'NO' + const result = reportSummary(form, offenderDetail, prison, locationDescription, involvedStaff, incidentDate) + expect(result.useOfForceDetails.bodyCameras).toEqual('No') + }) + + it("should return body-camera reference numbers within 'details' section even if they are part of 'evidence' data", () => { + form.useOfForceDetails = {} + form.evidence = { + bodyWornCamera: 'YES', + bodyWornCameraNumbers: [{ cameraNum: '1' }, { cameraNum: '2' }], + } + const result = reportSummary(form, offenderDetail, prison, locationDescription, involvedStaff, incidentDate) + expect(result.useOfForceDetails.bodyCameras).toEqual('Yes - 1, 2') + }) + it("should return body-camera details saved in 'details' data", () => { + form.useOfForceDetails = { + bodyWornCamera: 'YES', + bodyWornCameraNumbers: [{ cameraNum: '1' }, { cameraNum: '2' }], + } + const result = reportSummary(form, offenderDetail, prison, locationDescription, involvedStaff, incidentDate) + expect(result.useOfForceDetails.bodyCameras).toEqual('Yes - 1, 2') + }) + }) describe('Use of force reasons', () => { it('should return undefined', () => { form.reasonsForUseOfForce.reasons = undefined diff --git a/server/services/reportSummary.ts b/server/services/reportSummary.ts index 0ddb3658..5e8fc8e4 100644 --- a/server/services/reportSummary.ts +++ b/server/services/reportSummary.ts @@ -47,8 +47,14 @@ const createIncidentDetails = ( const createUseOfForceDetails = ( details: Partial = {}, - reasonsForUseOfForce: Partial = {} + reasonsForUseOfForce: Partial = {}, + evidence: Partial = {} ) => { + const bodyWornCamera = details.bodyWornCamera ? details.bodyWornCamera : evidence.bodyWornCamera + const bodyWornCameraNumbers = details.bodyWornCameraNumbers + ? details.bodyWornCameraNumbers + : evidence.bodyWornCameraNumbers + return { reasonsForUseOfForce: whenPresent(reasonsForUseOfForce.reasons, reasons => reasons.map(value => toLabel(UofReasons, value)).join(', ') @@ -68,6 +74,11 @@ const createUseOfForceDetails = ( painInducingTechniques: getPainInducingTechniques(details), handcuffsApplied: details.handcuffsApplied, + bodyCameras: whenPresent(bodyWornCamera, value => + value === BodyWornCameras.YES.value + ? `${YES} - ${extractCommaSeparatedList('cameraNum', bodyWornCameraNumbers)}` || YES + : toLabel(BodyWornCameras, value) + ), } } @@ -108,11 +119,6 @@ const createEvidence = (evidence: Partial = {}) => { evidenceBaggedTagged: baggedAndTaggedEvidence(evidence.evidenceTagAndDescription, evidence.baggedEvidence), photographs: evidence.photographsTaken, cctv: toLabel(Cctv, evidence.cctvRecording), - bodyCameras: whenPresent(evidence.bodyWornCamera, value => - value === Cctv.YES.value - ? `${YES} - ${extractCommaSeparatedList('cameraNum', evidence.bodyWornCameraNumbers)}` || YES - : toLabel(BodyWornCameras, value) - ), } } @@ -193,7 +199,7 @@ export = ( incidentDate ), offenderDetail, - useOfForceDetails: createUseOfForceDetails(useOfForceDetails, reasonsForUseOfForce), + useOfForceDetails: createUseOfForceDetails(useOfForceDetails, reasonsForUseOfForce, evidence), relocationAndInjuries: createRelocation(relocationAndInjuries), evidence: createEvidence(evidence), } diff --git a/server/views/formPages/incident/evidence.html b/server/views/formPages/incident/evidence.html index 75df3da5..e860168b 100644 --- a/server/views/formPages/incident/evidence.html +++ b/server/views/formPages/incident/evidence.html @@ -1,8 +1,6 @@ {% extends "../formTemplate.html" %} {% from "govuk/components/input/macro.njk" import govukInput %} {% from "govuk/components/radios/macro.njk" import govukRadios %} -{% from "govuk/components/select/macro.njk" import govukSelect %} -{% from "govuk/components/checkboxes/macro.njk" import govukCheckboxes %} {% import "../incidentMacros.njk" as incidentMacro %} {% from "govuk/components/fieldset/macro.njk" import govukFieldset %} {% from "govuk/components/button/macro.njk" import govukButton %} @@ -195,105 +193,6 @@

{{ pageTitle }}

}) }} - - - -
-
- - Was any part of the incident captured on a body-worn camera? - - - Error: - {{ cameraErrMsg.text}} - - -
-
-
- - -
- -
- - -
- {% macro addCameraNumber(index, value, showRemove) %} - {% call govukFieldset({ classes: 'add-another__item' }) %} -
-
- {{ - govukInput({ - label: { - html: 'Camera number' - }, - id: 'bodyWornCameraNumbers[' + index + '][cameraNum]', - name: 'bodyWornCameraNumbers[' + index + '][cameraNum]', - value: value, - errorMessage: errors | findErrors(['bodyWornCameraNumbers[' + index + ']', 'bodyWornCameraNumbers[' + index + '][cameraNum]']), - attributes: { - 'data-name': 'bodyWornCameraNumbers[%index%][cameraNum]', - 'data-id': 'bodyWornCameraNumbers[%index%][cameraNum]' - } - }) - }} -
-
- {% if showRemove %} - - {% endif %} -
-
- {% endcall %} - {% endmacro%} - - {% for camera in data.bodyWornCameraNumbers %} - {{ addCameraNumber(index = loop.index0, value = camera.cameraNum, showRemove = loop.length != 1) }} - {% else %} - {{ addCameraNumber(index = 0, value = null, showRemove = false) }} - {% endfor %} -
- {{ - govukButton({ - text: 'Add another', - classes: 'govuk-button--secondary add-another__add-button govuk-!-margin-bottom-4', - attributes: { 'data-qa-add-another-camera': true } - }) - }} -
-
- -
- - -
- - -
-
- - -
-
-
- -
-
- - @@ -302,4 +201,4 @@

{{ pageTitle }}

{% block script %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/server/views/formPages/incident/useOfForceDetails.html b/server/views/formPages/incident/useOfForceDetails.html index 5a241856..4bfa3993 100644 --- a/server/views/formPages/incident/useOfForceDetails.html +++ b/server/views/formPages/incident/useOfForceDetails.html @@ -31,7 +31,23 @@

{{ pageTitle }}

errorMessage: errors | findError('positiveCommunication') })}} - + + {{ incidentMacro.radioWithMultipleNestedTextBox({ + primaryQuestion:{ + text: "Was any part of the incident captured on a body-worn camera?", + name: "bodyWornCamera", + value: data.bodyWornCamera, + errorMessage: errors | findError('bodyWornCamera') + }, + followUpQuestion:{ + name: "bodyWornCameraNumbers", + value: data.bodyWornCameraNumbers, + errorMessage: errors, + otherIds: ['cameraNum', 'Camera number'] + } + }) }} + + {{ incidentMacro.radio( { text: "Were any personal protection techniques used?", name: "personalProtectionTechniques", @@ -40,7 +56,7 @@

{{ pageTitle }}

errorMessage: errors | findError('personalProtectionTechniques') })}} - + {{ incidentMacro.radiosWithNestedRadios({ primaryQuestion: { text: "Was a baton drawn by anyone during this incident?", @@ -61,7 +77,7 @@

{{ pageTitle }}

} )}} - + {{ incidentMacro.radiosWithNestedRadios({ primaryQuestion: { text: "Was PAVA drawn by anyone during this incident?", @@ -83,7 +99,7 @@

{{ pageTitle }}

} )}} - + {{ incidentMacro.radiosWithNestedRadios({ primaryQuestion: { text: "Was a guiding hold used?", @@ -112,7 +128,7 @@

{{ pageTitle }}

} )}} - + {{ incidentMacro.radio( { text: "Was an escorting hold used?", name: "escortingHold", @@ -121,7 +137,7 @@

{{ pageTitle }}

errorMessage: errors | findError('escortingHold') })}} - +
{{ incidentMacro.radiosWithNestedCheckboxes({ primaryQuestion: { @@ -142,7 +158,7 @@

{{ pageTitle }}

}}
- +
{{ incidentMacro.radiosWithNestedCheckboxes({ primaryQuestion: { @@ -163,7 +179,7 @@

{{ pageTitle }}

}}
- + {{ incidentMacro.radio( { text: "Were handcuffs applied?", name: "handcuffsApplied", @@ -174,4 +190,8 @@

{{ pageTitle }}

+{% endblock %} + +{% block script %} + {% endblock %} \ No newline at end of file diff --git a/server/views/formPages/incidentMacros.njk b/server/views/formPages/incidentMacros.njk index 405dfae9..faa4cc3b 100644 --- a/server/views/formPages/incidentMacros.njk +++ b/server/views/formPages/incidentMacros.njk @@ -1,3 +1,7 @@ +{% from "govuk/components/input/macro.njk" import govukInput %} +{% from "govuk/components/fieldset/macro.njk" import govukFieldset %} +{% from "govuk/components/button/macro.njk" import govukButton %} + {% macro radio(question) %} {% if question.errorMessage.text %} {% set govukFormGroupError = 'govuk-form-group--error' %} @@ -48,6 +52,122 @@ {% endmacro %} +{% macro radioWithMultipleNestedTextBox(question) %} + + {% if question.primaryQuestion.errorMessage %} + {% set govukFormGroupErrorOuter = 'govuk-form-group--error' %} + {% set primaryErrorMessageText = question.primaryQuestion.errorMessage.text %} + {% endif %} + +
+
+ + {{question.primaryQuestion.text}} + + + Error: + {{ primaryErrorMessageText}} + + +
+
+
+ + +
+ +
+ + +
+ {% for item in question.followUpQuestion.value %} + + {{ addAnother( + question.followUpQuestion.otherIds, + question.followUpQuestion.name, + question.followUpQuestion.errorMessage, + index = loop.index0, + value = item[question.followUpQuestion.otherIds[0]], + showRemove = loop.length != 1) + }} + {% else %} + {{ addAnother( + question.followUpQuestion.otherIds, + question.followUpQuestion.name, + question.followUpQuestion.errorMessage, + index = 0, + value = null, + showRemove = false) }} + {% endfor %} + +
+ {{ + govukButton({ + text: 'Add another', + classes: 'govuk-button--secondary add-another__add-button govuk-!-margin-bottom-4', + attributes: { 'data-qa-add-another-input': true } + }) + }} +
+
+ +
+ + +
+ + +
+
+ + +
+
+
+ +
+
+{% endmacro %} + +{% macro addAnother(otherIds, name, errors, index, value, showRemove) %} + {% call govukFieldset({ classes: 'add-another__item' }) %} +
+
+ {{ + govukInput({ + errorMessage: errors | findErrors([name + '[' + index + ']', name + '[' + index + ']['+ otherIds[0] + ']']), + label: { + html: otherIds[1] + }, + id: name + '[' + index + ']['+ otherIds[0] + ']', + name: name + '[' + index + ']['+ otherIds[0] + ']', + value: value, + attributes: { + 'data-name': name + '[%index%]['+ otherIds[0] +']', + 'data-id': name + '[%index%]['+ otherIds[0] +']' + } + }) + }} +
+
+ {% if showRemove %} + + {% endif %} +
+
+ {% endcall %} +{% endmacro%} + {% macro radiosWithNestedRadios(question) %} {% set radio_button_orientation = 'govuk-radios--inline' if question.orientation === 'inline' else 'govuk-radios'%} diff --git a/server/views/pages/reportDetailMacro.njk b/server/views/pages/reportDetailMacro.njk index 72fd4cb4..013ca67c 100644 --- a/server/views/pages/reportDetailMacro.njk +++ b/server/views/pages/reportDetailMacro.njk @@ -180,6 +180,16 @@ print: print }) }} + + {{ + reportDetailsMacros.tableRow({ + label: 'Was any part of the incident captured on a body-worn camera?', + 'data-qa': 'bodyCameras', + dataValue: data.useOfForceDetails.bodyCameras | capitalize, + print: print + }) + }} + {{ reportDetailsMacros.tableRow({ label: 'Were any personal protection techniques used?', @@ -357,13 +367,4 @@ print: print }) }} - {{ - reportDetailsMacros.tableRow({ - label: 'Was any part of the incident captured on a body-worn camera?', - 'data-qa': 'bodyCameras', - dataValue: data.evidence.bodyCameras | capitalize, - print: print - }) - }} - -{% endmacro %} \ No newline at end of file +{% endmacro %}