Skip to content

Commit 64a1dc3

Browse files
committed
CCM-8308: implement automated tests for modal
1 parent 658e3d2 commit 64a1dc3

File tree

10 files changed

+157
-52
lines changed

10 files changed

+157
-52
lines changed

frontend/src/__tests__/components/molecules/LogoutWarningModal/__snapshots__/LogoutWarningModal.test.tsx.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@ exports[`LogoutWarningModal should match snapshot 1`] = `
4343
<a
4444
class="nhsuk-link"
4545
data-testid="modal-sign-out"
46-
href="/auth/signout?redirect=%2Ftemplates%2Fcreate-and-submit-templates"
46+
href="/auth/signout"
4747
>
48-
Log out
48+
Sign out
4949
</a>
5050
</div>
5151
</div>

frontend/src/app/auth/inactive/page.dev.tsx

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
'use client';
22

3-
// this is an npm module, not node:path
4-
// eslint-disable-next-line unicorn/prefer-node-protocol
5-
import path from 'path';
6-
import React from 'react';
3+
import React, { Suspense } from 'react';
74
import { Button } from 'nhsuk-react-components';
85
import { useSearchParams } from 'next/navigation';
96
import { getBasePath } from '@utils/get-base-path';
@@ -13,15 +10,19 @@ import { SignOut } from '../signout/page.dev';
1310
const timeTillLogout =
1411
Number(process.env.NEXT_PUBLIC_TIME_TILL_LOGOUT_SECONDS) || 900;
1512

16-
export default function Inactive() {
17-
const searchParams = useSearchParams();
13+
const SignInButton = () => {
14+
const searchParams = useSearchParams().toString();
1815

19-
const requestRedirectPath =
20-
searchParams.get('redirect') ||
21-
`${getBasePath()}/create-and-submit-templates`;
16+
const signLinLink = getBasePath() + (searchParams ? `?${searchParams}` : '');
2217

23-
const redirectPath = path.normalize(requestRedirectPath);
18+
return (
19+
<Suspense fallback={<p>Loading...</p>}>
20+
<Button href={signLinLink}>Sign in</Button>
21+
</Suspense>
22+
);
23+
};
2424

25+
export default function Inactive() {
2526
return (
2627
<SignOut>
2728
<div className='nhsuk-grid-row'>
@@ -33,7 +34,7 @@ export default function Inactive() {
3334
</p>
3435
<p>Any unsaved changes have been lost.</p>
3536
<p>Sign in again to create and submit a template to NHS Notify.</p>
36-
<Button href={`/auth?redirect=${redirectPath}`}>Sign in</Button>
37+
<SignInButton />
3738
</div>
3839
</div>
3940
</SignOut>

frontend/src/app/layout.tsx

Lines changed: 12 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,12 @@ import '@styles/app.scss';
33
import { ClientLayout } from '@layouts/client/client-layout';
44
import content from '@content/content';
55
import { getBasePath } from '@utils/get-base-path';
6-
import { LogoutWarningModal } from '@molecules/LogoutWarningModal/LogoutWarningModal';
76

87
export const metadata: Metadata = {
98
title: content.global.mainLayout.title,
109
description: content.global.mainLayout.description,
1110
};
1211

13-
const config = {
14-
basePath: getBasePath(),
15-
logoutInSeconds:
16-
Number(process.env.NEXT_PUBLIC_TIME_TILL_LOGOUT_SECONDS) || 900, // 15 minutes force logout
17-
promptTimeSeconds:
18-
Number(process.env.NEXT_PUBLIC_PROMPT_SECONDS_BEFORE_LOGOUT) || 120, // 2 minutes before logout
19-
};
20-
2112
export default function RootLayout({
2213
children,
2314
}: {
@@ -26,62 +17,56 @@ export default function RootLayout({
2617
return (
2718
<html lang='en'>
2819
<head>
29-
<script src={`${config.basePath}/lib/nhsuk-9.1.0.min.js`} defer />
20+
<script src={`${getBasePath()}/lib/nhsuk-9.1.0.min.js`} defer />
3021
<title>{content.global.mainLayout.title}</title>
3122
<link
3223
rel='shortcut icon'
33-
href={`${config.basePath}/lib/assets/favicons/favicon.ico`}
24+
href={`${getBasePath()}/lib/assets/favicons/favicon.ico`}
3425
type='image/x-icon'
3526
/>
3627
<link
3728
rel='apple-touch-icon'
38-
href={`${config.basePath}/lib/assets/favicons/apple-touch-icon-180x180.png`}
29+
href={`${getBasePath()}/lib/assets/favicons/apple-touch-icon-180x180.png`}
3930
/>
4031
<link
4132
rel='mask-icon'
42-
href={`${config.basePath}/lib/assets/favicons/favicon.svg`}
33+
href={`${getBasePath()}/lib/assets/favicons/favicon.svg`}
4334
color='#005eb8'
4435
/>
4536
<link
4637
rel='icon'
4738
sizes='192x192'
48-
href={`${config.basePath}/lib/assets/favicons/favicon-192x192.png`}
39+
href={`${getBasePath()}/lib/assets/favicons/favicon-192x192.png`}
4940
/>
5041
<meta
5142
name='msapplication-TileImage'
52-
content={`${config.basePath}/lib/assets/favicons/mediumtile-144x144.png`}
43+
content={`${getBasePath()}/lib/assets/favicons/mediumtile-144x144.png`}
5344
/>
5445
<meta name='msapplication-TileColor' content='#005eb8' />
5546
<meta
5647
name='msapplication-square70x70logo'
57-
content={`${config.basePath}/lib/assets/favicons/smalltile-70x70.png`}
48+
content={`${getBasePath()}/lib/assets/favicons/smalltile-70x70.png`}
5849
/>
5950
<meta
6051
name='msapplication-square150x150logo'
61-
content={`${config.basePath}/lib/assets/favicons/mediumtile-150x150.png`}
52+
content={`${getBasePath()}/lib/assets/favicons/mediumtile-150x150.png`}
6253
/>
6354
<meta
6455
name='msapplication-wide310x150logo'
65-
content={`${config.basePath}/lib/assets/favicons/widetile-310x150.png`}
56+
content={`${getBasePath()}/lib/assets/favicons/widetile-310x150.png`}
6657
/>
6758
<meta
6859
name='msapplication-square310x310logo'
69-
content={`${config.basePath}/lib/assets/favicons/largetile-310x310.png`}
60+
content={`${getBasePath()}/lib/assets/favicons/largetile-310x310.png`}
7061
/>
7162
<script
7263
type='text/javascript'
73-
src={`${config.basePath}/lib/nhs-frontend-js-check.js`}
64+
src={`${getBasePath()}/lib/nhs-frontend-js-check.js`}
7465
defer
7566
/>
7667
</head>
7768
<body suppressHydrationWarning>
78-
<ClientLayout>
79-
<LogoutWarningModal
80-
logoutInSeconds={config.logoutInSeconds}
81-
promptBeforeLogoutSeconds={config.promptTimeSeconds}
82-
/>
83-
{children}
84-
</ClientLayout>
69+
<ClientLayout>{children}</ClientLayout>
8570
</body>
8671
</html>
8772
);

frontend/src/components/layouts/client/client-layout.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,30 @@ import { NHSNotifyHeader } from '@molecules/Header/Header';
66
import { NHSNotifyContainer } from '@layouts/container/container';
77
import { NHSNotifyFooter } from '@molecules/Footer/Footer';
88
import { NHSNotifySkipLink } from '@atoms/NHSNotifySkipLink/NHSNotifySkipLink';
9+
import { LogoutWarningModal } from '@molecules/LogoutWarningModal/LogoutWarningModal';
910
// eslint-disable-next-line import/no-unresolved
1011
import amplifyConfig from '@/amplify_outputs.json';
1112

1213
Amplify.configure(amplifyConfig, { ssr: true });
1314

15+
const config = {
16+
logoutInSeconds:
17+
Number(process.env.NEXT_PUBLIC_TIME_TILL_LOGOUT_SECONDS) || 900, // 15 minutes force logout
18+
promptTimeSeconds:
19+
Number(process.env.NEXT_PUBLIC_PROMPT_SECONDS_BEFORE_LOGOUT) || 120, // 2 minutes before logout
20+
};
21+
1422
export function ClientLayout({ children }: { children: React.ReactNode }) {
1523
return (
1624
<Authenticator.Provider>
1725
<NHSNotifySkipLink />
1826
<NHSNotifyHeader />
1927
<NHSNotifyContainer>{children}</NHSNotifyContainer>
2028
<NHSNotifyFooter />
29+
<LogoutWarningModal
30+
logoutInSeconds={config.logoutInSeconds}
31+
promptBeforeLogoutSeconds={config.promptTimeSeconds}
32+
/>
2133
</Authenticator.Provider>
2234
);
2335
}

frontend/src/components/molecules/LogoutWarningModal/LogoutWarningModal.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export const LogoutWarningModal = ({
2222
}) => {
2323
const {
2424
headerComponent: {
25-
links: { logOut: logOut },
25+
links: { signOut },
2626
},
2727
logoutWarningComponent,
2828
} = content.components;
@@ -41,7 +41,7 @@ export const LogoutWarningModal = ({
4141
router.push(
4242
`/auth/inactive?redirect=${encodeURIComponent(
4343
`${getBasePath()}/${pathname}`
44-
)}`,
44+
)}`
4545
);
4646
};
4747

@@ -99,10 +99,10 @@ export const LogoutWarningModal = ({
9999
<div className={styles.signOut}>
100100
<a
101101
className='nhsuk-link'
102-
href={logOut.href}
102+
href={signOut.href}
103103
data-testid='modal-sign-out'
104104
>
105-
{logOut.text}
105+
{signOut.text}
106106
</a>
107107
</div>
108108
</Modal.Footer>

frontend/src/components/molecules/Modal/Modal.module.scss

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
.modal {
2-
@media (max-width: 40.0525em) {
3-
max-width: 350px;
4-
}
5-
6-
background-color: white;
7-
top: 10rem;
2+
margin: 10rem auto;
83
max-width: 400px;
94
border: 0;
105
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);

tests/test-team/config/component/component.config.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@ import path from 'node:path';
22
import { defineConfig, devices } from '@playwright/test';
33
import baseConfig from '../playwright.config';
44

5+
const buildCommand = [
6+
'INCLUDE_AUTH_PAGES=true',
7+
'NEXT_PUBLIC_TIME_TILL_LOGOUT_SECONDS=25',
8+
'NEXT_PUBLIC_PROMPT_SECONDS_BEFORE_LOGOUT=5',
9+
'npm run build && npm run start',
10+
].join(' ');
11+
512
export default defineConfig({
613
...baseConfig,
714

@@ -33,6 +40,20 @@ export default defineConfig({
3340
dependencies: ['component:setup'],
3441
teardown: 'component:teardown',
3542
},
43+
{
44+
name: 'modal',
45+
testMatch: 'template-mgmt-logout-warning.component.modal.spec.ts',
46+
use: {
47+
screenshot: 'only-on-failure',
48+
baseURL: 'http://localhost:3000',
49+
...devices['Desktop Chrome'],
50+
headless: true,
51+
storageState: path.resolve(__dirname, '../.auth/user.json'),
52+
},
53+
dependencies: ['component:setup'],
54+
teardown: 'component:teardown',
55+
fullyParallel: true, // make these sets of tests parallel due to their slow nature.
56+
},
3657
{
3758
name: 'component:teardown',
3859
testMatch: 'component.teardown.ts',
@@ -41,7 +62,7 @@ export default defineConfig({
4162
/* Run your local dev server before starting the tests */
4263
webServer: {
4364
timeout: 2 * 60 * 1000, // 2 minutes
44-
command: 'INCLUDE_AUTH_PAGES=true npm run build && npm run start',
65+
command: buildCommand,
4566
cwd: path.resolve(__dirname, '../../../..'),
4667
url: 'http://localhost:3000/templates/create-and-submit-templates',
4768
reuseExistingServer: !process.env.CI,

tests/test-team/helpers/auth/cognito-auth-helper.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,26 @@ export enum TestUserId {
2323
* User1 is generally the signed in user
2424
*/
2525
User1 = 'User1',
26+
2627
/**
2728
* User2 provides an alternative user allowing to check for things like template ownership
2829
*/
2930
User2 = 'User2',
31+
32+
/**
33+
* User3 idle user that stays stayed in
34+
*/
35+
User3 = 'User3',
36+
37+
/**
38+
* User4 idle user which signs out automatically
39+
*/
40+
User4 = 'User4',
41+
42+
/**
43+
* User5 idle user which signs out manually
44+
*/
45+
User5 = 'User5',
3046
}
3147

3248
export type TestUser = {

tests/test-team/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"version": "0.0.1",
44
"private": true,
55
"scripts": {
6-
"test:local-ui": "playwright test --project component -c config/component/component.config.ts",
6+
"test:local-ui": "playwright test -c config/component/component.config.ts",
77
"test:local-ui-e2e": "playwright test --project e2e-local -c config/e2e/e2e.config.ts",
88
"test:api": "playwright test --project api -c config/api/api.config.ts",
99
"lint": "eslint .",
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { test, expect } from '@playwright/test';
2+
import {
3+
createAuthHelper,
4+
TestUser,
5+
TestUserId,
6+
} from '../helpers/auth/cognito-auth-helper';
7+
import { TemplateMgmtSignInPage } from '../pages/templates-mgmt-login-page';
8+
9+
test.use({ storageState: { cookies: [], origins: [] } });
10+
11+
test.describe('Logout warning', () => {
12+
let staySignedInUser: TestUser;
13+
let manualSignOutUser: TestUser;
14+
let automaticallySignedOutUser: TestUser;
15+
16+
test.beforeAll(async () => {
17+
const authHelper = createAuthHelper();
18+
19+
staySignedInUser = await authHelper.getTestUser(TestUserId.User3);
20+
manualSignOutUser = await authHelper.getTestUser(TestUserId.User4);
21+
automaticallySignedOutUser = await authHelper.getTestUser(TestUserId.User5);
22+
});
23+
24+
test('logout warning should pop up and close after clicking "Stay signed in"', async ({
25+
page,
26+
}) => {
27+
const loginPage = new TemplateMgmtSignInPage(page);
28+
29+
await loginPage.loadPage();
30+
31+
await loginPage.cognitoSignIn(staySignedInUser);
32+
33+
const dialog = page.locator('dialog');
34+
35+
await dialog.waitFor({ state: 'visible', timeout: 25_000 });
36+
37+
await dialog.getByRole('button', { name: 'Stay signed in' }).click();
38+
39+
expect(dialog).not.toBeVisible();
40+
});
41+
42+
test('logout warning should pop up and close after user clicks "Sign out"', async ({
43+
page,
44+
}) => {
45+
const loginPage = new TemplateMgmtSignInPage(page);
46+
47+
await loginPage.loadPage();
48+
49+
await loginPage.cognitoSignIn(manualSignOutUser);
50+
51+
const dialog = page.locator('dialog');
52+
53+
await dialog.waitFor({ state: 'visible', timeout: 25_000 });
54+
55+
await dialog.getByRole('link', { name: 'Sign out' }).click();
56+
57+
await expect(dialog).not.toBeVisible();
58+
});
59+
60+
test('logout warning should force logout after timeout', async ({ page }) => {
61+
const loginPage = new TemplateMgmtSignInPage(page);
62+
63+
await loginPage.loadPage();
64+
65+
await loginPage.cognitoSignIn(automaticallySignedOutUser);
66+
67+
const dialog = page.locator('dialog');
68+
69+
await dialog.waitFor({ state: 'visible', timeout: 25_000 });
70+
71+
await page
72+
.getByRole('heading', { level: 1, name: "You've been signed out" })
73+
.waitFor({ state: 'visible', timeout: 10_000 });
74+
});
75+
});

0 commit comments

Comments
 (0)