Skip to content

Commit

Permalink
MAP-625 add new weapons observed question
Browse files Browse the repository at this point in the history
  • Loading branch information
GurnankCheema committed Jan 29, 2024
1 parent 8dbae43 commit 8d7f2cd
Show file tree
Hide file tree
Showing 12 changed files with 255 additions and 8 deletions.
1 change: 1 addition & 0 deletions assets/js/add-another-detail.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
new AddAnother($('.add-another-body-worn-camera'), '.remove-button-container')
new AddAnother($('.add-another-weapons-observed'), '.remove-button-container')
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
const { use } = require('passport')
const { offender } = require('../../mockApis/data')

const ReportUseOfForcePage = require('../../pages/createReport/reportUseOfForcePage')
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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'],
Expand All @@ -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,
Expand Down Expand Up @@ -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')
Expand All @@ -141,5 +170,31 @@ 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.weaponsObserved().check('YES')
useOfForceDetailsPage.weaponTypes(0).type('gun')
useOfForceDetailsPage.addAnotherWeapon()
useOfForceDetailsPage.weaponTypes(1).type('gun')

useOfForceDetailsPage.clickSaveAndContinue()

useOfForceDetailsPage.errorSummary().contains("Camera '1' has already been added - remove this camera")
useOfForceDetailsPage.errorSummary().contains("Weapon 'gun' has already been added - remove this weapon")
})
})
2 changes: 2 additions & 0 deletions integration-tests/integration/seedData.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions integration-tests/pages/createReport/useOfForceDetailsPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ const useOfForceDetailsPage = () =>
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: {
Expand Down Expand Up @@ -58,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')
Expand Down
21 changes: 21 additions & 0 deletions server/config/forms/useOfForceDetailsForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,27 @@ 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')
.ruleset.unique('weaponType')
.message("Weapon '{#value.weaponType}' has already been added - remove this weapon")
.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),
Expand Down
118 changes: 116 additions & 2 deletions server/config/forms/useOfForceDetailsValidation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ beforeEach(() => {
batonDrawn: 'true',
batonUsed: 'true',
pavaDrawn: 'true',
weaponsObserved: 'NO',
pavaUsed: 'true',
guidingHold: 'true',
guidingHoldOfficersInvolved: '2',
Expand Down Expand Up @@ -53,7 +54,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)

Expand All @@ -78,6 +79,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',
Expand All @@ -100,7 +102,7 @@ describe('complete schema', () => {
},
])

expect(errors.length).toEqual(10)
expect(errors.length).toEqual(11)

expect(formResponse).toEqual({})
})
Expand Down Expand Up @@ -332,6 +334,118 @@ 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 return validation error if more than one weapon with same identifier', () => {
validInput.weaponsObserved = 'YES'
validInput.weaponTypes = [{ weaponType: 'Gun' }, { weaponType: 'Gun' }]
const { errors } = check(validInput)

expect(errors).toEqual([
{ href: '#weaponTypes[1]', text: "Weapon 'Gun' has already been added - remove this weapon" },
])
})

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,
Expand Down
6 changes: 5 additions & 1 deletion server/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ export const findEnum = <K extends string>(type: LabelledEnum<K>, val: string):
export const BodyWornCameras = toEnum({
YES: { value: 'YES', label: 'Yes' },
NO: { value: 'NO', label: 'No' },
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({
Expand Down
2 changes: 2 additions & 0 deletions server/data/UseOfForceReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export type UseOfForceDetails = {
batonUsed: boolean
pavaDrawn: boolean
pavaUsed: boolean
weaponsObserved: boolean
weaponTypes: { weaponType: string }[]
guidingHold: boolean
guidingHoldOfficersInvolved: number
escortingHold?: boolean
Expand Down
7 changes: 7 additions & 0 deletions server/services/reportSummary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
toLabel,
UofReasons,
findEnum,
WeaponsObserved,
} from '../config/types'
import { Prison } from '../data/prisonClientTypes'
import {
Expand Down Expand Up @@ -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)
),
}
}

Expand Down
Loading

0 comments on commit 8d7f2cd

Please sign in to comment.