Skip to content

Commit eea8ab2

Browse files
authored
VB-4966 Add choose date or session block page (#1379)
1 parent 874b591 commit eea8ab2

5 files changed

Lines changed: 235 additions & 13 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { RequestHandler } from 'express'
2+
import { AuditService, BlockDatesOrSessionsService, VisitService } from '../../../services'
3+
4+
export default class BlockSessionController {
5+
public constructor(
6+
private readonly auditService: AuditService,
7+
private readonly blockDatesOrSessionsService: BlockDatesOrSessionsService,
8+
private readonly visitService: VisitService,
9+
) {}
10+
11+
public view(): RequestHandler {
12+
return async (req, res) => {
13+
// TODO implement block session view functionality - temporary redirect to avoid 404s
14+
res.redirect('/block-visit-dates/block-date-or-session')
15+
}
16+
}
17+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import type { Express } from 'express'
2+
import request from 'supertest'
3+
import * as cheerio from 'cheerio'
4+
import { SessionData } from 'express-session'
5+
import { FieldValidationError } from 'express-validator'
6+
import { appWithAllRoutes, FlashData, flashProvider } from '../testutils/appSetup'
7+
8+
let app: Express
9+
let flashData: FlashData
10+
let sessionData: SessionData
11+
12+
const url = '/block-visit-dates/block-date-or-session'
13+
const date = '2024-09-06'
14+
15+
beforeEach(() => {
16+
sessionData = { blockDateOrSession: { date, backLinkHref: '#back-link-from-session' } } as SessionData
17+
18+
app = appWithAllRoutes({ sessionData })
19+
20+
flashData = {}
21+
flashProvider.mockImplementation((key: keyof FlashData) => flashData[key])
22+
})
23+
24+
afterEach(() => {
25+
jest.resetAllMocks()
26+
})
27+
28+
describe('Choose date or session block', () => {
29+
describe(`GET ${url}`, () => {
30+
it('should redirect to blocked dates listing page if no new block date in session', () => {
31+
sessionData.blockDateOrSession = undefined
32+
return request(app).get(url).expect(302).expect('location', '/block-visit-dates')
33+
})
34+
35+
it('should display choose date or session block page', () => {
36+
return request(app)
37+
.get(url)
38+
.expect('Content-Type', /html/)
39+
.expect(res => {
40+
const $ = cheerio.load(res.text)
41+
expect($('.govuk-back-link').attr('href')).toBe('/block-visit-dates')
42+
expect($('h1').text()).toBe('What would you like to block on Friday 6 September 2024?')
43+
44+
expect($('input[name=blockType]').length).toBe(2)
45+
expect($('input[name=blockType]').eq(0).val()).toBe('date')
46+
expect($('input[name=blockType]').eq(1).val()).toBe('session')
47+
expect($('input[name=blockType]:checked').length).toBe(0)
48+
49+
expect($('[data-test=submit]').text().trim()).toBe('Continue')
50+
})
51+
})
52+
53+
it('should render validation errors', () => {
54+
const validationError = { path: 'blockType', msg: 'No answer selected' } as FieldValidationError
55+
56+
flashData = { errors: [validationError] }
57+
58+
return request(app)
59+
.get(url)
60+
.expect('Content-Type', /html/)
61+
.expect(res => {
62+
const $ = cheerio.load(res.text)
63+
expect($('.govuk-error-summary a[href="#blockType-error"]').text()).toBe(validationError.msg)
64+
expect($('#blockType-error').text()).toContain(validationError.msg)
65+
})
66+
})
67+
})
68+
69+
describe(`POST ${url}`, () => {
70+
it('should redirect to blocked dates listing page if no new block date in session', () => {
71+
sessionData.blockDateOrSession = undefined
72+
return request(app).post(url).expect(302).expect('location', '/block-visit-dates')
73+
})
74+
75+
it('should redirect to block new date page if this is selected', () => {
76+
return request(app)
77+
.post(url)
78+
.send({ blockType: 'date' })
79+
.expect(302)
80+
.expect('location', '/block-visit-dates/block-new-date')
81+
.expect(() => {
82+
expect(flashProvider).toHaveBeenCalledTimes(0)
83+
expect(sessionData.blockDateOrSession.backLinkHref).toBe('/block-visit-dates/block-date-or-session')
84+
})
85+
})
86+
87+
it('should redirect to block new session page if this is selected', () => {
88+
return request(app)
89+
.post(url)
90+
.send({ blockType: 'session' })
91+
.expect(302)
92+
.expect('location', '/block-visit-dates/block-new-session')
93+
.expect(() => {
94+
expect(flashProvider).toHaveBeenCalledTimes(0)
95+
expect(sessionData.blockDateOrSession.backLinkHref).toBe('/block-visit-dates/block-date-or-session')
96+
})
97+
})
98+
99+
it('should set form validation errors and redirect to same page', () => {
100+
const expectedValidationError: FieldValidationError = {
101+
location: 'body',
102+
msg: 'No answer selected',
103+
path: 'blockType',
104+
type: 'field',
105+
value: 'invalid',
106+
}
107+
108+
return request(app)
109+
.post(url)
110+
.send({ blockType: 'invalid' })
111+
.expect(302)
112+
.expect('location', '/block-visit-dates/block-date-or-session')
113+
.expect(() => {
114+
expect(flashProvider).toHaveBeenCalledWith('errors', [expectedValidationError])
115+
expect(flashProvider).toHaveBeenCalledTimes(1)
116+
})
117+
})
118+
})
119+
})
Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,50 @@
11
import { RequestHandler } from 'express'
2-
import { AuditService, BlockDatesOrSessionsService } from '../../services'
3-
import logger from '../../../logger'
2+
import { body, matchedData, ValidationChain, validationResult } from 'express-validator'
43

54
export default class ChooseDateOrSessionBlockController {
6-
public constructor(
7-
private readonly auditService: AuditService,
8-
private readonly blockDatesOrSessionsService: BlockDatesOrSessionsService,
9-
) {}
5+
public constructor() {}
106

117
public view(): RequestHandler {
128
return async (req, res) => {
13-
// FIXME temporary redirect until session block pages built
14-
logger.info('Block session page: redirecting to block new date page')
15-
return res.redirect('/block-visit-dates/block-new-date')
9+
const { blockDateOrSession } = req.session
10+
if (!blockDateOrSession) {
11+
return res.redirect('/block-visit-dates')
12+
}
13+
14+
blockDateOrSession.backLinkHref = '/block-visit-dates'
15+
const { backLinkHref, date } = blockDateOrSession
16+
17+
return res.render('pages/blockDatesOrSessions/chooseDateOrSessionBlock', {
18+
backLinkHref,
19+
errors: req.flash('errors'),
20+
date,
21+
})
1622
}
1723
}
24+
25+
public submit(): RequestHandler {
26+
return async (req, res) => {
27+
const { blockDateOrSession } = req.session
28+
if (!blockDateOrSession) {
29+
return res.redirect('/block-visit-dates')
30+
}
31+
32+
const errors = validationResult(req)
33+
if (!errors.isEmpty()) {
34+
req.flash('errors', errors.array())
35+
return res.redirect('/block-visit-dates/block-date-or-session')
36+
}
37+
38+
const { blockType } = matchedData<{ blockType: 'date' | 'session' }>(req)
39+
blockDateOrSession.backLinkHref = '/block-visit-dates/block-date-or-session'
40+
41+
return res.redirect(
42+
blockType === 'date' ? '/block-visit-dates/block-new-date' : '/block-visit-dates/block-new-session',
43+
)
44+
}
45+
}
46+
47+
public validate(): ValidationChain[] {
48+
return [body('blockType', 'No answer selected').isIn(['date', 'session'])]
49+
}
1850
}

server/routes/blockDatesOrSessions/index.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import BlockDateController from './blockDates/blockDateController'
55
import UnblockDateController from './blockDates/unblockDateController'
66
import ChooseDateOrSessionBlockController from './chooseDateOrSessionBlockController'
77
import config from '../../config'
8+
import BlockSessionController from './blockSessions/blockSessionController'
89

910
export default function routes(services: Services): Router {
1011
const router = Router()
@@ -14,10 +15,7 @@ export default function routes(services: Services): Router {
1415
services.visitSessionsService,
1516
)
1617

17-
const chooseDateOrSessionBlockController = new ChooseDateOrSessionBlockController(
18-
services.auditService,
19-
services.blockDatesOrSessionsService,
20-
)
18+
const chooseDateOrSessionBlockController = new ChooseDateOrSessionBlockController()
2119

2220
const blockDateController = new BlockDateController(
2321
services.auditService,
@@ -27,6 +25,12 @@ export default function routes(services: Services): Router {
2725

2826
const unblockDateController = new UnblockDateController(services.auditService, services.blockDatesOrSessionsService)
2927

28+
const blockSessionController = new BlockSessionController(
29+
services.auditService,
30+
services.blockDatesOrSessionsService,
31+
services.visitService,
32+
)
33+
3034
// Block visit dates or sessions main page with listing
3135
router.get(
3236
'/',
@@ -38,6 +42,11 @@ export default function routes(services: Services): Router {
3842

3943
// Choose whether to block a date or a session
4044
router.get('/block-date-or-session', chooseDateOrSessionBlockController.view())
45+
router.post(
46+
'/block-date-or-session',
47+
chooseDateOrSessionBlockController.validate(),
48+
chooseDateOrSessionBlockController.submit(),
49+
)
4150

4251
// Date block pages
4352
router.get('/block-new-date', blockDateController.view())
@@ -46,6 +55,9 @@ export default function routes(services: Services): Router {
4655
// Unblock date
4756
router.post('/unblock-date', unblockDateController.validate(), unblockDateController.submit())
4857

58+
// Session block pages
59+
router.get('/block-new-session', blockSessionController.view())
60+
4961
// Unblock session
5062
router.post('/unblock-session', (req, res) => res.redirect('/block-visit-dates')) // TODO implement unblock session functionality
5163

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{% extends "layout.njk" %}
2+
{% from "govuk/components/radios/macro.njk" import govukRadios %}
3+
{% from "govuk/components/button/macro.njk" import govukButton %}
4+
5+
{% set pageHeaderTitle = "What would you like to block on " +
6+
date | formatDate('EEEE d MMMM yyyy') + "?" %}
7+
8+
{% block content %}
9+
<div class="govuk-grid-row">
10+
<div class="govuk-grid-column-two-thirds">
11+
{% include "partials/errorSummary.njk" %}
12+
13+
<h1 class="govuk-heading-l">{{ pageHeaderTitle }}</h1>
14+
15+
<form action="/block-visit-dates/block-date-or-session" method="POST" novalidate>
16+
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
17+
18+
{{ govukRadios({
19+
name: "blockType",
20+
items: [
21+
{
22+
value: "date",
23+
text: "The full day"
24+
},
25+
{
26+
value: "session",
27+
text: "A single session"
28+
}
29+
],
30+
errorMessage: errors | findError('blockType'),
31+
attributes: { "data-test": "block-type" }
32+
}) }}
33+
34+
{{ govukButton({
35+
text: "Continue",
36+
attributes: { "data-test": "submit" },
37+
preventDoubleClick: true
38+
}) }}
39+
</form>
40+
</div>
41+
</div>
42+
{% endblock %}

0 commit comments

Comments
 (0)