Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
pmcphee77 committed Feb 3, 2025
1 parent 4014551 commit bd674ee
Show file tree
Hide file tree
Showing 22 changed files with 1,068 additions and 52 deletions.
17 changes: 17 additions & 0 deletions assets/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,22 @@ const resetConditionals = () => {
})
}
}

const setNoFixedAddressConditional = () => {
const fixedAddressSection = document.querySelector('div[fixed-address-section]')
const fixedAddressCheckbox = document.querySelector('input[name="noFixedAddress"]')
const showOrHide = () => {
if (fixedAddressCheckbox.checked) {
fixedAddressSection.classList.add('govuk-visually-hidden')
} else {
fixedAddressSection.classList.remove('govuk-visually-hidden')
}
}
if (fixedAddressSection && fixedAddressCheckbox) {
showOrHide()
fixedAddressCheckbox.addEventListener('click', showOrHide)
}
}
setNoFixedAddressConditional()
lastAppointment()
resetConditionals()
2 changes: 1 addition & 1 deletion e2e_tests/steps/mas/personal-details/personalDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const loginMasAndGoToPersonalDetails = async (page: Page, crn: string) =>
await loginToManageMySupervision(page)
await searchForCrn(page, crn)
await page.getByRole('link', { name: 'Personal details' }).first().click()
await expect(page.locator('h1.govuk-heading-l')).toContainText('Personal Details')
await expect(page.locator('h1.govuk-heading-l')).toContainText('Personal details')
}

export const searchForCrn = async (page: Page, crn: string) => {
Expand Down
2 changes: 1 addition & 1 deletion integration_tests/e2e/personalDetails.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ context('Personal Details', () => {
page.assertRiskTags()
page.headerCrn().should('contain.text', 'X000001')
page.headerName().should('contain.text', 'Eula Schmeler')
page.pageHeading().should('contain.text', 'Personal Details')
page.pageHeading().should('contain.text', 'Personal details')
page.getTab('overview').should('contain.text', 'Overview')
page.getTab('personalDetails').should('contain.text', 'Personal details')
page.getTab('risk').should('contain.text', 'Risk')
Expand Down
89 changes: 89 additions & 0 deletions integration_tests/e2e/personalDetails/editContactDetails.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import Page from '../../pages/page'
import EditContactDetails from '../../pages/personalDetails/editContactDetails'

context('Edit contact details', () => {
it('Edit contact details page is rendered based on non fixed address', () => {
cy.visit('/case/X000001/personal-details/edit-contact-details')
const page = Page.verifyOnPage(EditContactDetails)
page.getElement('phoneNumber').should('be.visible')
page.getElement('mobileNumber').should('be.visible')
page.getElement('emailAddress').should('be.visible')
page.getElement('buildingName').should('not.be.visible')
page.getElement('buildingNumber').should('not.be.visible')
page.getElement('streetName').should('not.be.visible')
page.getElement('district').should('not.be.visible')
page.getElement('town').should('not.be.visible')
page.getElement('county').should('not.be.visible')
page.getElement('postcode').should('not.be.visible')
page.getElement('startDate').should('be.visible')
page.getElement('endDate').should('be.visible')
page.getElement('notes').should('be.visible')
page.getCheckboxField('noFixedAddress').click()
page.getElement('buildingName').should('be.visible')
page.getElement('buildingNumber').should('be.visible')
page.getElement('streetName').should('be.visible')
page.getElement('district').should('be.visible')
page.getElement('town').should('be.visible')
page.getElement('county').should('be.visible')
page.getElement('postcode').should('be.visible')
})
it('Submitting successfully should redirect to Personal details screen with update banner', () => {
cy.visit('/case/X000001/personal-details/edit-contact-details')
const page = Page.verifyOnPage(EditContactDetails)
page.getElement('submit-btn').click()
page.getElement('updateBanner').should('contain.text', 'Contact details updated')
})
it('Submitting with an end date should stay on edit page with warning banner, then second submit will redirect to Personal details screen with update banner', () => {
cy.visit('/case/X000001/personal-details/edit-contact-details')
const page = Page.verifyOnPage(EditContactDetails)
page.getElementInput('endDate').type('03/02/2025')
page.getElement('submit-btn').click()
page
.getElement('infoBanner')
.should(
'contain.text',
'An end will change this to a previous address and you will need to add a new main address.',
)
page.getElement('submit-btn').click()
page.getElement('updateBanner').should('contain.text', 'Contact details updated')
})

it('Submitting with invalid data with over 35 chars should show error messages', () => {
cy.visit('/case/X000001/personal-details/edit-contact-details')
const page = Page.verifyOnPage(EditContactDetails)
page.getElementInput('phoneNumber').clear().type('1'.repeat(36))
page.getElementInput('mobileNumber').clear().type('1'.repeat(36))
page.getElementInput('emailAddress').clear().type('1'.repeat(36))
page.getCheckboxField('noFixedAddress').click()
page.getElementInput('buildingName').clear().type('1'.repeat(36))
page.getElementInput('buildingNumber').clear().type('1'.repeat(36))
page.getElementInput('streetName').clear().type('1'.repeat(36))
page.getElementInput('district').clear().type('1'.repeat(36))
page.getElementInput('town').clear().type('1'.repeat(36))
page.getElementInput('county').clear().type('1'.repeat(36))
page.getElementInput('postcode').clear().type('1'.repeat(36))

page.getElement('submit-btn').click()
page.getElement('phoneNumberError').should('contain.text', 'Phone number must be 35 characters or less.')
page.getElement('mobileNumberError').should('contain.text', 'Mobile number must be 35 characters or less.')
page.getElement('emailAddress').should('contain.text', 'Enter an email address in the correct format.')

page.getElement('buildingName').should('contain.text', 'Building name must be 35 characters or less.')
page.getElement('buildingNumber').should('contain.text', 'Building number must be 35 characters or less.')
page.getElement('streetName').should('contain.text', 'Street name must be 35 characters or less.')
page.getElement('district').should('contain.text', 'District must be 35 characters or less.')
page.getElement('town').should('contain.text', 'Town or city must be 35 characters or less.')
page.getElement('county').should('contain.text', 'County must be 35 characters or less.')
page.getElement('postcode').should('contain.text', 'Enter a full UK postcode.')
})

it('Submitting with a dates later than today should show error messages', () => {
cy.visit('/case/X000001/personal-details/edit-contact-details')
const page = Page.verifyOnPage(EditContactDetails)
page.getElementInput('endDate').type('03/02/2099')
page.getElementInput('startDate').clear().type('03/02/2099')
page.getElement('submit-btn').click()
page.getElement('endDateError').should('contain.text', 'End date can not be later than today.')
page.getElement('startDateError').should('contain.text', 'Start date can not be later than today.')
})
})
2 changes: 1 addition & 1 deletion integration_tests/pages/personalDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ import Page from './page'

export default class PersonalDetailsPage extends Page {
constructor() {
super('Personal Details')
super('Personal details')
}
}
27 changes: 27 additions & 0 deletions integration_tests/pages/personalDetails/editContactDetails.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Page, { PageElement } from '../page'

export default class EditContactDetails extends Page {
constructor() {
super('Edit contact details')
}

getNoFixedAddress = (addressId: string, rowName: string, type: string): PageElement => {
return cy.get(`#noFixedAddress`)
}

getElement = (name: string): PageElement => {
return cy.get(`[data-qa="${name}"]`)
}

getElementInput = (name: string): PageElement => {
return cy.get(`[data-qa="${name}"] input`)
}

getDateElementInput = (name: string): PageElement => {
return cy.get(`[data-qa="${name}"] govuk-input moj-js-datepicker-input`)
}

getCheckboxField = (name: string): PageElement => {
return cy.get(`input[type="checkbox"]`)
}
}
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"test": "jest",
"test:ci": "jest --runInBand",
"security_audit": "npx audit-ci --config audit-ci.json",
"int-test": "cypress run --config video=false",
"int-test": "cypress run --spec integration_tests/e2e/personalDetails/editContactDetails.cy.ts --config video=false",
"int-test-ui": "cypress open --e2e --browser chrome",
"clean": "rm -rf dist build node_modules stylesheets",
"sentry:sourcemaps": "sentry-cli sourcemaps inject --org ministryofjustice --project hmpps-manage-people-on-probation-ui ./dist && sentry-cli sourcemaps upload --org ministryofjustice --project hmpps-manage-people-on-probation-ui ./dist",
Expand Down Expand Up @@ -125,6 +125,7 @@
"nunjucks": "^3.2.4",
"passport": "^0.7.0",
"passport-oauth2": "^1.8.0",
"postcode-validator": "^3.10.2",
"prom-client": "^15.1.3",
"redis": "^4.7.0",
"slugify": "^1.6.6",
Expand Down
1 change: 1 addition & 0 deletions server/@types/Route.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Location } from '../data/model/caseload'
import { SentryConfig } from '../config'

interface Locals {
errorMessages: Record<string, string>
filters?: ActivityLogFiltersResponse
user: { token: string; authSource: string; username?: string }
compactView?: boolean
Expand Down
10 changes: 10 additions & 0 deletions server/data/masApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
DisabilityOverview,
PersonalContact,
PersonalDetails,
PersonalDetailsUpdateRequest,
ProvisionOverview,
} from './model/personalDetails'
import { AddressOverview, PersonSummary } from './model/common'
Expand Down Expand Up @@ -84,6 +85,15 @@ export default class MasApiClient extends RestClient {
return this.get({ path: `/personal-details/${crn}`, handle404: false })
}

async updatePersonalDetails(crn: string, body: PersonalDetailsUpdateRequest): Promise<PersonalDetails | null> {
return this.post({
data: body,
path: `/personal-details/${crn}`,
handle404: true,
handle500: true,
})
}

async getPersonalContact(crn: string, id: string): Promise<PersonalContact | null> {
return this.get({ path: `/personal-details/${crn}/personal-contact/${id}`, handle404: false })
}
Expand Down
32 changes: 32 additions & 0 deletions server/data/model/personalDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,27 @@ export interface PersonalDetails {
religionOrBelief?: string
sexualOrientation?: string
documents: Document[]
addressTypes: AddressType[]
}

export interface PersonalDetailsUpdateRequest {
[index: string]: string | boolean
phoneNumber?: string
mobileNumber?: string
emailAddress?: string
buildingName?: string
buildingNumber?: string
streetName?: string
district?: string
town?: string
county?: string
postcode?: string
addressType?: string
startDate?: string
endDate?: string
notes?: string
verified?: boolean
noFixedAddress?: boolean
}

export interface PersonalContact {
Expand All @@ -49,6 +70,11 @@ export interface Address {
lastUpdatedBy?: Name
}

export interface AddressType {
code?: string
description?: string
}

export interface PersonAddress {
buildingName?: string
buildingNumber?: string
Expand All @@ -63,7 +89,13 @@ export interface PersonAddress {
from: string
to: string
type?: string
typeCode?: string
status?: string
verified?: boolean
noFixedAddress?: boolean
notes?: string
startDate?: string
endDate?: string
}

export interface Circumstances {
Expand Down
74 changes: 74 additions & 0 deletions server/middleware/validation/validationUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { postcodeValidator } from 'postcode-validator'
import { DateTime } from 'luxon'
import { PersonalDetailsUpdateRequest } from '../../data/model/personalDetails'

export interface ErrorCheck {
fn: (...args: any[]) => boolean
msg: string
length?: number
crossField?: string
}

export interface ErrorChecks {
optional: boolean
checks: ErrorCheck[]
}

export interface ValidationSpec {
[index: string]: ErrorChecks
}

export const isEmail = (string: string) => /[a-z0-9]+@[a-z]+\.[a-z]{2,3}/.test(string)
export const isNotEmpty = (args: any[]) => {
return !!args[0] && args[0] !== undefined
}
export const isNumeric = (args: any[]) => /^[+-]?\d+(\.\d+)?$/.test(args[0])
export const isUkEmail = (args: any[]) => {
return postcodeValidator(args[0], 'GB')
}
export const charsOrLess = (args: any[]) => {
return !(args[1].length > args[0])
}
export const isValidDate = (args: any[]) => {
return DateTime.fromFormat(args[0], 'd/M/yyyy').isValid
}

export const isNotLaterThanToday = (args: any[]) => {
const date = DateTime.fromFormat(args[0], 'd/M/yyyy')
return date.isValid && date <= DateTime.now()
}

export const isNotLaterThan = (args: any[]) => {
const notLaterThanDate = DateTime.fromFormat(args[0], 'd/M/yyyy')
const date = DateTime.fromFormat(args[1], 'd/M/yyyy')
if (!notLaterThanDate.isValid || !date.isValid) {
return true
}
return notLaterThanDate <= date
}

export function validateWithSpec(request: PersonalDetailsUpdateRequest, validationSpec: ValidationSpec) {
const errors: Record<string, string> = {}
Object.entries(validationSpec).forEach(entry => {
const fieldName = entry[0]
const checks = entry[1]
if (Object.prototype.hasOwnProperty.call(request, fieldName)) {
if (!request[fieldName] && checks.optional === true) {
return
}
for (const c of checks.checks) {
let args: any[] = c?.length ? [c.length, request[fieldName]] : [request[fieldName]]
if (c?.crossField) {
args = [request[c?.crossField], request[fieldName]]
}
if (!c.fn(args)) {
errors[fieldName] = c.msg
break
}
}
} else if (checks.optional === false) {
errors[fieldName] = checks.checks[0].msg
}
})
return errors
}
1 change: 1 addition & 0 deletions server/properties/errorMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const ruleKeys = [
'isAfterTo',
'isBeforeFrom',
] as const
const personalContactKeys = ['phoneNumber', 'mobileNumber'] as const
const appointmentsKeys = [
'type',
'sentence',
Expand Down
Loading

0 comments on commit bd674ee

Please sign in to comment.