@defra/lis-infra-ui-services 0.23.0 → 0.25.0-lreg-666.alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -4
- package/package.json +8 -2
- package/src/components/error-pages.js +3 -0
- package/src/components/error-pages.test.js +18 -0
- package/src/components/layout.js +3 -0
- package/src/components/layout.test.js +18 -0
- package/src/components/scrollable-table.js +68 -0
- package/src/components/scrollable-table.test.js +178 -0
- package/src/components/sortable-paginated-table.scss +22 -1
- package/src/components/sub-navigation.scss +1 -1
- package/src/errors/index.js +1 -6
- package/src/errors/index.test.js +3 -38
- package/src/nunjucks/components/search-form/search-form.test.js +33 -1
- package/src/nunjucks/components/search-form/template.njk +12 -12
- package/src/nunjucks/components/sortable-paginated-table/sortable-paginated-table.test.js +15 -1
- package/src/nunjucks/components/sortable-paginated-table/template.njk +1 -1
- package/src/nunjucks/context.js +1 -5
- package/src/nunjucks/context.test.js +7 -15
- package/src/nunjucks/plugin.test.js +1 -3
- package/src/nunjucks/routes/error/error-pages.test.js +6 -42
- package/src/nunjucks/routes/error/not-authorised.njk +1 -4
- package/src/nunjucks/routes/error/page-not-found.njk +3 -11
- package/src/nunjucks/routes/error/problem-with-service.njk +0 -3
- package/src/nunjucks/routes/error/service-unavailable.njk +0 -3
- package/src/nunjucks/templates/layouts/base.njk +7 -2
- package/src/nunjucks/templates/layouts/base.test.js +76 -0
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Install dependencies for this package locally with `npm install` from this direc
|
|
|
15
15
|
| 503 | `service-unavailable` |
|
|
16
16
|
| anything else | `problem-with-service` |
|
|
17
17
|
|
|
18
|
-
The
|
|
18
|
+
The contact link on these pages comes from the `errorPages` option of `createNunjucksConfig` (or `createNunjucksContextBuilder`), exposed to templates as `errorPages`. The key is optional:
|
|
19
19
|
|
|
20
20
|
```js
|
|
21
21
|
createNunjucksConfig({
|
|
@@ -24,13 +24,41 @@ createNunjucksConfig({
|
|
|
24
24
|
getRequestBasePath,
|
|
25
25
|
errorPages: {
|
|
26
26
|
contactHref:
|
|
27
|
-
'https://www.gov.uk/government/organisations/british-cattle-movement-service'
|
|
28
|
-
homeHref: '/', // default, "Home page" on page not found
|
|
29
|
-
getStartedHref: '/' // default, "Get started" on page not found
|
|
27
|
+
'https://www.gov.uk/government/organisations/british-cattle-movement-service' // default
|
|
30
28
|
}
|
|
31
29
|
})
|
|
32
30
|
```
|
|
33
31
|
|
|
32
|
+
## Layout scripts
|
|
33
|
+
|
|
34
|
+
`layoutComponents` from `@defra/lis-infra-ui-services/components/layout` is the list of GOV.UK Frontend components the layout template renders: Service navigation and Skip link.
|
|
35
|
+
|
|
36
|
+
`errorPageComponents` from `@defra/lis-infra-ui-services/components/error-pages` is the list the shared error pages render (Button, for the not-authorised sign-in button). Every app that serves the shared error pages must add it.
|
|
37
|
+
|
|
38
|
+
Start each with `createAll`, alongside any page-level components (see the full example under [Scrollable table](#scrollable-table)).
|
|
39
|
+
|
|
40
|
+
Wrap the combined list in a `Set`, so a component that appears in more than one list (or that the app also lists itself) is only initialised once: govuk's base class logs an InitError on double init.
|
|
41
|
+
|
|
42
|
+
### Scrollable table
|
|
43
|
+
|
|
44
|
+
Apps that render the sortable paginated table macro also start `ScrollableTable` from `@defra/lis-infra-ui-services/components/scrollable-table`. It drops the tab stop (and the region role and label) from a table wrapper that fits, and restores them when the table overflows, re-checking whenever the wrapper or table changes size. Without JavaScript the wrapper stays focusable.
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
import { createAll, Tabs } from 'govuk-frontend'
|
|
48
|
+
import { errorPageComponents } from '@defra/lis-infra-ui-services/components/error-pages'
|
|
49
|
+
import { layoutComponents } from '@defra/lis-infra-ui-services/components/layout'
|
|
50
|
+
import { ScrollableTable } from '@defra/lis-infra-ui-services/components/scrollable-table'
|
|
51
|
+
|
|
52
|
+
for (const component of new Set([
|
|
53
|
+
...layoutComponents,
|
|
54
|
+
...errorPageComponents,
|
|
55
|
+
ScrollableTable,
|
|
56
|
+
Tabs
|
|
57
|
+
])) {
|
|
58
|
+
createAll(component)
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
34
62
|
## Scheduled maintenance
|
|
35
63
|
|
|
36
64
|
Each hub switches maintenance on with its own config value, `maintenance.availableFrom`: the UK local date-time the service is back, in ISO 8601 form without an offset, e.g. `2026-11-19T09:00` or `2026-11-19T09:00:00`. Values with a `Z` or `+hh:mm` offset, and dates without a time, throw at start-up. An empty value leaves the service running as normal.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defra/lis-infra-ui-services",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0-lreg-666.alpha.1",
|
|
4
4
|
"description": "UI-Services shared definitions for the livestock taxonomy topology.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"repository": {
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
".": "./src/index.js",
|
|
12
12
|
"./base-path": "./src/base-path.js",
|
|
13
13
|
"./components/autocomplete": "./src/components/autocomplete.js",
|
|
14
|
+
"./components/error-pages": "./src/components/error-pages.js",
|
|
15
|
+
"./components/layout": "./src/components/layout.js",
|
|
16
|
+
"./components/scrollable-table": "./src/components/scrollable-table.js",
|
|
14
17
|
"./components/autocomplete.scss": "./src/components/autocomplete.scss",
|
|
15
18
|
"./components/search-form.scss": "./src/components/search-form.scss",
|
|
16
19
|
"./components/service-navigation.scss": "./src/components/service-navigation.scss",
|
|
@@ -71,7 +74,8 @@
|
|
|
71
74
|
"peerDependencies": {
|
|
72
75
|
"@defra/lis-hubs-infra-access": ">=0.29.0 <1.0.0",
|
|
73
76
|
"@defra/lis-hubs-infra-core": ">=0.4.0 <1.0.0",
|
|
74
|
-
"eslint": "^9.0.0"
|
|
77
|
+
"eslint": "^9.0.0",
|
|
78
|
+
"govuk-frontend": ">=6.5.0 <7.0.0"
|
|
75
79
|
},
|
|
76
80
|
"devDependencies": {
|
|
77
81
|
"@defra/lis-hubs-infra-access": "^0.29.0",
|
|
@@ -81,7 +85,9 @@
|
|
|
81
85
|
"@vitest/coverage-istanbul": "^4.1.10",
|
|
82
86
|
"audit-ci": "^7.1.0",
|
|
83
87
|
"eslint": "^9.0.0",
|
|
88
|
+
"govuk-frontend": "6.5.0",
|
|
84
89
|
"husky": "^9.1.7",
|
|
90
|
+
"jsdom": "^29.1.1",
|
|
85
91
|
"prettier": "^3.8.4",
|
|
86
92
|
"vitest": "^4.1.10"
|
|
87
93
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { Button } from 'govuk-frontend'
|
|
3
|
+
import { describe, expect, test } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { errorPageComponents } from './error-pages.js'
|
|
6
|
+
|
|
7
|
+
describe('errorPageComponents', () => {
|
|
8
|
+
test('contains the error page components in order', () => {
|
|
9
|
+
// Arrange
|
|
10
|
+
const expected = [Button]
|
|
11
|
+
|
|
12
|
+
// Act
|
|
13
|
+
const actual = errorPageComponents
|
|
14
|
+
|
|
15
|
+
// Assert
|
|
16
|
+
expect(actual).toEqual(expected)
|
|
17
|
+
})
|
|
18
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { ServiceNavigation, SkipLink } from 'govuk-frontend'
|
|
3
|
+
import { describe, expect, test } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { layoutComponents } from './layout.js'
|
|
6
|
+
|
|
7
|
+
describe('layoutComponents', () => {
|
|
8
|
+
test('contains the layout components in order', () => {
|
|
9
|
+
// Arrange
|
|
10
|
+
const expected = [ServiceNavigation, SkipLink]
|
|
11
|
+
|
|
12
|
+
// Act
|
|
13
|
+
const actual = layoutComponents
|
|
14
|
+
|
|
15
|
+
// Assert
|
|
16
|
+
expect(actual).toEqual(expected)
|
|
17
|
+
})
|
|
18
|
+
})
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Component } from 'govuk-frontend'
|
|
2
|
+
|
|
3
|
+
const ARIA_LABEL_ATTRIBUTE = 'aria-label'
|
|
4
|
+
const ORIGINAL_LABEL_ATTRIBUTE = 'data-original-aria-label'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Drops the tab stop from a scrollable region while its content fits.
|
|
8
|
+
*/
|
|
9
|
+
export class ScrollableTable extends Component {
|
|
10
|
+
static moduleName = 'lis-scrollable-table'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {object} $root - the scrollable wrapper element
|
|
14
|
+
*/
|
|
15
|
+
constructor($root) {
|
|
16
|
+
super($root)
|
|
17
|
+
|
|
18
|
+
this.update()
|
|
19
|
+
|
|
20
|
+
// Watches the table as well as the wrapper: the table can widen after
|
|
21
|
+
// start-up without the window resizing, for example when the font loads.
|
|
22
|
+
const resizeObserver = new globalThis.ResizeObserver(() => this.update())
|
|
23
|
+
resizeObserver.observe(this.$root)
|
|
24
|
+
for (const $child of this.$root.children) {
|
|
25
|
+
resizeObserver.observe($child)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Applies the focusable region attributes only while the content overflows.
|
|
31
|
+
*/
|
|
32
|
+
update() {
|
|
33
|
+
if (this.$root.scrollWidth > this.$root.clientWidth) {
|
|
34
|
+
this.makeFocusableRegion()
|
|
35
|
+
} else {
|
|
36
|
+
this.removeFocusableRegion()
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Lets keyboard users reach the region to scroll it.
|
|
42
|
+
*/
|
|
43
|
+
makeFocusableRegion() {
|
|
44
|
+
const label =
|
|
45
|
+
this.$root.getAttribute(ORIGINAL_LABEL_ATTRIBUTE) ??
|
|
46
|
+
this.$root.getAttribute(ARIA_LABEL_ATTRIBUTE)
|
|
47
|
+
|
|
48
|
+
if (label !== null) {
|
|
49
|
+
this.$root.setAttribute(ARIA_LABEL_ATTRIBUTE, label)
|
|
50
|
+
}
|
|
51
|
+
this.$root.setAttribute('tabindex', '0')
|
|
52
|
+
this.$root.setAttribute('role', 'region')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Avoids an empty tab stop on a table that fits, keeping the label to restore later.
|
|
57
|
+
*/
|
|
58
|
+
removeFocusableRegion() {
|
|
59
|
+
const label = this.$root.getAttribute(ARIA_LABEL_ATTRIBUTE)
|
|
60
|
+
|
|
61
|
+
if (label !== null) {
|
|
62
|
+
this.$root.setAttribute(ORIGINAL_LABEL_ATTRIBUTE, label)
|
|
63
|
+
}
|
|
64
|
+
this.$root.removeAttribute('tabindex')
|
|
65
|
+
this.$root.removeAttribute('role')
|
|
66
|
+
this.$root.removeAttribute(ARIA_LABEL_ATTRIBUTE)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { createAll } from 'govuk-frontend'
|
|
3
|
+
import { afterEach, beforeAll, describe, expect, test, vi } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { ScrollableTable } from './scrollable-table.js'
|
|
6
|
+
|
|
7
|
+
function setWidths(element, { scrollWidth, clientWidth }) {
|
|
8
|
+
Object.defineProperty(element, 'scrollWidth', {
|
|
9
|
+
configurable: true,
|
|
10
|
+
value: scrollWidth
|
|
11
|
+
})
|
|
12
|
+
Object.defineProperty(element, 'clientWidth', {
|
|
13
|
+
configurable: true,
|
|
14
|
+
value: clientWidth
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const resizeObservers = []
|
|
19
|
+
|
|
20
|
+
class FakeResizeObserver {
|
|
21
|
+
constructor(callback) {
|
|
22
|
+
this.callback = callback
|
|
23
|
+
this.observed = []
|
|
24
|
+
resizeObservers.push(this)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
observe(element) {
|
|
28
|
+
this.observed.push(element)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function triggerResize() {
|
|
33
|
+
for (const resizeObserver of resizeObservers) {
|
|
34
|
+
resizeObserver.callback([])
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function renderWrapper(widths) {
|
|
39
|
+
document.body.innerHTML =
|
|
40
|
+
'<div data-module="lis-scrollable-table" tabindex="0" role="region" aria-label="Animals"><table></table></div>'
|
|
41
|
+
const wrapper = document.querySelector('[data-module]')
|
|
42
|
+
setWidths(wrapper, widths)
|
|
43
|
+
return wrapper
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('ScrollableTable', () => {
|
|
47
|
+
beforeAll(() => {
|
|
48
|
+
document.body.classList.add('govuk-frontend-supported')
|
|
49
|
+
vi.stubGlobal('ResizeObserver', FakeResizeObserver)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
vi.restoreAllMocks()
|
|
54
|
+
resizeObservers.length = 0
|
|
55
|
+
document.body.innerHTML = ''
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('removes the focusable region attributes when the table does not overflow', () => {
|
|
59
|
+
// Arrange
|
|
60
|
+
const wrapper = renderWrapper({ scrollWidth: 300, clientWidth: 300 })
|
|
61
|
+
|
|
62
|
+
// Act
|
|
63
|
+
createAll(ScrollableTable)
|
|
64
|
+
|
|
65
|
+
// Assert
|
|
66
|
+
expect(wrapper.hasAttribute('tabindex')).toBe(false)
|
|
67
|
+
expect(wrapper.hasAttribute('role')).toBe(false)
|
|
68
|
+
expect(wrapper.hasAttribute('aria-label')).toBe(false)
|
|
69
|
+
expect(wrapper.getAttribute('data-original-aria-label')).toBe('Animals')
|
|
70
|
+
expect(wrapper.hasAttribute('data-lis-scrollable-table-init')).toBe(true)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('keeps the focusable region attributes when the table overflows', () => {
|
|
74
|
+
// Arrange
|
|
75
|
+
const wrapper = renderWrapper({ scrollWidth: 600, clientWidth: 300 })
|
|
76
|
+
|
|
77
|
+
// Act
|
|
78
|
+
createAll(ScrollableTable)
|
|
79
|
+
|
|
80
|
+
// Assert
|
|
81
|
+
expect(wrapper.getAttribute('tabindex')).toBe('0')
|
|
82
|
+
expect(wrapper.getAttribute('role')).toBe('region')
|
|
83
|
+
expect(wrapper.getAttribute('aria-label')).toBe('Animals')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('restores the attributes, including the label, when the table starts to overflow when resized', () => {
|
|
87
|
+
// Arrange
|
|
88
|
+
const wrapper = renderWrapper({ scrollWidth: 300, clientWidth: 300 })
|
|
89
|
+
createAll(ScrollableTable)
|
|
90
|
+
|
|
91
|
+
// Act
|
|
92
|
+
setWidths(wrapper, { scrollWidth: 600, clientWidth: 300 })
|
|
93
|
+
triggerResize()
|
|
94
|
+
|
|
95
|
+
// Assert
|
|
96
|
+
expect(wrapper.getAttribute('tabindex')).toBe('0')
|
|
97
|
+
expect(wrapper.getAttribute('role')).toBe('region')
|
|
98
|
+
expect(wrapper.getAttribute('aria-label')).toBe('Animals')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('removes the attributes again when the table stops overflowing when resized', () => {
|
|
102
|
+
// Arrange
|
|
103
|
+
const wrapper = renderWrapper({ scrollWidth: 600, clientWidth: 300 })
|
|
104
|
+
createAll(ScrollableTable)
|
|
105
|
+
|
|
106
|
+
// Act
|
|
107
|
+
setWidths(wrapper, { scrollWidth: 300, clientWidth: 300 })
|
|
108
|
+
triggerResize()
|
|
109
|
+
|
|
110
|
+
// Assert
|
|
111
|
+
expect(wrapper.hasAttribute('tabindex')).toBe(false)
|
|
112
|
+
expect(wrapper.hasAttribute('aria-label')).toBe(false)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('leaves a wrapper without an aria-label unlabelled when restoring', () => {
|
|
116
|
+
// Arrange
|
|
117
|
+
const wrapper = renderWrapper({ scrollWidth: 300, clientWidth: 300 })
|
|
118
|
+
wrapper.removeAttribute('aria-label')
|
|
119
|
+
createAll(ScrollableTable)
|
|
120
|
+
|
|
121
|
+
// Act
|
|
122
|
+
setWidths(wrapper, { scrollWidth: 600, clientWidth: 300 })
|
|
123
|
+
triggerResize()
|
|
124
|
+
|
|
125
|
+
// Assert
|
|
126
|
+
expect(wrapper.getAttribute('role')).toBe('region')
|
|
127
|
+
expect(wrapper.hasAttribute('aria-label')).toBe(false)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('watches both the wrapper and the table for size changes', () => {
|
|
131
|
+
// Arrange
|
|
132
|
+
const wrapper = renderWrapper({ scrollWidth: 300, clientWidth: 300 })
|
|
133
|
+
|
|
134
|
+
// Act
|
|
135
|
+
createAll(ScrollableTable)
|
|
136
|
+
|
|
137
|
+
// Assert
|
|
138
|
+
expect(resizeObservers[0].observed).toEqual([
|
|
139
|
+
wrapper,
|
|
140
|
+
wrapper.querySelector('table')
|
|
141
|
+
])
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test('throws when constructed twice on the same element', () => {
|
|
145
|
+
// Arrange
|
|
146
|
+
const wrapper = renderWrapper({ scrollWidth: 300, clientWidth: 300 })
|
|
147
|
+
const first = new ScrollableTable(wrapper)
|
|
148
|
+
|
|
149
|
+
// Act
|
|
150
|
+
let second, error
|
|
151
|
+
try {
|
|
152
|
+
second = new ScrollableTable(wrapper)
|
|
153
|
+
} catch (e) {
|
|
154
|
+
error = e
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Assert
|
|
158
|
+
expect(first).toBeInstanceOf(ScrollableTable)
|
|
159
|
+
expect(second).toBeUndefined()
|
|
160
|
+
expect(error).toBeDefined()
|
|
161
|
+
expect(error.name).toBe('InitError')
|
|
162
|
+
expect(resizeObservers).toHaveLength(1)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('does not double-apply when createAll runs twice on the same document', () => {
|
|
166
|
+
// Arrange
|
|
167
|
+
renderWrapper({ scrollWidth: 300, clientWidth: 300 })
|
|
168
|
+
const log = vi.spyOn(console, 'log').mockReturnValueOnce(undefined)
|
|
169
|
+
createAll(ScrollableTable)
|
|
170
|
+
|
|
171
|
+
// Act
|
|
172
|
+
createAll(ScrollableTable)
|
|
173
|
+
|
|
174
|
+
// Assert
|
|
175
|
+
expect(resizeObservers).toHaveLength(1)
|
|
176
|
+
expect(log).toHaveBeenCalledTimes(1)
|
|
177
|
+
})
|
|
178
|
+
})
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
&:focus {
|
|
16
|
-
outline:
|
|
16
|
+
outline: $govuk-focus-width solid transparent;
|
|
17
17
|
color: govuk-functional-colour('focus-text');
|
|
18
18
|
background-color: govuk-functional-colour('focus');
|
|
19
19
|
box-shadow:
|
|
@@ -34,6 +34,27 @@
|
|
|
34
34
|
position: relative;
|
|
35
35
|
overflow-x: auto;
|
|
36
36
|
|
|
37
|
+
// Scroll shadows: an edge shadows only while more of the table is hidden
|
|
38
|
+
// past it. The body-coloured covers scroll with the content and hide each
|
|
39
|
+
// shadow once that end is reached, so a table that fits shows none.
|
|
40
|
+
background:
|
|
41
|
+
linear-gradient(
|
|
42
|
+
to right,
|
|
43
|
+
govuk-functional-colour('body-background') 30%,
|
|
44
|
+
transparent
|
|
45
|
+
)
|
|
46
|
+
left center / 40px 100% no-repeat local,
|
|
47
|
+
linear-gradient(
|
|
48
|
+
to left,
|
|
49
|
+
govuk-functional-colour('body-background') 30%,
|
|
50
|
+
transparent
|
|
51
|
+
)
|
|
52
|
+
right center / 40px 100% no-repeat local,
|
|
53
|
+
radial-gradient(farthest-side at 0 50%, rgba(0, 0, 0, 0.2), transparent)
|
|
54
|
+
left center / 14px 100% no-repeat scroll,
|
|
55
|
+
radial-gradient(farthest-side at 100% 50%, rgba(0, 0, 0, 0.2), transparent)
|
|
56
|
+
right center / 14px 100% no-repeat scroll;
|
|
57
|
+
|
|
37
58
|
// The GOV.UK non-text focus state - yellow alone has too little contrast
|
|
38
59
|
// against a white page. Both rings sit outside the box, clear of the text.
|
|
39
60
|
&:focus {
|
package/src/errors/index.js
CHANGED
|
@@ -58,10 +58,5 @@ export function catchAll(request, h) {
|
|
|
58
58
|
return h.continue
|
|
59
59
|
}
|
|
60
60
|
const { view, pageTitle } = errorPageFor(statusCode)
|
|
61
|
-
|
|
62
|
-
process.env.NODE_ENV === 'development'
|
|
63
|
-
? (response?.message ?? response?.source?.message)
|
|
64
|
-
: undefined
|
|
65
|
-
|
|
66
|
-
return h.view(view, { pageTitle, developmentMessage }).code(statusCode)
|
|
61
|
+
return h.view(view, { pageTitle }).code(statusCode)
|
|
67
62
|
}
|
package/src/errors/index.test.js
CHANGED
|
@@ -83,10 +83,7 @@ test.each([
|
|
|
83
83
|
catchAll(request, toolkit)
|
|
84
84
|
|
|
85
85
|
// Assert
|
|
86
|
-
assert.deepEqual(toolkit.view.mock.calls[0], [
|
|
87
|
-
view,
|
|
88
|
-
{ pageTitle, developmentMessage: undefined }
|
|
89
|
-
])
|
|
86
|
+
assert.deepEqual(toolkit.view.mock.calls[0], [view, { pageTitle }])
|
|
90
87
|
assert.deepEqual(toolkit.code.mock.calls[0], [statusCode])
|
|
91
88
|
}
|
|
92
89
|
)
|
|
@@ -103,8 +100,7 @@ test('catchAll logs server errors and renders the problem with the service page'
|
|
|
103
100
|
assert.deepEqual(toolkit.view.mock.calls[0], [
|
|
104
101
|
'error/problem-with-service',
|
|
105
102
|
{
|
|
106
|
-
pageTitle: 'Sorry, there is a problem with the service'
|
|
107
|
-
developmentMessage: undefined
|
|
103
|
+
pageTitle: 'Sorry, there is a problem with the service'
|
|
108
104
|
}
|
|
109
105
|
])
|
|
110
106
|
assert.deepEqual(toolkit.code.mock.calls[0], [
|
|
@@ -143,43 +139,12 @@ test('catchAll prefers a client-attached statusCode over a boomified 500', () =>
|
|
|
143
139
|
// Assert
|
|
144
140
|
assert.deepEqual(toolkit.view.mock.calls[0], [
|
|
145
141
|
'error/page-not-found',
|
|
146
|
-
{ pageTitle: 'Page not found'
|
|
142
|
+
{ pageTitle: 'Page not found' }
|
|
147
143
|
])
|
|
148
144
|
assert.deepEqual(toolkit.code.mock.calls[0], [statusCodes.notFound])
|
|
149
145
|
assert.equal(logger.error.mock.calls.length, 0)
|
|
150
146
|
})
|
|
151
147
|
|
|
152
|
-
test("catchAll passes the error's own message to the page in development", () => {
|
|
153
|
-
// Arrange
|
|
154
|
-
vi.stubEnv('NODE_ENV', 'development')
|
|
155
|
-
const request = createBoomRequest(statusCodes.internalServerError)
|
|
156
|
-
request.response.message = 'Upstream timed out'
|
|
157
|
-
const toolkit = createToolkit()
|
|
158
|
-
|
|
159
|
-
// Act
|
|
160
|
-
catchAll(request, toolkit)
|
|
161
|
-
|
|
162
|
-
// Assert
|
|
163
|
-
assert.equal(
|
|
164
|
-
toolkit.view.mock.calls[0][1].developmentMessage,
|
|
165
|
-
'Upstream timed out'
|
|
166
|
-
)
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
test("catchAll leaves out the error's own message outside development", () => {
|
|
170
|
-
// Arrange
|
|
171
|
-
vi.stubEnv('NODE_ENV', 'production')
|
|
172
|
-
const request = createBoomRequest(statusCodes.internalServerError)
|
|
173
|
-
request.response.message = 'Upstream timed out'
|
|
174
|
-
const toolkit = createToolkit()
|
|
175
|
-
|
|
176
|
-
// Act
|
|
177
|
-
catchAll(request, toolkit)
|
|
178
|
-
|
|
179
|
-
// Assert
|
|
180
|
-
assert.equal(toolkit.view.mock.calls[0][1].developmentMessage, undefined)
|
|
181
|
-
})
|
|
182
|
-
|
|
183
148
|
test('catchAll does not log the 503 the maintenance plugin answered with', () => {
|
|
184
149
|
// Arrange
|
|
185
150
|
const toolkit = createToolkit()
|
|
@@ -42,11 +42,43 @@ describe('searchForm()', () => {
|
|
|
42
42
|
|
|
43
43
|
// Assert
|
|
44
44
|
expect(html).toContain(
|
|
45
|
-
'<
|
|
45
|
+
'<div class="govuk-hint" id="search-hint">Search by ear tag</div>'
|
|
46
46
|
)
|
|
47
47
|
expect(html).toContain('aria-describedby="search-hint"')
|
|
48
48
|
})
|
|
49
49
|
|
|
50
|
+
test('makes the form the search landmark, labelled by the label text', () => {
|
|
51
|
+
// Act
|
|
52
|
+
const html = render({ label: 'Search animals', hint: 'Search by ear tag' })
|
|
53
|
+
|
|
54
|
+
// Assert
|
|
55
|
+
expect(html).toContain(
|
|
56
|
+
'<form method="GET" class="lis-search-form" role="search" aria-label="Search animals">'
|
|
57
|
+
)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('wraps the label, hint and input in the form', () => {
|
|
61
|
+
// Act
|
|
62
|
+
const html = render({ label: 'Search animals', hint: 'Search by ear tag' })
|
|
63
|
+
|
|
64
|
+
// Assert
|
|
65
|
+
expect(html.indexOf('<form')).toBeLessThan(html.indexOf('govuk-form-group'))
|
|
66
|
+
expect(html.indexOf('govuk-hint')).toBeLessThan(html.indexOf('</form>'))
|
|
67
|
+
expect(html.indexOf('</form>')).toBeGreaterThan(
|
|
68
|
+
html.indexOf('type="search"')
|
|
69
|
+
)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('renders the inset text outside the form', () => {
|
|
73
|
+
// Act
|
|
74
|
+
const html = render({ label: 'Search', insetText: 'Up to 24 hours' })
|
|
75
|
+
|
|
76
|
+
// Assert
|
|
77
|
+
expect(html.indexOf('govuk-inset-text')).toBeGreaterThan(
|
|
78
|
+
html.indexOf('</form>')
|
|
79
|
+
)
|
|
80
|
+
})
|
|
81
|
+
|
|
50
82
|
test('does not describe the input by a hint when none is given', () => {
|
|
51
83
|
// Act
|
|
52
84
|
const html = render({ label: 'Search' })
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{%- set id = params.id or params.name or "search" -%}
|
|
2
2
|
|
|
3
|
-
<
|
|
4
|
-
<
|
|
5
|
-
<
|
|
6
|
-
{{
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
<form method="GET" class="lis-search-form" role="search" aria-label="{{ params.label }}">
|
|
4
|
+
<div class="govuk-form-group">
|
|
5
|
+
<h2 class="govuk-label-wrapper">
|
|
6
|
+
<label class="govuk-label govuk-label--m" for="{{ id }}">
|
|
7
|
+
{{ params.label }}
|
|
8
|
+
</label>
|
|
9
|
+
</h2>
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
{%- if params.hint -%}
|
|
12
|
+
<div class="govuk-hint" id="{{ id }}-hint">{{ params.hint }}</div>
|
|
13
|
+
{%- endif -%}
|
|
13
14
|
|
|
14
|
-
<form method="GET" class="lis-search-form">
|
|
15
15
|
{%- for field in params.hiddenFields -%}
|
|
16
16
|
<input type="hidden" name="{{ field.name }}" value="{{ field.value }}">
|
|
17
17
|
{%- endfor -%}
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
</button>
|
|
35
35
|
</div>
|
|
36
36
|
</div>
|
|
37
|
-
</
|
|
38
|
-
</
|
|
37
|
+
</div>
|
|
38
|
+
</form>
|
|
39
39
|
|
|
40
40
|
{%- if params.insetText -%}
|
|
41
41
|
<div class="govuk-inset-text">
|
|
@@ -169,7 +169,21 @@ test('wraps the table in a focusable, labelled region so it can be scrolled by k
|
|
|
169
169
|
|
|
170
170
|
assert.match(
|
|
171
171
|
html,
|
|
172
|
-
/<div class="lis-sortable-table__wrapper" tabindex="0" role="region" aria-label="Animals table">\s*<table/
|
|
172
|
+
/<div class="lis-sortable-table__wrapper" data-module="lis-scrollable-table" tabindex="0" role="region" aria-label="Animals table">\s*<table/
|
|
173
|
+
)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
test('marks the wrapper as a lis-scrollable-table module', () => {
|
|
177
|
+
const html = render({
|
|
178
|
+
caption: 'Animals',
|
|
179
|
+
columns,
|
|
180
|
+
rows,
|
|
181
|
+
baseHref: '/animals'
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
assert.match(
|
|
185
|
+
html,
|
|
186
|
+
/<div class="lis-sortable-table__wrapper" data-module="lis-scrollable-table"/
|
|
173
187
|
)
|
|
174
188
|
})
|
|
175
189
|
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
{{ params.caption }}{% if sortableColumns | length %}<span class="govuk-visually-hidden"> (column headers with links are sortable).</span>{% endif %}
|
|
47
47
|
{%- endset -%}
|
|
48
48
|
|
|
49
|
-
<div class="lis-sortable-table__wrapper" tabindex="0" role="region" aria-label="{{ (params.caption ~ ' table') if params.caption else 'Table' }}">
|
|
49
|
+
<div class="lis-sortable-table__wrapper" data-module="lis-scrollable-table" tabindex="0" role="region" aria-label="{{ (params.caption ~ ' table') if params.caption else 'Table' }}">
|
|
50
50
|
{{ govukTable({
|
|
51
51
|
caption: (captionHtml | safe) if params.caption else undefined,
|
|
52
52
|
captionClasses: params.captionClasses or "govuk-visually-hidden",
|
package/src/nunjucks/context.js
CHANGED
|
@@ -52,8 +52,6 @@ function createPermissionCheck(request) {
|
|
|
52
52
|
/**
|
|
53
53
|
* @typedef {object} ErrorPageLinks
|
|
54
54
|
* @property {string} [contactHref] - every error page's contact link; defaults to the BCMS GOV.UK page
|
|
55
|
-
* @property {string} [homeHref] - the page not found "Home page" link; defaults to '/'
|
|
56
|
-
* @property {string} [getStartedHref] - the page not found "Get started" link; defaults to '/'
|
|
57
55
|
*/
|
|
58
56
|
|
|
59
57
|
/**
|
|
@@ -69,9 +67,7 @@ export function createNunjucksContextBuilder({
|
|
|
69
67
|
errorPages = {}
|
|
70
68
|
}) {
|
|
71
69
|
const errorPageLinks = {
|
|
72
|
-
contactHref: errorPages.contactHref ?? BCMS_CONTACT_HREF
|
|
73
|
-
homeHref: errorPages.homeHref ?? '/',
|
|
74
|
-
getStartedHref: errorPages.getStartedHref ?? '/'
|
|
70
|
+
contactHref: errorPages.contactHref ?? BCMS_CONTACT_HREF
|
|
75
71
|
}
|
|
76
72
|
const sectionName = moduleId ? (getModuleById(moduleId)?.label ?? null) : null
|
|
77
73
|
const manifestPath = path.join(
|
|
@@ -292,7 +292,7 @@ describe('createNunjucksContextBuilder() hub flags', () => {
|
|
|
292
292
|
})
|
|
293
293
|
|
|
294
294
|
describe('createNunjucksContextBuilder() errorPages', () => {
|
|
295
|
-
test('defaults the contact link to the BCMS GOV.UK page
|
|
295
|
+
test('defaults the contact link to the BCMS GOV.UK page', () => {
|
|
296
296
|
// Arrange
|
|
297
297
|
const contextBuilder = createNunjucksContextBuilder({
|
|
298
298
|
config: createConfig(),
|
|
@@ -308,9 +308,7 @@ describe('createNunjucksContextBuilder() errorPages', () => {
|
|
|
308
308
|
// Assert
|
|
309
309
|
expect(context.errorPages).toEqual({
|
|
310
310
|
contactHref:
|
|
311
|
-
'https://www.gov.uk/government/organisations/british-cattle-movement-service'
|
|
312
|
-
homeHref: '/',
|
|
313
|
-
getStartedHref: '/'
|
|
311
|
+
'https://www.gov.uk/government/organisations/british-cattle-movement-service'
|
|
314
312
|
})
|
|
315
313
|
})
|
|
316
314
|
|
|
@@ -322,9 +320,7 @@ describe('createNunjucksContextBuilder() errorPages', () => {
|
|
|
322
320
|
logger: {},
|
|
323
321
|
readFileSync: () => '{}',
|
|
324
322
|
errorPages: {
|
|
325
|
-
contactHref: 'https://example.com/contact'
|
|
326
|
-
homeHref: '/home',
|
|
327
|
-
getStartedHref: '/start'
|
|
323
|
+
contactHref: 'https://example.com/contact'
|
|
328
324
|
}
|
|
329
325
|
})
|
|
330
326
|
const request = { headers: {}, path: '/', auth: { credentials: null } }
|
|
@@ -334,20 +330,18 @@ describe('createNunjucksContextBuilder() errorPages', () => {
|
|
|
334
330
|
|
|
335
331
|
// Assert
|
|
336
332
|
expect(context.errorPages).toEqual({
|
|
337
|
-
contactHref: 'https://example.com/contact'
|
|
338
|
-
homeHref: '/home',
|
|
339
|
-
getStartedHref: '/start'
|
|
333
|
+
contactHref: 'https://example.com/contact'
|
|
340
334
|
})
|
|
341
335
|
})
|
|
342
336
|
|
|
343
|
-
test('keeps the default for
|
|
337
|
+
test('keeps the default for a link the errorPages option leaves undefined', () => {
|
|
344
338
|
// Arrange
|
|
345
339
|
const contextBuilder = createNunjucksContextBuilder({
|
|
346
340
|
config: createConfig(),
|
|
347
341
|
getRequestBasePath: () => '',
|
|
348
342
|
logger: {},
|
|
349
343
|
readFileSync: () => '{}',
|
|
350
|
-
errorPages: {
|
|
344
|
+
errorPages: { contactHref: undefined }
|
|
351
345
|
})
|
|
352
346
|
const request = { headers: {}, path: '/', auth: { credentials: null } }
|
|
353
347
|
|
|
@@ -357,9 +351,7 @@ describe('createNunjucksContextBuilder() errorPages', () => {
|
|
|
357
351
|
// Assert
|
|
358
352
|
expect(context.errorPages).toEqual({
|
|
359
353
|
contactHref:
|
|
360
|
-
'https://www.gov.uk/government/organisations/british-cattle-movement-service'
|
|
361
|
-
homeHref: '/',
|
|
362
|
-
getStartedHref: '/start'
|
|
354
|
+
'https://www.gov.uk/government/organisations/british-cattle-movement-service'
|
|
363
355
|
})
|
|
364
356
|
})
|
|
365
357
|
})
|
|
@@ -94,9 +94,7 @@ describe('createNunjucksConfig()', () => {
|
|
|
94
94
|
get: vi.fn((key) => (key === 'root' ? projectRoot : false))
|
|
95
95
|
}
|
|
96
96
|
const errorPages = {
|
|
97
|
-
contactHref: 'https://example.com/contact'
|
|
98
|
-
homeHref: '/home',
|
|
99
|
-
getStartedHref: '/start'
|
|
97
|
+
contactHref: 'https://example.com/contact'
|
|
100
98
|
}
|
|
101
99
|
|
|
102
100
|
// Act
|
|
@@ -38,7 +38,7 @@ function textOf(html) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
describe('error pages', () => {
|
|
41
|
-
test('page not found shows the heading, the
|
|
41
|
+
test('page not found shows the heading, the guidance and the BCMS link', () => {
|
|
42
42
|
// Arrange
|
|
43
43
|
const context = { pageTitle: 'Page not found' }
|
|
44
44
|
|
|
@@ -51,18 +51,13 @@ describe('error pages', () => {
|
|
|
51
51
|
)
|
|
52
52
|
expect(html).toContain('<h1 class="govuk-heading-l">Page not found</h1>')
|
|
53
53
|
expect(textOf(html)).toContain(
|
|
54
|
-
|
|
54
|
+
'If you typed the web address, check it is correct. If you pasted the web address, check you copied the entire address. If the web address is correct or you selected a link or button, contact the British Cattle Movement Service (BCMS) if you need to speak to someone about your account.'
|
|
55
55
|
)
|
|
56
|
-
expect(html).toContain('<a class="govuk-link" href="/home">Home page</a>')
|
|
57
56
|
expect(html).toContain(
|
|
58
|
-
'<a class="govuk-link" href="/
|
|
59
|
-
)
|
|
60
|
-
expect(html).toContain(
|
|
61
|
-
'<a class="govuk-link" href="https://www.gov.uk">GOV.UK</a>'
|
|
62
|
-
)
|
|
63
|
-
expect(html).toContain(
|
|
64
|
-
'<a class="govuk-link" href="https://example.com/contact">contact our support team</a>'
|
|
57
|
+
'<a class="govuk-link" href="https://example.com/contact">British Cattle Movement Service (BCMS)</a>'
|
|
65
58
|
)
|
|
59
|
+
expect(html).not.toContain('Home page')
|
|
60
|
+
expect(html).not.toContain('Get started')
|
|
66
61
|
})
|
|
67
62
|
|
|
68
63
|
test('not authorised offers a sign in button when signInHref is given', () => {
|
|
@@ -104,7 +99,7 @@ describe('error pages', () => {
|
|
|
104
99
|
'<title>Sorry, you are not authorised to view this page - Livestock Information</title>'
|
|
105
100
|
)
|
|
106
101
|
expect(html).toContain(
|
|
107
|
-
'<h1 class="govuk-heading-l">Sorry, you are not authorised to view this page
|
|
102
|
+
'<h1 class="govuk-heading-l">Sorry, you are not authorised to view this page</h1>'
|
|
108
103
|
)
|
|
109
104
|
expect(textOf(html)).toContain(
|
|
110
105
|
'Check your details are correct and try again. If you think this is incorrect contact our support team.'
|
|
@@ -160,35 +155,4 @@ describe('error pages', () => {
|
|
|
160
155
|
'<a class="govuk-link" href="https://example.com/contact">British Cattle Movement Service (BCMS)</a>'
|
|
161
156
|
)
|
|
162
157
|
})
|
|
163
|
-
|
|
164
|
-
test('service unavailable says later when no return time is given', () => {
|
|
165
|
-
// Arrange
|
|
166
|
-
const context = { pageTitle: 'Sorry, the service is unavailable' }
|
|
167
|
-
|
|
168
|
-
// Act
|
|
169
|
-
const html = render('error/service-unavailable.njk', context)
|
|
170
|
-
|
|
171
|
-
// Assert
|
|
172
|
-
expect(textOf(html)).toContain('You will be able to use the service later.')
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
test.each([
|
|
176
|
-
'error/page-not-found.njk',
|
|
177
|
-
'error/not-authorised.njk',
|
|
178
|
-
'error/problem-with-service.njk',
|
|
179
|
-
'error/service-unavailable.njk'
|
|
180
|
-
])('%s shows the development message only when one is given', (view) => {
|
|
181
|
-
// Arrange
|
|
182
|
-
const withMessage = { developmentMessage: 'Upstream timed out' }
|
|
183
|
-
|
|
184
|
-
// Act
|
|
185
|
-
const htmlWithMessage = render(view, withMessage)
|
|
186
|
-
const htmlWithoutMessage = render(view)
|
|
187
|
-
|
|
188
|
-
// Assert
|
|
189
|
-
expect(htmlWithMessage).toContain(
|
|
190
|
-
'<p class="govuk-body">Upstream timed out</p>'
|
|
191
|
-
)
|
|
192
|
-
expect(htmlWithoutMessage).not.toContain('Upstream timed out')
|
|
193
|
-
})
|
|
194
158
|
})
|
|
@@ -3,15 +3,12 @@
|
|
|
3
3
|
{% block content %}
|
|
4
4
|
<div class="govuk-grid-row">
|
|
5
5
|
<div class="govuk-grid-column-two-thirds">
|
|
6
|
-
<h1 class="govuk-heading-l">Sorry, you are not authorised to view this page
|
|
6
|
+
<h1 class="govuk-heading-l">Sorry, you are not authorised to view this page</h1>
|
|
7
7
|
<p class="govuk-body">Check your details are correct and try again.</p>
|
|
8
8
|
<p class="govuk-body">If you think this is incorrect <a class="govuk-link" href="{{ errorPages.contactHref }}">contact our support team</a>.</p>
|
|
9
9
|
{% if signInHref %}
|
|
10
10
|
<a href="{{ signInHref }}" role="button" draggable="false" class="govuk-button" data-module="govuk-button">Sign in</a>
|
|
11
11
|
{% endif %}
|
|
12
|
-
{% if developmentMessage %}
|
|
13
|
-
<p class="govuk-body">{{ developmentMessage }}</p>
|
|
14
|
-
{% endif %}
|
|
15
12
|
</div>
|
|
16
13
|
</div>
|
|
17
14
|
{% endblock %}
|
|
@@ -4,17 +4,9 @@
|
|
|
4
4
|
<div class="govuk-grid-row">
|
|
5
5
|
<div class="govuk-grid-column-two-thirds">
|
|
6
6
|
<h1 class="govuk-heading-l">Page not found</h1>
|
|
7
|
-
<p class="govuk-body">
|
|
8
|
-
<p class="govuk-body">
|
|
9
|
-
<
|
|
10
|
-
<li><a class="govuk-link" href="{{ errorPages.homeHref }}">Home page</a></li>
|
|
11
|
-
<li><a class="govuk-link" href="{{ errorPages.getStartedHref }}">Get started</a></li>
|
|
12
|
-
<li><a class="govuk-link" href="https://www.gov.uk">GOV.UK</a></li>
|
|
13
|
-
</ul>
|
|
14
|
-
<p class="govuk-body">If you cannot find the information you need, <a class="govuk-link" href="{{ errorPages.contactHref }}">contact our support team</a>.</p>
|
|
15
|
-
{% if developmentMessage %}
|
|
16
|
-
<p class="govuk-body">{{ developmentMessage }}</p>
|
|
17
|
-
{% endif %}
|
|
7
|
+
<p class="govuk-body">If you typed the web address, check it is correct.</p>
|
|
8
|
+
<p class="govuk-body">If you pasted the web address, check you copied the entire address.</p>
|
|
9
|
+
<p class="govuk-body">If the web address is correct or you selected a link or button, contact the <a class="govuk-link" href="{{ errorPages.contactHref }}">British Cattle Movement Service (BCMS)</a> if you need to speak to someone about your account.</p>
|
|
18
10
|
</div>
|
|
19
11
|
</div>
|
|
20
12
|
{% endblock %}
|
|
@@ -6,9 +6,6 @@
|
|
|
6
6
|
<h1 class="govuk-heading-l">Sorry, there is a problem with the service</h1>
|
|
7
7
|
<p class="govuk-body">Try again later.</p>
|
|
8
8
|
<p class="govuk-body">Contact the <a class="govuk-link" href="{{ errorPages.contactHref }}">British Cattle Movement Service (BCMS)</a> if you need to speak to someone about your account.</p>
|
|
9
|
-
{% if developmentMessage %}
|
|
10
|
-
<p class="govuk-body">{{ developmentMessage }}</p>
|
|
11
|
-
{% endif %}
|
|
12
9
|
</div>
|
|
13
10
|
</div>
|
|
14
11
|
{% endblock %}
|
|
@@ -10,9 +10,6 @@
|
|
|
10
10
|
<p class="govuk-body">You will be able to use the service later.</p>
|
|
11
11
|
{% endif %}
|
|
12
12
|
<p class="govuk-body">Contact the <a class="govuk-link" href="{{ errorPages.contactHref }}">British Cattle Movement Service (BCMS)</a> if you need to speak to someone about your account.</p>
|
|
13
|
-
{% if developmentMessage %}
|
|
14
|
-
<p class="govuk-body">{{ developmentMessage }}</p>
|
|
15
|
-
{% endif %}
|
|
16
13
|
</div>
|
|
17
14
|
</div>
|
|
18
15
|
{% endblock %}
|
|
@@ -38,13 +38,16 @@
|
|
|
38
38
|
<link href="{{ getAssetPath('src/client/stylesheets/application.scss') }}" rel="stylesheet">
|
|
39
39
|
{% endblock %}
|
|
40
40
|
|
|
41
|
-
{% block
|
|
41
|
+
{% block govukHeader %}
|
|
42
42
|
{{ govukHeader({
|
|
43
43
|
homepageUrl: "https://www.gov.uk/",
|
|
44
44
|
classes: "app-header",
|
|
45
45
|
containerClasses: "govuk-width-container",
|
|
46
46
|
useTudorCrown: true
|
|
47
47
|
}) }}
|
|
48
|
+
{% endblock %}
|
|
49
|
+
|
|
50
|
+
{% block govukServiceNavigation %}
|
|
48
51
|
{% if isSignedIn %}
|
|
49
52
|
{% set signOutNavigationEnd %}
|
|
50
53
|
<li class="govuk-service-navigation__item app-service-navigation__item--right">
|
|
@@ -60,7 +63,9 @@
|
|
|
60
63
|
navigationEnd: signOutNavigationEnd
|
|
61
64
|
}
|
|
62
65
|
}) }}
|
|
66
|
+
{% endblock %}
|
|
63
67
|
|
|
68
|
+
{% block headerEnd %}
|
|
64
69
|
{{ govukPhaseBanner({
|
|
65
70
|
tag: {
|
|
66
71
|
html: "<b>Steel Thread</b>",
|
|
@@ -84,7 +89,7 @@
|
|
|
84
89
|
|
|
85
90
|
{% block content %}{% endblock %}
|
|
86
91
|
|
|
87
|
-
{% block
|
|
92
|
+
{% block govukFooter %}
|
|
88
93
|
{{ govukFooter({
|
|
89
94
|
meta: {
|
|
90
95
|
items: [
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
5
|
+
import nunjucks from 'nunjucks'
|
|
6
|
+
import { describe, expect, test } from 'vitest'
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url)
|
|
9
|
+
const govukRoot = path.join(
|
|
10
|
+
path.dirname(require.resolve('govuk-frontend/package.json')),
|
|
11
|
+
'dist'
|
|
12
|
+
)
|
|
13
|
+
const layoutsRoot = path.dirname(fileURLToPath(import.meta.url))
|
|
14
|
+
const templatesRoot = path.dirname(layoutsRoot)
|
|
15
|
+
const componentsRoot = path.join(path.dirname(templatesRoot), 'components')
|
|
16
|
+
const environment = new nunjucks.Environment(
|
|
17
|
+
new nunjucks.FileSystemLoader([templatesRoot, componentsRoot, govukRoot]),
|
|
18
|
+
{ autoescape: true, trimBlocks: true, lstripBlocks: true }
|
|
19
|
+
)
|
|
20
|
+
environment.addGlobal('getAssetPath', (asset) => `/${asset}`)
|
|
21
|
+
|
|
22
|
+
function render(context = {}) {
|
|
23
|
+
return environment.render('layouts/base.njk', {
|
|
24
|
+
isSignedIn: true,
|
|
25
|
+
logoutUrl: '/sign-out',
|
|
26
|
+
navigation: [{ href: '/one', text: 'One' }],
|
|
27
|
+
...context
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function countOf(html, fragment) {
|
|
32
|
+
return html.split(fragment).length - 1
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('base layout', () => {
|
|
36
|
+
test('renders one header landmark wrapping the header and service navigation', () => {
|
|
37
|
+
// Act
|
|
38
|
+
const html = render()
|
|
39
|
+
const headerStart = html.indexOf('<header class="govuk-template__header')
|
|
40
|
+
const headerEnd = html.indexOf('</header>', headerStart)
|
|
41
|
+
const headerHtml = html.slice(headerStart, headerEnd)
|
|
42
|
+
|
|
43
|
+
// Assert
|
|
44
|
+
expect(countOf(html, '<header class="govuk-template__header')).toBe(1)
|
|
45
|
+
expect(headerHtml).toContain('govuk-header')
|
|
46
|
+
expect(headerHtml).toContain('govuk-service-navigation')
|
|
47
|
+
expect(headerHtml).toContain('govuk-phase-banner')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('renders one footer landmark wrapping the footer', () => {
|
|
51
|
+
// Act
|
|
52
|
+
const html = render()
|
|
53
|
+
const footerStart = html.indexOf('<footer class="govuk-template__footer')
|
|
54
|
+
const footerEnd = html.indexOf('</footer>', footerStart)
|
|
55
|
+
|
|
56
|
+
// Assert
|
|
57
|
+
expect(countOf(html, '<footer class="govuk-template__footer')).toBe(1)
|
|
58
|
+
expect(html.slice(footerStart, footerEnd)).toContain('govuk-footer')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('renders the sign out link when signed in', () => {
|
|
62
|
+
// Act
|
|
63
|
+
const html = render()
|
|
64
|
+
|
|
65
|
+
// Assert
|
|
66
|
+
expect(html).toContain('href="/sign-out"')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('does not render the sign out link when signed out', () => {
|
|
70
|
+
// Act
|
|
71
|
+
const html = render({ isSignedIn: false })
|
|
72
|
+
|
|
73
|
+
// Assert
|
|
74
|
+
expect(html).not.toContain('Sign out')
|
|
75
|
+
})
|
|
76
|
+
})
|