@financial-times/dotcom-server-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/package.json CHANGED
@@ -1,17 +1,16 @@
1
1
  {
2
2
  "name": "@financial-times/dotcom-server-navigation",
3
- "version": "7.3.0",
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,7 +18,7 @@
19
18
  "author": "",
20
19
  "license": "MIT",
21
20
  "dependencies": {
22
- "@financial-times/dotcom-types-navigation": "^7.3.0",
21
+ "@financial-times/dotcom-types-navigation": "file:../dotcom-types-navigation",
23
22
  "@types/deep-freeze": "^0.1.1",
24
23
  "deep-freeze": "0.0.1",
25
24
  "ft-poller": "^7.2.1",
@@ -35,6 +34,10 @@
35
34
  "node": ">= 14.0.0",
36
35
  "npm": "7.x || 8.x"
37
36
  },
37
+ "files": [
38
+ "dist/",
39
+ "src/"
40
+ ],
38
41
  "repository": {
39
42
  "type": "git",
40
43
  "repository": "https://github.com/Financial-Times/dotcom-page-kit.git",
@@ -44,4 +47,4 @@
44
47
  "volta": {
45
48
  "extends": "../../package.json"
46
49
  }
47
- }
50
+ }
@@ -0,0 +1,62 @@
1
+ import { TNavMenus, TNavMeganav, TNavMenu } from '@financial-times/dotcom-types-navigation'
2
+
3
+ const meganav: TNavMeganav[] = [
4
+ {
5
+ component: 'sectionlist',
6
+ dataset: 'subsections',
7
+ title: 'Sections',
8
+ data: [[{ label: 'Mega Foo', url: '/world' }], [{ label: 'Mega Bar', url: '/world/uk', submenu: null }]]
9
+ },
10
+ {
11
+ component: 'articlelist',
12
+ dataset: 'popular',
13
+ title: 'Most Read',
14
+ data: [
15
+ { label: 'Mega Baz', url: '/content/baz' },
16
+ { label: 'Mega Qux', url: '/content/qux?location=${currentPath}' }
17
+ ]
18
+ }
19
+ ]
20
+
21
+ const submenu: TNavMenu = {
22
+ label: 'submenu',
23
+ items: [
24
+ { label: 'Baz', url: '/world/uk', submenu: null },
25
+ { label: 'Qux', url: '/fake-item-nested?location=${currentPath}', submenu: null }
26
+ ]
27
+ }
28
+
29
+ const menus: Partial<TNavMenus> = {
30
+ 'navbar-uk': {
31
+ label: 'Navigation',
32
+ items: [
33
+ { label: 'Foo', url: '/world/uk', submenu: null },
34
+ { label: 'Bar', url: '/fake-item?location=${currentPath}', submenu, meganav }
35
+ ]
36
+ },
37
+
38
+ footer: {
39
+ label: 'Footer',
40
+ items: [
41
+ {
42
+ label: 'Tools',
43
+ url: null,
44
+ submenu: {
45
+ label: null,
46
+ items: [
47
+ [
48
+ { label: 'Alerts Hub', url: 'http://markets.ft.com/data/alerts/', submenu: null },
49
+ { label: 'Lexicon', url: 'http://lexicon.ft.com/', submenu: null }
50
+ ],
51
+ [
52
+ { label: 'News feed', url: '/news-feed', submenu: null },
53
+ { label: 'Newsletters', url: '/newsletters', submenu: null }
54
+ ]
55
+ ]
56
+ }
57
+ }
58
+ ]
59
+ }
60
+ }
61
+
62
+ export { menus }
@@ -0,0 +1,83 @@
1
+ import { decorateMenuData as subject } from '../decorateMenuData'
2
+ import { menus } from '../__fixtures__/menus'
3
+ import dlv from 'dlv'
4
+
5
+ describe('dotcom-server-navigation/src/decorateMenuData', () => {
6
+ describe('.decorateMenu()', () => {
7
+ it('returns a new deeply cloned object rather than mutating in place', () => {
8
+ const decorated = subject(menus['navbar-uk'], '/world/uk')
9
+
10
+ expect(decorated).not.toBe(menus['navbar-uk'])
11
+ expect(decorated.items).not.toBe(menus['navbar-uk'].items)
12
+ expect(decorated.items[0]).not.toBe(menus['navbar-uk'].items[0])
13
+ })
14
+
15
+ it('marks menu items whose `url` property matches `currentPath` as `selected`', () => {
16
+ const decorated = subject(menus['navbar-uk'], '/world/uk')
17
+
18
+ const a = dlv(decorated, ['items', 0])
19
+ const b = dlv(decorated, ['items', 1, 'submenu', 'items', 0])
20
+ const c = dlv(decorated, ['items', 1, 'meganav', 0, 'data', 1, 0])
21
+ const d = dlv(decorated, ['items', 1])
22
+
23
+ expect(a.selected).toBe(true)
24
+ expect(b.selected).toBe(true)
25
+ expect(c.selected).toBe(true)
26
+ expect(d.selected).toBe(false)
27
+ })
28
+
29
+ it('replaces the ${currentPath} placeholder with the value of `currentPath`', () => {
30
+ const decorated = subject(menus['navbar-uk'], '/world/us/politics')
31
+
32
+ const a = dlv(decorated, ['items', 1])
33
+ const b = dlv(decorated, ['items', 1, 'submenu', 'items', 1])
34
+ const c = dlv(decorated, ['items', 1, 'meganav', 1, 'data', 1])
35
+
36
+ expect(a.url).toBe('/fake-item?location=/world/us/politics')
37
+ expect(b.url).toBe('/fake-item-nested?location=/world/us/politics')
38
+ expect(c.url).toBe('/content/qux?location=/world/us/politics')
39
+ })
40
+
41
+ it('replaces the ${currentPath} placeholder with %2F in URLs which contain keywords', () => {
42
+ const testKeyword = (itemUrl: string) => {
43
+ const decorated = subject(menus['navbar-uk'], itemUrl)
44
+
45
+ const a = dlv(decorated, ['items', 1, 'url'])
46
+ const b = dlv(decorated, ['items', 1, 'submenu', 'items', 1, 'url'])
47
+
48
+ expect(a).toBe('/fake-item?location=%2F')
49
+ expect(b).toBe('/fake-item-nested?location=%2F')
50
+ }
51
+
52
+ testKeyword('/uk/products/bar')
53
+ testKeyword('/world/barriers')
54
+ testKeyword('/world/us/errors')
55
+ })
56
+
57
+ it('can clone submenu properties', () => {
58
+ const decorated = subject(menus.footer, '/world/uk')
59
+ const submenu = dlv(decorated, ['items', 0, 'submenu', 'items'])
60
+
61
+ submenu.forEach((column) => {
62
+ column.forEach((menuItem) => {
63
+ expect(menuItem).toHaveProperty('label')
64
+ expect(menuItem).toHaveProperty('url')
65
+ expect(menuItem).toHaveProperty('selected')
66
+ })
67
+ })
68
+ })
69
+
70
+ it('can clone meganav properties', () => {
71
+ const decorated = subject(menus['navbar-uk'], '/world/uk')
72
+ const meganav = dlv(decorated, ['items', 1, 'meganav'])
73
+
74
+ expect(meganav[0]).toHaveProperty('component', 'sectionlist')
75
+ expect(meganav[0]).toHaveProperty('title', 'Sections')
76
+ expect(meganav[0]).toHaveProperty('data')
77
+
78
+ expect(meganav[1]).toHaveProperty('component', 'articlelist')
79
+ expect(meganav[1]).toHaveProperty('title', 'Most Read')
80
+ expect(meganav[1]).toHaveProperty('data')
81
+ })
82
+ })
83
+ })
@@ -0,0 +1,25 @@
1
+ import * as subject from '../editions'
2
+
3
+ describe('dotcom-server-navigation/src/editions', () => {
4
+ describe('.isEdition()', () => {
5
+ it('returns true for editions which exist', () => {
6
+ expect(subject.isEdition('uk')).toBe(true)
7
+ })
8
+
9
+ it('returns false for editions which do not exist', () => {
10
+ expect(subject.isEdition('london')).toBe(false)
11
+ })
12
+ })
13
+
14
+ describe('.getEditions()', () => {
15
+ it('returns the current selected edition', () => {
16
+ const result = subject.getEditions('uk')
17
+ expect(result.current).toEqual(expect.objectContaining({ id: 'uk' }))
18
+ })
19
+
20
+ it('returns the all other editions', () => {
21
+ const result = subject.getEditions('uk')
22
+ expect(result.others).toEqual([expect.objectContaining({ id: 'international' })])
23
+ })
24
+ })
25
+ })
@@ -0,0 +1,134 @@
1
+ import nock from 'nock'
2
+ import { Navigation } from '..'
3
+ import menusData from '../../../../__fixtures__/menus.json'
4
+
5
+ const subNavigationData = {
6
+ ancestors: [{ label: 'some-ancestors' }],
7
+ children: [{ label: 'some-children' }],
8
+ item: { label: 'current-page' }
9
+ }
10
+
11
+ const FakePoller = {
12
+ start: jest.fn(),
13
+ getData: jest.fn()
14
+ }
15
+
16
+ jest.mock('ft-poller', () => {
17
+ return jest.fn().mockImplementation(() => FakePoller)
18
+ })
19
+
20
+ const clone = (obj) => JSON.parse(JSON.stringify(obj))
21
+
22
+ describe('dotcom-server-navigation', () => {
23
+ let navigationInstance
24
+
25
+ beforeEach(() => {
26
+ FakePoller.start.mockResolvedValue(null)
27
+ FakePoller.getData.mockResolvedValue(clone(menusData))
28
+
29
+ navigationInstance = new Navigation()
30
+ })
31
+
32
+ afterEach(() => {
33
+ nock.cleanAll()
34
+ jest.clearAllMocks()
35
+ })
36
+
37
+ describe('constructor', () => {
38
+ it('initialises the poller', () => {
39
+ expect(FakePoller.start).toHaveBeenCalled()
40
+ })
41
+ })
42
+
43
+ describe('.getMenusData()', () => {
44
+ let result
45
+
46
+ beforeAll(async () => {
47
+ result = await navigationInstance.getMenusData()
48
+ })
49
+
50
+ it('returns the raw menus data', () => {
51
+ expect(result).toEqual(menusData)
52
+ })
53
+ })
54
+
55
+ describe('.getMenusFor()', () => {
56
+ let result
57
+
58
+ beforeAll(async () => {
59
+ result = await navigationInstance.getMenusFor('/', 'uk')
60
+ })
61
+
62
+ it('returns the shared menu data', () => {
63
+ expect(result).toHaveProperty('account.label', 'Account')
64
+ expect(result).toHaveProperty('footer.label', 'Footer')
65
+ expect(result).toHaveProperty('user.label', 'User')
66
+ })
67
+
68
+ it('returns the edition specific menu data', () => {
69
+ expect(result).toHaveProperty('drawer.label', 'Drawer')
70
+ expect(result).toHaveProperty('navbar.label', 'Navigation')
71
+ })
72
+
73
+ it('returns decorated menu data', () => {
74
+ expect(result).toHaveProperty('navbar.items.0.selected', true)
75
+ expect(result).toHaveProperty('drawer.items.0.submenu.items.0.selected', true)
76
+ })
77
+ })
78
+
79
+ // nock used here because SubNavigation fetches its data directly rather than pulling from Poller
80
+ describe('.getSubNavigationFor()', () => {
81
+ let result
82
+
83
+ beforeAll(async () => {
84
+ nock('http://next-navigation.ft.com')
85
+ .get('/v2/hierarchy/streamPage')
86
+ .reply(200, clone(subNavigationData))
87
+
88
+ result = await navigationInstance.getSubNavigationFor('/streamPage')
89
+ })
90
+
91
+ it('fetches the sub-navigation data', () => {
92
+ expect(result).toHaveProperty('breadcrumb')
93
+ expect(result).toHaveProperty('subsections')
94
+ })
95
+
96
+ it('appends the current page to the list of ancestors', () => {
97
+ expect(result).toHaveProperty('breadcrumb.1.label', 'current-page')
98
+ })
99
+
100
+ it('appends a selected property to the current page', () => {
101
+ expect(result).toHaveProperty('breadcrumb.1.selected', true)
102
+ })
103
+
104
+ describe('when things go wrong', () => {
105
+ it('throws an HTTP error when fetch fails', async () => {
106
+ nock('http://next-navigation.ft.com').get('/v2/hierarchy/streamPage').reply(500)
107
+
108
+ await expect(navigationInstance.getSubNavigationFor('streamPage')).rejects.toMatchObject({
109
+ message: 'Sub-navigation for streamPage could not be found.'
110
+ })
111
+ })
112
+ })
113
+ })
114
+
115
+ describe('.getEditionsFor()', () => {
116
+ let result
117
+
118
+ beforeAll(async () => {
119
+ result = await navigationInstance.getEditionsFor('uk')
120
+ })
121
+
122
+ it('returns the editions data', () => {
123
+ expect(result).toHaveProperty('current')
124
+ expect(result).toHaveProperty('others')
125
+ })
126
+
127
+ describe('with an invalid edition', () => {
128
+ it('throws an error', () => {
129
+ const test = () => navigationInstance.getEditionsFor('london')
130
+ expect(test).toThrow('The provided edition "london" is not a valid edition')
131
+ })
132
+ })
133
+ })
134
+ })
@@ -0,0 +1,38 @@
1
+ import { selectMenuDataForEdition as subject } from '../selectMenuDataForEdition'
2
+ import { TNavMenus } from '@financial-times/dotcom-types-navigation'
3
+
4
+ const fixture = {
5
+ account: {},
6
+ anon: {},
7
+ footer: {},
8
+ 'drawer-international': {},
9
+ 'drawer-uk': {},
10
+ 'navbar-simple': {},
11
+ 'navbar-right': {},
12
+ 'navbar-right-anon': {},
13
+ 'navbar-international': {},
14
+ 'navbar-uk': {},
15
+ user: {}
16
+ } as TNavMenus
17
+
18
+ describe('dotcom-server-navigation/src/selectMenuDataForEdition', () => {
19
+ it('returns a new object', () => {
20
+ const result = subject(fixture, 'uk')
21
+ expect(result).not.toBe(fixture)
22
+ })
23
+
24
+ it('appends shared menus', () => {
25
+ const result = subject(fixture, 'uk')
26
+
27
+ expect(result.account).toBe(fixture.account)
28
+ expect(result.footer).toBe(fixture.footer)
29
+ expect(result.user).toBe(fixture.user)
30
+ })
31
+
32
+ it('appends edition specific menus', () => {
33
+ const result = subject(fixture, 'uk')
34
+
35
+ expect(result.drawer).toBe(fixture['drawer-uk'])
36
+ expect(result.navbar).toBe(fixture['navbar-uk'])
37
+ })
38
+ })