@financial-times/dotcom-middleware-navigation 7.3.0 → 7.3.2
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/dist/node/handleEdition.d.ts +2 -0
- package/dist/node/index.d.ts +1 -0
- package/dist/node/navigation.d.ts +7 -0
- package/dist/node/normalizePath.d.ts +1 -0
- package/dist/tsconfig.tsbuildinfo +2082 -0
- package/package.json +9 -6
- package/src/__test__/handleEdition.spec.ts +64 -0
- package/src/__test__/navigation.spec.ts +184 -0
package/package.json
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@financial-times/dotcom-middleware-navigation",
|
|
3
|
-
"version": "7.3.
|
|
3
|
+
"version": "7.3.2",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/node/index.js",
|
|
6
6
|
"types": "src/index.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
9
|
-
"tsc": "../../node_modules/.bin/tsc --incremental",
|
|
10
9
|
"clean": "npm run clean:dist && npm run clean:node_modules",
|
|
11
10
|
"clean:dist": "rm -rf dist",
|
|
12
11
|
"clean:node_modules": "rm -rf node_modules",
|
|
12
|
+
"build:node": "tsc",
|
|
13
13
|
"build": "npm run build:node",
|
|
14
|
-
"build:node": "npm run tsc -- --module commonjs --outDir ./dist/node",
|
|
15
14
|
"dev": "npm run build:node -- --watch",
|
|
16
15
|
"preinstall": "[ \"$INIT_CWD\" != \"$PWD\" ] || npm_config_yes=true npx check-engine"
|
|
17
16
|
},
|
|
@@ -19,8 +18,8 @@
|
|
|
19
18
|
"author": "",
|
|
20
19
|
"license": "MIT",
|
|
21
20
|
"dependencies": {
|
|
22
|
-
"@financial-times/dotcom-server-navigation": "
|
|
23
|
-
"@financial-times/dotcom-types-navigation": "
|
|
21
|
+
"@financial-times/dotcom-server-navigation": "file:../../packages/dotcom-server-navigation",
|
|
22
|
+
"@financial-times/dotcom-types-navigation": "file:../dotcom-types-navigation"
|
|
24
23
|
},
|
|
25
24
|
"devDependencies": {
|
|
26
25
|
"check-engine": "^1.10.1",
|
|
@@ -30,6 +29,10 @@
|
|
|
30
29
|
"node": ">= 14.0.0",
|
|
31
30
|
"npm": "7.x || 8.x"
|
|
32
31
|
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist/",
|
|
34
|
+
"src/"
|
|
35
|
+
],
|
|
33
36
|
"repository": {
|
|
34
37
|
"type": "git",
|
|
35
38
|
"repository": "https://github.com/Financial-Times/dotcom-page-kit.git",
|
|
@@ -39,4 +42,4 @@
|
|
|
39
42
|
"volta": {
|
|
40
43
|
"extends": "../../package.json"
|
|
41
44
|
}
|
|
42
|
-
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import subject from '../handleEdition'
|
|
2
|
+
import httpMocks from 'node-mocks-http'
|
|
3
|
+
|
|
4
|
+
describe('dotcom-middleware-navigation/src/handleEdition', () => {
|
|
5
|
+
describe('with no query string parameter or cookie set', () => {
|
|
6
|
+
let request
|
|
7
|
+
let response
|
|
8
|
+
let result
|
|
9
|
+
|
|
10
|
+
beforeAll(() => {
|
|
11
|
+
request = httpMocks.createRequest()
|
|
12
|
+
response = httpMocks.createResponse()
|
|
13
|
+
result = subject(request, response)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('returns the default edition', () => {
|
|
17
|
+
expect(result).toEqual('uk')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('sets an edition vary header', () => {
|
|
21
|
+
expect(response.getHeader('vary')).toBe('FT-Edition')
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('with an edition header set', () => {
|
|
26
|
+
let request
|
|
27
|
+
let response
|
|
28
|
+
let result
|
|
29
|
+
|
|
30
|
+
beforeAll(() => {
|
|
31
|
+
request = httpMocks.createRequest({ headers: { 'ft-edition': 'international' } })
|
|
32
|
+
response = httpMocks.createResponse()
|
|
33
|
+
result = subject(request, response)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('returns the set edition', () => {
|
|
37
|
+
expect(result).toEqual('international')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('sets an edition vary header', () => {
|
|
41
|
+
expect(response.getHeader('vary')).toBe('FT-Edition')
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('with a query string parameter set', () => {
|
|
46
|
+
let request
|
|
47
|
+
let response
|
|
48
|
+
let result
|
|
49
|
+
|
|
50
|
+
beforeAll(() => {
|
|
51
|
+
request = httpMocks.createRequest({ query: { edition: 'international' } })
|
|
52
|
+
response = httpMocks.createResponse()
|
|
53
|
+
result = subject(request, response)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('returns the selected edition', () => {
|
|
57
|
+
expect(result).toEqual('international')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('sets a next-edition cookie with the selected edition', () => {
|
|
61
|
+
expect(response.cookies['next-edition'].value).toBe('international')
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
})
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import * as subject from '../navigation'
|
|
2
|
+
import httpMocks from 'node-mocks-http'
|
|
3
|
+
|
|
4
|
+
const fakeMenusData = {
|
|
5
|
+
navbar: {
|
|
6
|
+
label: 'Navbar UK',
|
|
7
|
+
items: [{ label: 'Foo', url: '#' }]
|
|
8
|
+
},
|
|
9
|
+
drawer: {
|
|
10
|
+
label: 'Drawer UK',
|
|
11
|
+
items: [{ label: 'Foo', url: '#' }]
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const fakeEditionsData = {
|
|
16
|
+
current: { id: 'uk', name: 'UK', url: '/' },
|
|
17
|
+
others: [{ id: 'international', name: 'International', url: '/' }]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const fakeActionsData = { id: 'subscribe', name: 'Subscribe for full access', url: '/products?' }
|
|
21
|
+
|
|
22
|
+
const fakeSubNavigationData = {
|
|
23
|
+
breadcrumb: 'some-breadcrumb',
|
|
24
|
+
subsections: 'some-subsections'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const fakeNavigationData = { ...fakeMenusData, editions: fakeEditionsData, currentPath: '' }
|
|
28
|
+
|
|
29
|
+
const FakeNavigation = {
|
|
30
|
+
getSubscribeAction: jest.fn().mockResolvedValue(fakeActionsData),
|
|
31
|
+
getMenusFor: jest.fn().mockResolvedValue(fakeMenusData),
|
|
32
|
+
getSubNavigationFor: jest.fn().mockResolvedValue(fakeSubNavigationData),
|
|
33
|
+
getEditionsFor: jest.fn().mockReturnValue(fakeEditionsData)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
jest.mock(
|
|
37
|
+
'@financial-times/dotcom-server-navigation',
|
|
38
|
+
() => {
|
|
39
|
+
return {
|
|
40
|
+
Navigation: jest.fn().mockImplementation(() => FakeNavigation)
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
{ virtual: true }
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
describe('dotcom-middleware-navigation', () => {
|
|
47
|
+
let request
|
|
48
|
+
let response
|
|
49
|
+
let next
|
|
50
|
+
let instance
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
request = httpMocks.createRequest()
|
|
54
|
+
response = httpMocks.createResponse({ locals: {} })
|
|
55
|
+
next = jest.fn()
|
|
56
|
+
instance = subject.init()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
jest.clearAllMocks()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('returns a middleware function', () => {
|
|
64
|
+
expect(instance).toBeInstanceOf(Function)
|
|
65
|
+
expect(instance.length).toBe(3)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('when handling a request', () => {
|
|
69
|
+
it('appends the navigation properties to response.locals', async () => {
|
|
70
|
+
await instance(request, response, next)
|
|
71
|
+
expect(response.locals.navigation).toEqual(expect.objectContaining(fakeNavigationData))
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('prefers the vanity URL if set', async () => {
|
|
75
|
+
const request = httpMocks.createRequest({
|
|
76
|
+
path: '/path/to/app/1234',
|
|
77
|
+
headers: {
|
|
78
|
+
'ft-vanity-url': '/vanity-url'
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
await instance(request, response, next)
|
|
83
|
+
expect(FakeNavigation.getMenusFor).toHaveBeenCalledWith('/vanity-url', 'uk')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('calls the fallthrough function', async () => {
|
|
87
|
+
await instance(request, response, next)
|
|
88
|
+
expect(next).toHaveBeenCalled()
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe('when handling a request with sub-navigation enabled', () => {
|
|
93
|
+
let instance
|
|
94
|
+
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
instance = subject.init({ enableSubNavigation: true })
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('prefers the path provided by the vanity URL header when available', async () => {
|
|
100
|
+
request = httpMocks.createRequest({
|
|
101
|
+
path: '/path/to/page/123',
|
|
102
|
+
headers: {
|
|
103
|
+
'ft-vanity-url': '/vanity-url'
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
await instance(request, response, next)
|
|
108
|
+
|
|
109
|
+
expect(FakeNavigation.getSubNavigationFor).toHaveBeenCalledWith('/vanity-url')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('normalizes the current path', async () => {
|
|
113
|
+
request = httpMocks.createRequest({
|
|
114
|
+
path: '/path/to/page/123',
|
|
115
|
+
query: {
|
|
116
|
+
page: 2
|
|
117
|
+
},
|
|
118
|
+
headers: {
|
|
119
|
+
'ft-vanity-url': '/vanity-url?page=2'
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
await instance(request, response, next)
|
|
124
|
+
|
|
125
|
+
expect(FakeNavigation.getSubNavigationFor).toHaveBeenCalledWith('/vanity-url')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('appends the sub-navigation properties on response.locals for the current path', async () => {
|
|
129
|
+
await instance(request, response, next)
|
|
130
|
+
expect(response.locals.navigation).toEqual(expect.objectContaining(fakeSubNavigationData))
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('fails silently if the the sub-navigation request fails', async () => {
|
|
134
|
+
FakeNavigation.getSubNavigationFor.mockRejectedValueOnce(new Error('Fail'))
|
|
135
|
+
|
|
136
|
+
await instance(request, response, next)
|
|
137
|
+
|
|
138
|
+
expect(next).not.toHaveBeenCalledWith(expect.any(Error))
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
describe('when handling a request with a custom getCurrentPath', () => {
|
|
143
|
+
it('executes the provided getCurrentPath function', async () => {
|
|
144
|
+
request = httpMocks.createRequest({
|
|
145
|
+
path: '/path/to/page/123',
|
|
146
|
+
headers: {
|
|
147
|
+
'ft-vanity-url': '/vanity-url?page=2'
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
const dummyPath = '/foo'
|
|
151
|
+
const instance = subject.init({ getCurrentPath: () => dummyPath })
|
|
152
|
+
await instance(request, response, next)
|
|
153
|
+
|
|
154
|
+
expect(FakeNavigation.getMenusFor).toHaveBeenCalledWith(dummyPath, 'uk')
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('allows overriding of how to calculate current path logic', async () => {
|
|
158
|
+
request = httpMocks.createRequest({
|
|
159
|
+
path: '/path/to/page/123',
|
|
160
|
+
headers: {
|
|
161
|
+
'ft-blocked-url': '/ig-content-test'
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
const instance = subject.init({ getCurrentPath: (request) => request.get('ft-blocked-url') })
|
|
166
|
+
await instance(request, response, next)
|
|
167
|
+
|
|
168
|
+
expect(FakeNavigation.getMenusFor).toHaveBeenCalledWith('/ig-content-test', 'uk')
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
describe('when something goes wrong', () => {
|
|
173
|
+
beforeEach(() => {
|
|
174
|
+
FakeNavigation.getMenusFor = jest.fn(() => {
|
|
175
|
+
throw Error('Whoops')
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('catches the error safely', async () => {
|
|
180
|
+
await instance(request, response, next)
|
|
181
|
+
expect(next).toHaveBeenCalledWith(expect.any(Error))
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
})
|