Skip to content

Commit 42c801d

Browse files
authored
Add basic playwright tests (#33)
* add basic playwright tests * fix playwright env check for AWS credentials * fix env prioritization * add loading overlay; fix playwright install * fix test stage names
1 parent 4316175 commit 42c801d

File tree

11 files changed

+307
-97
lines changed

11 files changed

+307
-97
lines changed

.github/workflows/deploy-dev.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ jobs:
7070

7171
test-dev:
7272
runs-on: ubuntu-latest
73-
name: Run Live Integration Tests
73+
name: Run Live Tests
7474
needs:
7575
- deploy-dev
7676
concurrency:
@@ -92,3 +92,8 @@ jobs:
9292
run: make dev_health_check
9393
- name: Run live testing
9494
run: make test_live_integration
95+
- name: Run E2E testing
96+
run: make test_e2e
97+
env:
98+
PLAYWRIGHT_USERNAME: ${{ secrets.PLAYWRIGHT_USERNAME }}
99+
PLAYWRIGHT_PASSWORD: ${{ secrets.PLAYWRIGHT_PASSWORD }}

.github/workflows/deploy-prod.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ jobs:
7070

7171
test-dev:
7272
runs-on: ubuntu-latest
73-
name: Run Live Integration Tests
73+
name: Run Live Tests
7474
needs:
7575
- deploy-dev
7676
concurrency:
@@ -90,6 +90,11 @@ jobs:
9090
python-version: 3.11
9191
- name: Run live testing
9292
run: make test_live_integration
93+
- name: Run E2E testing
94+
run: make test_e2e
95+
env:
96+
PLAYWRIGHT_USERNAME: ${{ secrets.PLAYWRIGHT_USERNAME }}
97+
PLAYWRIGHT_PASSWORD: ${{ secrets.PLAYWRIGHT_PASSWORD }}
9398

9499
deploy-prod:
95100
runs-on: ubuntu-latest

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,7 @@ dist_ui/
137137

138138
*.pyc
139139
__pycache__
140+
/test-results/
141+
/playwright-report/
142+
/blob-report/
143+
/playwright/.cache/

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ test_unit: install_test_deps
7474
yarn prettier
7575
yarn test:unit
7676

77+
test_e2e: install_test_deps
78+
yarn playwright install
79+
yarn test:e2e
80+
7781
dev_health_check:
7882
curl -f https://$(application_key).aws.qa.acmuiuc.org/api/v1/healthz && curl -f https://manage.qa.acmuiuc.org
7983

e2e/base.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { test as base } from '@playwright/test';
2+
import {
3+
SecretsManagerClient,
4+
GetSecretValueCommand,
5+
} from "@aws-sdk/client-secrets-manager";
6+
7+
export const getSecretValue = async (
8+
secretId: string,
9+
): Promise<Record<string, string | number | boolean> | null> => {
10+
const smClient = new SecretsManagerClient();
11+
const data = await smClient.send(
12+
new GetSecretValueCommand({ SecretId: secretId }),
13+
);
14+
if (!data.SecretString) {
15+
return null;
16+
}
17+
try {
18+
return JSON.parse(data.SecretString) as Record<
19+
string,
20+
string | number | boolean
21+
>;
22+
} catch {
23+
return null;
24+
}
25+
};
26+
27+
async function getSecrets() {
28+
let response = { PLAYWRIGHT_USERNAME: '', PLAYWRIGHT_PASSWORD: '' }
29+
let keyData;
30+
if (!process.env.PLAYWRIGHT_USERNAME || !process.env.PLAYWRIGHT_PASSWORD) {
31+
keyData = await getSecretValue('infra-core-api-config')
32+
}
33+
response['PLAYWRIGHT_USERNAME'] = process.env.PLAYWRIGHT_USERNAME || (keyData ? keyData['playwright_username'] : '');
34+
response['PLAYWRIGHT_PASSWORD'] = process.env.PLAYWRIGHT_PASSWORD || (keyData ? keyData['playwright_password'] : '');
35+
return response;
36+
}
37+
38+
const secrets = await getSecrets();
39+
40+
async function becomeUser(page) {
41+
await page.goto('https://manage.qa.acmuiuc.org/login');
42+
await page.getByRole('button', { name: 'Sign in with Illinois NetID' }).click();
43+
await page.getByPlaceholder('[email protected]').click();
44+
await page.getByPlaceholder('[email protected]').fill(secrets['PLAYWRIGHT_USERNAME']);
45+
await page.getByPlaceholder('[email protected]').press('Enter');
46+
await page.getByPlaceholder('Password').click();
47+
await page.getByPlaceholder('Password').fill(secrets['PLAYWRIGHT_PASSWORD']);
48+
await page.getByRole('button', { name: 'Sign in' }).click();
49+
await page.getByRole('button', { name: 'No' }).click();
50+
}
51+
52+
export const test = base.extend<{ becomeUser: (page) => Promise<void> }>({
53+
becomeUser: async ({ }, use) => {
54+
use(becomeUser)
55+
},
56+
});

e2e/tests/login.spec.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
2+
import { expect } from '@playwright/test';
3+
import { test } from '../base';
4+
import { describe } from 'node:test';
5+
6+
describe("Login tests", () => {
7+
test('A user can login and view the home screen', async ({ page, becomeUser }) => {
8+
await becomeUser(page);
9+
await expect(page.locator('a').filter({ hasText: 'Management Portal DEV ENV' })).toBeVisible();
10+
await expect(page.locator('a').filter({ hasText: 'Events' })).toBeVisible();
11+
await expect(page.locator('a').filter({ hasText: 'Ticketing/Merch' })).toBeVisible();
12+
await expect(page.locator('a').filter({ hasText: 'IAM' })).toBeVisible();
13+
await expect(page.getByRole('link', { name: 'ACM Logo Management Portal' })).toBeVisible();
14+
await expect(page.getByRole('link', { name: 'P', exact: true })).toBeVisible();
15+
await page.getByRole('link', { name: 'P', exact: true }).click();
16+
await expect(page.getByLabel('PMy Account')).toContainText('Name Playwright Core User');
17+
await expect(page.getByLabel('PMy Account')).toContainText('[email protected]');
18+
expect(page.url()).toEqual('https://manage.qa.acmuiuc.org/home');
19+
});
20+
})

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
"test:unit-ui": "yarn test:unit --ui",
2222
"test:unit-watch": "vitest tests/unit",
2323
"test:live": "vitest tests/live",
24-
"test:live-ui": "yarn test:live --ui"
24+
"test:live-ui": "yarn test:live --ui",
25+
"test:e2e": "playwright test",
26+
"test:e2e-ui": "playwright test --ui"
2527
},
2628
"dependencies": {
2729
"@aws-sdk/client-dynamodb": "^3.624.0",
@@ -49,6 +51,7 @@
4951
},
5052
"devDependencies": {
5153
"@eslint/compat": "^1.1.1",
54+
"@playwright/test": "^1.49.1",
5255
"@tsconfig/node20": "^20.1.4",
5356
"@types/node": "^22.1.0",
5457
"@types/pluralize": "^0.0.33",

playwright.config.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { defineConfig, devices } from '@playwright/test';
2+
3+
export default defineConfig({
4+
testDir: './e2e/tests',
5+
/* Run tests in files in parallel */
6+
fullyParallel: false,
7+
/* Fail the build on CI if you accidentally left test.only in the source code. */
8+
forbidOnly: !!process.env.CI,
9+
/* Retry on CI only */
10+
retries: process.env.CI ? 2 : 0,
11+
/* Opt out of parallel tests on CI. */
12+
workers: process.env.CI ? 1 : undefined,
13+
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
14+
reporter: 'html',
15+
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
16+
use: {
17+
/* Base URL to use in actions like `await page.goto('/')`. */
18+
// baseURL: 'http://127.0.0.1:3000',
19+
20+
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
21+
trace: 'on-first-retry',
22+
},
23+
24+
/* Configure projects for major browsers */
25+
projects: [
26+
{
27+
name: 'chromium',
28+
use: { ...devices['Desktop Chrome'] },
29+
},
30+
31+
{
32+
name: 'firefox',
33+
use: { ...devices['Desktop Firefox'] },
34+
},
35+
],
36+
37+
/* Run your local dev server before starting the tests */
38+
// webServer: {
39+
// command: 'npm run start',
40+
// url: 'http://127.0.0.1:3000',
41+
// reuseExistingServer: !process.env.CI,
42+
// },
43+
});

src/common/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ const environmentConfig: EnvironmentConfigType = {
7070
},
7171
UserRoleMapping: {
7272
"[email protected]": [AppRoles.TICKETS_SCANNER],
73+
"kLkvWTYwNnJfBkIK7mBi4niXXHYNR7ygbV8utlvFxjw": allAppRoles
7374
},
7475
AzureRoleMapping: { AutonomousWriters: [AppRoles.EVENTS_MANAGER] },
7576
ValidCorsOrigins: [

src/ui/pages/iam/GroupMemberManagement.tsx

Lines changed: 43 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
import { IconTrash, IconUserPlus } from '@tabler/icons-react';
1616
import { notifications } from '@mantine/notifications';
1717
import { GroupMemberGetResponse, EntraActionResponse } from '@common/types/iam';
18+
import FullScreenLoader from '@ui/components/AuthContext/LoadingScreen';
1819

1920
interface GroupMemberManagementProps {
2021
fetchMembers: () => Promise<GroupMemberGetResponse>;
@@ -29,7 +30,7 @@ export const GroupMemberManagement: React.FC<GroupMemberManagementProps> = ({
2930
const [toAdd, setToAdd] = useState<string[]>([]);
3031
const [toRemove, setToRemove] = useState<string[]>([]);
3132
const [email, setEmail] = useState('');
32-
const [isLoading, setIsLoading] = useState(false);
33+
const [isLoading, setIsLoading] = useState(true);
3334
const [confirmationModal, setConfirmationModal] = useState(false);
3435

3536
useEffect(() => {
@@ -46,6 +47,7 @@ export const GroupMemberManagement: React.FC<GroupMemberManagementProps> = ({
4647
}
4748
};
4849
loadMembers();
50+
setIsLoading(false);
4951
}, [fetchMembers]);
5052

5153
const handleAddMember = () => {
@@ -132,7 +134,6 @@ export const GroupMemberManagement: React.FC<GroupMemberManagementProps> = ({
132134
setIsLoading(false);
133135
}
134136
};
135-
136137
return (
137138
<Box p="md">
138139
<Text fw={500} mb={4}>
@@ -145,44 +146,47 @@ export const GroupMemberManagement: React.FC<GroupMemberManagementProps> = ({
145146
Current Members
146147
</Text>
147148
<ScrollArea style={{ height: 250 }}>
148-
<List spacing="sm">
149-
{members.map((member) => (
150-
<ListItem key={member.email}>
151-
<Group justify="space-between">
152-
<Box>
153-
<Text size="sm">
154-
{member.name} ({member.email})
155-
</Text>
156-
{toRemove.includes(member.email) && (
157-
<Badge color="red" size="sm">
158-
Queued for removal
149+
{isLoading && <FullScreenLoader />}
150+
{!isLoading && (
151+
<List spacing="sm">
152+
{members.map((member) => (
153+
<ListItem key={member.email}>
154+
<Group justify="space-between">
155+
<Box>
156+
<Text size="sm">
157+
{member.name} ({member.email})
158+
</Text>
159+
{toRemove.includes(member.email) && (
160+
<Badge color="red" size="sm">
161+
Queued for removal
162+
</Badge>
163+
)}
164+
</Box>
165+
<ActionIcon
166+
color="red"
167+
variant="light"
168+
onClick={() => handleRemoveMember(member.email)}
169+
data-testid={`remove-exec-member-${member.email}`}
170+
>
171+
<IconTrash size={16} />
172+
</ActionIcon>
173+
</Group>
174+
</ListItem>
175+
))}
176+
{toAdd.map((member) => (
177+
<ListItem key={member}>
178+
<Group justify="space-between">
179+
<Box>
180+
<Text size="sm">{member}</Text>
181+
<Badge color="green" size="sm">
182+
Queued for addition
159183
</Badge>
160-
)}
161-
</Box>
162-
<ActionIcon
163-
color="red"
164-
variant="light"
165-
onClick={() => handleRemoveMember(member.email)}
166-
data-testid={`remove-exec-member-${member.email}`}
167-
>
168-
<IconTrash size={16} />
169-
</ActionIcon>
170-
</Group>
171-
</ListItem>
172-
))}
173-
{toAdd.map((member) => (
174-
<ListItem key={member}>
175-
<Group justify="space-between">
176-
<Box>
177-
<Text size="sm">{member}</Text>
178-
<Badge color="green" size="sm">
179-
Queued for addition
180-
</Badge>
181-
</Box>
182-
</Group>
183-
</ListItem>
184-
))}
185-
</List>
184+
</Box>
185+
</Group>
186+
</ListItem>
187+
))}
188+
</List>
189+
)}
186190
</ScrollArea>
187191
</Box>
188192

0 commit comments

Comments
 (0)