Skip to content

Commit dee3e74

Browse files
authored
CCM-11148: Pass routing feature flag to header (#699)
1 parent 5723f41 commit dee3e74

File tree

14 files changed

+246
-77
lines changed

14 files changed

+246
-77
lines changed

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@
1616
- Then you need to install the following plugins:
1717

1818
```shell
19-
asdf plugin-add nodejs
20-
asdf plugin-add direnv
21-
asdf plugin-add terraform
22-
asdf plugin-add gitleaks
23-
asdf plugin-add pre-commit
19+
asdf plugin add nodejs
20+
asdf plugin add direnv
21+
asdf plugin add terraform
22+
asdf plugin add gitleaks
23+
asdf plugin add pre-commit
2424
```
2525

2626
- Now you can install the tools, and they will be runnable from within the `nhs-notify-web-template-management` directory:

frontend/src/__tests__/components/molecules/Header.test.tsx

Lines changed: 51 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import { render, screen, within } from '@testing-library/react';
22
import { useAuthStatus } from '@hooks/use-auth-status';
33
import { getSessionServer } from '@utils/amplify-utils';
44
import { getIdTokenClaims } from '@utils/token-utils';
5+
import { serverIsFeatureEnabled } from '@utils/server-features';
56
import { NhsNotifyHeader } from '@molecules/Header/Header';
7+
import { useFeatureFlags } from '@providers/features-provider';
68

79
jest.mock('@hooks/use-auth-status');
810
const mockUseAuthStatus = jest.mocked(useAuthStatus);
@@ -13,22 +15,37 @@ const mockGetIdTokenClaims = jest.mocked(getIdTokenClaims);
1315
jest.mock('@utils/amplify-utils');
1416
const mockGetSessionServer = jest.mocked(getSessionServer);
1517

18+
jest.mock('@utils/server-features');
19+
const mockServerIsFeatureEnabled = jest.mocked(serverIsFeatureEnabled);
20+
21+
jest.mock('@providers/features-provider');
22+
const mockUseFeatureFlags = jest.mocked(useFeatureFlags);
23+
1624
jest.mock('nhs-notify-web-template-management-utils/logger');
1725

1826
beforeEach(() => {
1927
jest.resetAllMocks();
2028
mockUseAuthStatus.mockImplementation((status) => status ?? 'configuring');
29+
mockServerIsFeatureEnabled.mockResolvedValue(false); // default for most tests
30+
mockUseFeatureFlags.mockReturnValue({
31+
proofing: false,
32+
routing: false,
33+
});
2134
});
2235

2336
describe('NhsNotifyHeader', () => {
2437
describe('when unauthenticated', () => {
2538
beforeEach(() => {
2639
mockGetSessionServer.mockResolvedValue({});
2740
mockGetIdTokenClaims.mockReturnValue({});
41+
mockUseFeatureFlags.mockReturnValue({
42+
proofing: false,
43+
routing: false,
44+
});
2845
});
2946

3047
it('initializes the authStatus as unauthenticated', async () => {
31-
const header = await NhsNotifyHeader({});
48+
const header = await NhsNotifyHeader();
3249

3350
render(header);
3451

@@ -40,7 +57,7 @@ describe('NhsNotifyHeader', () => {
4057
});
4158

4259
it('renders the logo and service name with the correct url', async () => {
43-
const header = await NhsNotifyHeader({});
60+
const header = await NhsNotifyHeader();
4461

4562
render(header);
4663

@@ -54,15 +71,15 @@ describe('NhsNotifyHeader', () => {
5471
});
5572

5673
it(`renders the authentication link as 'sign in'`, async () => {
57-
const header = await NhsNotifyHeader({});
74+
const header = await NhsNotifyHeader();
5875

5976
render(header);
6077

6178
expect(screen.getByTestId('sign-in-link')).toHaveTextContent('Sign in');
6279
});
6380

6481
it('does not show the navigation links', async () => {
65-
const header = await NhsNotifyHeader({});
82+
const header = await NhsNotifyHeader();
6683

6784
render(header);
6885

@@ -72,7 +89,7 @@ describe('NhsNotifyHeader', () => {
7289
});
7390

7491
it('matches snapshot (unauthenticated)', async () => {
75-
const header = await NhsNotifyHeader({});
92+
const header = await NhsNotifyHeader();
7693

7794
const container = render(header);
7895

@@ -91,14 +108,17 @@ describe('NhsNotifyHeader', () => {
91108
displayName: 'Dr Test Example',
92109
clientName: 'NHS England',
93110
});
111+
112+
mockUseFeatureFlags.mockReturnValue({
113+
proofing: false,
114+
routing: false,
115+
});
94116
});
95117

96118
it('initializes the authStatus as authenticated', async () => {
97-
const header = await NhsNotifyHeader({});
98-
119+
const header = await NhsNotifyHeader();
99120
render(header);
100121

101-
// hook used in AuthLink, HeaderNavigation, HeaderAccountDetails
102122
expect(mockUseAuthStatus).toHaveBeenCalledTimes(3);
103123

104124
for (const call of mockUseAuthStatus.mock.calls) {
@@ -107,7 +127,7 @@ describe('NhsNotifyHeader', () => {
107127
});
108128

109129
it('renders the users display name', async () => {
110-
const header = await NhsNotifyHeader({});
130+
const header = await NhsNotifyHeader();
111131

112132
render(header);
113133

@@ -117,7 +137,7 @@ describe('NhsNotifyHeader', () => {
117137
});
118138

119139
it('renders the client name', async () => {
120-
const header = await NhsNotifyHeader({});
140+
const header = await NhsNotifyHeader();
121141

122142
render(header);
123143

@@ -127,7 +147,7 @@ describe('NhsNotifyHeader', () => {
127147
});
128148

129149
it(`renders auth link as 'Sign out'`, async () => {
130-
const header = await NhsNotifyHeader({});
150+
const header = await NhsNotifyHeader();
131151

132152
render(header);
133153
expect(screen.getByTestId('sign-out-link')).toHaveTextContent('Sign out');
@@ -139,7 +159,7 @@ describe('NhsNotifyHeader', () => {
139159
});
140160
mockGetIdTokenClaims.mockReturnValueOnce({});
141161

142-
const header = await NhsNotifyHeader({});
162+
const header = await NhsNotifyHeader();
143163

144164
render(header);
145165

@@ -154,17 +174,22 @@ describe('NhsNotifyHeader', () => {
154174
});
155175

156176
it('matches snapshot (authenticated)', async () => {
157-
const header = await NhsNotifyHeader({});
177+
const header = await NhsNotifyHeader();
158178

159179
const container = render(header);
160180
expect(container.asFragment()).toMatchSnapshot();
161181
});
162182

163183
describe(`with 'routing' flag enabled`, () => {
164-
it('renders both the navigation links with correct hrefs', async () => {
165-
const header = await NhsNotifyHeader({
166-
features: { routing: true },
184+
beforeEach(() => {
185+
mockUseFeatureFlags.mockReturnValue({
186+
proofing: false,
187+
routing: true,
167188
});
189+
});
190+
191+
it('renders both the navigation links with correct hrefs', async () => {
192+
const header = await NhsNotifyHeader();
168193

169194
render(header);
170195

@@ -177,18 +202,20 @@ describe('NhsNotifyHeader', () => {
177202
const plansLink = within(nav).getByRole('link', {
178203
name: 'Message plans',
179204
});
180-
expect(plansLink).toHaveAttribute(
181-
'href',
182-
'/templates-and-message-plans/message-plans'
183-
);
205+
expect(plansLink).toHaveAttribute('href', '/message-plans');
184206
});
185207
});
186208

187209
describe(`with 'routing' flag disabled`, () => {
188-
it('renders the templates link with correct href', async () => {
189-
const header = await NhsNotifyHeader({
190-
features: { routing: false },
210+
beforeEach(() => {
211+
mockUseFeatureFlags.mockReturnValue({
212+
proofing: false,
213+
routing: false,
191214
});
215+
});
216+
217+
it('renders the templates link with correct href', async () => {
218+
const header = await NhsNotifyHeader();
192219

193220
render(header);
194221

@@ -201,9 +228,7 @@ describe('NhsNotifyHeader', () => {
201228
});
202229

203230
it('should not render the message plans link', async () => {
204-
const header = await NhsNotifyHeader({
205-
features: { routing: false },
206-
});
231+
const header = await NhsNotifyHeader();
207232

208233
render(header);
209234

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { getSessionServer } from '@utils/amplify-utils';
2+
import { fetchClient } from '@utils/server-features';
3+
import { initialFeatureFlags } from '@utils/features';
4+
import FeatureFlagProviderServer from '@providers/features-provider-server';
5+
6+
jest.mock('@utils/amplify-utils');
7+
jest.mock('@utils/server-features');
8+
9+
const mockGetSessionServer = jest.mocked(getSessionServer);
10+
const mockFetchClient = jest.mocked(fetchClient);
11+
12+
describe('FeatureFlagProviderServer', () => {
13+
describe('when unauthenticated', () => {
14+
beforeEach(() => {
15+
mockGetSessionServer.mockResolvedValueOnce({});
16+
});
17+
18+
it('returns initial flags when unauthenticated', async () => {
19+
const child = await FeatureFlagProviderServer({ children: <div /> });
20+
21+
expect(child.props.featureFlags).toEqual(initialFeatureFlags);
22+
});
23+
24+
it('should not fetch client', async () => {
25+
await FeatureFlagProviderServer({ children: <div /> });
26+
27+
expect(mockFetchClient).not.toHaveBeenCalled();
28+
});
29+
});
30+
31+
describe('when authenticated', () => {
32+
beforeEach(() => {
33+
mockGetSessionServer.mockResolvedValueOnce({ accessToken: 'mocktoken' });
34+
});
35+
36+
it('returns flags when authenticated', async () => {
37+
mockFetchClient.mockResolvedValueOnce({
38+
data: { features: { proofing: true, routing: true } },
39+
});
40+
41+
const child = await FeatureFlagProviderServer({ children: <div /> });
42+
43+
expect(child.props.featureFlags).toEqual({
44+
proofing: true,
45+
routing: true,
46+
});
47+
});
48+
49+
it('should fall back to default flags on fetch error', async () => {
50+
mockFetchClient.mockRejectedValueOnce({});
51+
52+
const child = await FeatureFlagProviderServer({ children: <div /> });
53+
54+
expect(child.props.featureFlags).toEqual(initialFeatureFlags);
55+
});
56+
57+
it('should fall back to default flag when feature flag is missing', async () => {
58+
mockFetchClient.mockResolvedValue({
59+
data: {
60+
features: {
61+
proofing: true,
62+
},
63+
},
64+
});
65+
const child = await FeatureFlagProviderServer({ children: <div /> });
66+
67+
expect(child.props.featureFlags).toEqual({
68+
proofing: true,
69+
routing: false,
70+
});
71+
});
72+
});
73+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { render, screen } from '@testing-library/react';
2+
import {
3+
FeatureFlagProvider,
4+
useFeatureFlags,
5+
} from '@providers/features-provider';
6+
7+
const TestComponent = () => {
8+
const featureFlags = useFeatureFlags();
9+
return (
10+
<div>
11+
<p data-testid='proofing'>{featureFlags.proofing ? 'true' : 'false'}</p>
12+
<p data-testid='routing'>{featureFlags.routing ? 'true' : 'false'}</p>
13+
</div>
14+
);
15+
};
16+
17+
it('renders provided feature flags', () => {
18+
render(
19+
<FeatureFlagProvider featureFlags={{ proofing: true, routing: false }}>
20+
<TestComponent />
21+
</FeatureFlagProvider>
22+
);
23+
expect(screen.getByTestId('proofing')).toHaveTextContent('true');
24+
expect(screen.getByTestId('routing')).toHaveTextContent('false');
25+
});

frontend/src/app/layout.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { NhsNotifyHeader } from '@molecules/Header/Header';
99
import { NHSNotifyContainer } from '@layouts/container/container';
1010
import { NHSNotifyFooter } from '@molecules/Footer/Footer';
1111
import { LogoutWarningModal } from '@molecules/LogoutWarningModal/LogoutWarningModal';
12+
import FeatureFlagProviderServer from '@providers/features-provider-server';
1213

1314
// https://nextjs.org/docs/app/api-reference/functions/generate-metadata#metadata-object
1415
export const metadata: Metadata = {
@@ -67,14 +68,16 @@ export default function RootLayout({
6768
<script src={`${getBasePath()}/lib/nhs-frontend-js-check.js`} defer />
6869
<CookiesProvider>
6970
<AuthProvider>
70-
<NHSNotifySkipLink />
71-
<NhsNotifyHeader />
72-
<NHSNotifyContainer>{children}</NHSNotifyContainer>
73-
<NHSNotifyFooter />
74-
<LogoutWarningModal
75-
logoutInSeconds={config.logoutInSeconds}
76-
promptBeforeLogoutSeconds={config.promptTimeSeconds}
77-
/>
71+
<FeatureFlagProviderServer>
72+
<NHSNotifySkipLink />
73+
<NhsNotifyHeader />
74+
<NHSNotifyContainer>{children}</NHSNotifyContainer>
75+
<NHSNotifyFooter />
76+
<LogoutWarningModal
77+
logoutInSeconds={config.logoutInSeconds}
78+
promptBeforeLogoutSeconds={config.promptTimeSeconds}
79+
/>
80+
</FeatureFlagProviderServer>
7881
</AuthProvider>
7982
</CookiesProvider>
8083
</body>

frontend/src/components/molecules/Header/Header.tsx

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,15 @@
11
import React from 'react';
22
import type { AuthStatus } from '@aws-amplify/ui';
33
import Link from 'next/link';
4-
import { ClientFeatures } from 'nhs-notify-backend-client';
54
import content from '@content/content';
65
import { getSessionServer } from '@utils/amplify-utils';
76
import { getIdTokenClaims } from '@utils/token-utils';
87
import { HeaderAccountDetails } from './HeaderAccountDetails';
98
import { HeaderNavigation } from './HeaderNavigation';
109

11-
interface NhsNotifyHeaderProps {
12-
features?: ClientFeatures;
13-
}
14-
1510
const headerContent = content.components.header;
1611

17-
export async function NhsNotifyHeader({
18-
// TODO: CCM-11148 Use real routing feature flag
19-
features,
20-
}: NhsNotifyHeaderProps) {
12+
export async function NhsNotifyHeader() {
2113
const session = await getSessionServer();
2214

2315
const authStatus: AuthStatus = session?.accessToken
@@ -64,10 +56,7 @@ export async function NhsNotifyHeader({
6456
initialAuthStatus={authStatus}
6557
/>
6658
</div>
67-
<HeaderNavigation
68-
initialAuthStatus={authStatus}
69-
routingEnabled={features?.routing}
70-
/>
59+
<HeaderNavigation initialAuthStatus={authStatus} />
7160
</header>
7261
);
7362
}

0 commit comments

Comments
 (0)