diff --git a/src/common/types/msGraphApi.ts b/src/common/types/msGraphApi.ts new file mode 100644 index 0000000..934034d --- /dev/null +++ b/src/common/types/msGraphApi.ts @@ -0,0 +1,12 @@ +export interface UserProfileDataBase { + userPrincipalName: string; + displayName?: string; + givenName?: string; + surname?: string; + mail?: string; + otherMails?: string[] +} + +export interface UserProfileData extends UserProfileDataBase { + discordUsername?: string; +} diff --git a/src/common/utils.ts b/src/common/utils.ts new file mode 100644 index 0000000..a0f02bd --- /dev/null +++ b/src/common/utils.ts @@ -0,0 +1,11 @@ +export function transformCommaSeperatedName(name: string) { + if (name.includes(",")) { + try { + const split = name.split(",") + return `${split[1].slice(1, split[1].length)} ${split[0]}` + } catch (e) { + return name; + } + } + return name; +} diff --git a/src/ui/Router.tsx b/src/ui/Router.tsx index 1258db8..c9a6e25 100644 --- a/src/ui/Router.tsx +++ b/src/ui/Router.tsx @@ -17,6 +17,29 @@ import { ScanTicketsPage } from './pages/tickets/ScanTickets.page'; import { SelectTicketsPage } from './pages/tickets/SelectEventId.page'; import { ViewTicketsPage } from './pages/tickets/ViewTickets.page'; import { ManageIamPage } from './pages/iam/ManageIam.page'; +import { ManageProfilePage } from './pages/profile/ManageProfile.page'; + +const ProfileRediect: React.FC = () => { + const location = useLocation(); + + // Don't store login-related paths and ALLOW the callback path + const excludedPaths = [ + '/login', + '/logout', + '/force_login', + '/a', + '/auth/callback', // Add this to excluded paths + ]; + + if (excludedPaths.includes(location.pathname)) { + return ; + } + + // Include search params and hash in the return URL if they exist + const returnPath = location.pathname + location.search + location.hash; + const loginUrl = `/profile?returnTo=${encodeURIComponent(returnPath)}&firstTime=true`; + return ; +}; // Component to handle redirects to login with return path const LoginRedirect: React.FC = () => { @@ -56,6 +79,18 @@ const commonRoutes = [ }, ]; +const profileRouter = createBrowserRouter([ + ...commonRoutes, + { + path: '/profile', + element: , + }, + { + path: '*', + element: , + }, +]); + const unauthenticatedRouter = createBrowserRouter([ ...commonRoutes, { @@ -66,7 +101,6 @@ const unauthenticatedRouter = createBrowserRouter([ path: '/login', element: , }, - // Catch-all route that preserves the attempted path { path: '*', element: , @@ -87,6 +121,10 @@ const authenticatedRouter = createBrowserRouter([ path: '/logout', element: , }, + { + path: '/profile', + element: , + }, { path: '/home', element: , @@ -163,7 +201,11 @@ const ErrorBoundary: React.FC = ({ children }) => { export const Router: React.FC = () => { const { isLoggedIn } = useAuth(); - const router = isLoggedIn ? authenticatedRouter : unauthenticatedRouter; + const router = isLoggedIn + ? authenticatedRouter + : isLoggedIn === null + ? profileRouter + : unauthenticatedRouter; return ( diff --git a/src/ui/components/AppShell/index.tsx b/src/ui/components/AppShell/index.tsx index c5a4aba..867184f 100644 --- a/src/ui/components/AppShell/index.tsx +++ b/src/ui/components/AppShell/index.tsx @@ -25,7 +25,7 @@ import { HeaderNavbar } from '../Navbar/index.js'; import { AuthenticatedProfileDropdown } from '../ProfileDropdown/index.js'; import { getCurrentRevision } from '@ui/util/revision.js'; -interface AcmAppShellProps { +export interface AcmAppShellProps { children: ReactNode; active?: string; showLoader?: boolean; @@ -164,7 +164,7 @@ const AcmAppShell: React.FC = ({ padding="md" header={{ height: 60 }} navbar={{ - width: 200, + width: showSidebar ? 200 : 0, breakpoint: 'sm', collapsed: { mobile: !opened }, }} diff --git a/src/ui/components/AuthContext/AuthCallbackHandler.page.tsx b/src/ui/components/AuthContext/AuthCallbackHandler.page.tsx index 5eb6c49..57ca19e 100644 --- a/src/ui/components/AuthContext/AuthCallbackHandler.page.tsx +++ b/src/ui/components/AuthContext/AuthCallbackHandler.page.tsx @@ -34,11 +34,6 @@ export const AuthCallback: React.FC = () => { setTimeout(() => { handleCallback(); }, 100); - - // Cleanup function - return () => { - console.log('Callback component unmounting'); // Debug log 8 - }; }, [instance, navigate]); return ; diff --git a/src/ui/components/AuthContext/index.tsx b/src/ui/components/AuthContext/index.tsx index c805b40..1f0251c 100644 --- a/src/ui/components/AuthContext/index.tsx +++ b/src/ui/components/AuthContext/index.tsx @@ -19,6 +19,8 @@ import { CACHE_KEY_PREFIX } from '../AuthGuard/index.js'; import FullScreenLoader from './LoadingScreen.js'; import { getRunEnvironmentConfig, ValidServices } from '@ui/config.js'; +import { transformCommaSeperatedName } from '@common/utils.js'; +import { useApi } from '@ui/util/api.js'; interface AuthContextDataWrapper { isLoggedIn: boolean; @@ -28,6 +30,7 @@ interface AuthContextDataWrapper { getToken: CallableFunction; logoutCallback: CallableFunction; getApiToken: CallableFunction; + setLoginStatus: CallableFunction; } export type AuthContextData = { @@ -53,7 +56,6 @@ export const clearAuthCache = () => { export const AuthProvider: React.FC = ({ children }) => { const { instance, inProgress, accounts } = useMsal(); - const [userData, setUserData] = useState(null); const [isLoggedIn, setIsLoggedIn] = useState(false); @@ -67,11 +69,9 @@ export const AuthProvider: React.FC = ({ children }) => { if (response) { handleMsalResponse(response); } else if (accounts.length > 0) { - // User is already logged in, set the state - const [lastName, firstName] = accounts[0].name?.split(',') || []; setUserData({ email: accounts[0].username, - name: `${firstName} ${lastName}`, + name: transformCommaSeperatedName(accounts[0].name || ''), }); setIsLoggedIn(true); } @@ -94,10 +94,9 @@ export const AuthProvider: React.FC = ({ children }) => { }) .then((silentResponse) => { if (silentResponse?.account?.name) { - const [lastName, firstName] = silentResponse.account.name.split(','); setUserData({ - email: silentResponse.account.username, - name: `${firstName} ${lastName}`, + email: accounts[0].username, + name: transformCommaSeperatedName(accounts[0].name || ''), }); setIsLoggedIn(true); } @@ -105,18 +104,16 @@ export const AuthProvider: React.FC = ({ children }) => { .catch(console.error); return; } - - // Use response.account instead of accounts[0] - const [lastName, firstName] = response.account.name?.split(',') || []; setUserData({ - email: response.account.username, - name: `${firstName} ${lastName}`, + email: accounts[0].username, + name: transformCommaSeperatedName(accounts[0].name || ''), }); setIsLoggedIn(true); } }, [accounts, instance] ); + const getApiToken = useCallback( async (service: ValidServices) => { if (!userData) { @@ -194,6 +191,9 @@ export const AuthProvider: React.FC = ({ children }) => { }, [instance] ); + const setLoginStatus = useCallback((val: boolean) => { + setIsLoggedIn(val); + }, []); const logout = useCallback(async () => { try { @@ -209,7 +209,16 @@ export const AuthProvider: React.FC = ({ children }) => { }; return ( {inProgress !== InteractionStatus.None ? ( diff --git a/src/ui/components/AuthGuard/index.tsx b/src/ui/components/AuthGuard/index.tsx index 0634013..070aa63 100644 --- a/src/ui/components/AuthGuard/index.tsx +++ b/src/ui/components/AuthGuard/index.tsx @@ -1,7 +1,7 @@ import { Card, Text, Title } from '@mantine/core'; import React, { ReactNode, useEffect, useState } from 'react'; -import { AcmAppShell } from '@ui/components/AppShell'; +import { AcmAppShell, AcmAppShellProps } from '@ui/components/AppShell'; import FullScreenLoader from '@ui/components/AuthContext/LoadingScreen'; import { getRunEnvironmentConfig, ValidService } from '@ui/config'; import { useApi } from '@ui/util/api'; @@ -60,11 +60,13 @@ export const clearAuthCache = () => { } }; -export const AuthGuard: React.FC<{ - resourceDef: ResourceDefinition; - children: ReactNode; - isAppShell?: boolean; -}> = ({ resourceDef, children, isAppShell = true }) => { +export const AuthGuard: React.FC< + { + resourceDef: ResourceDefinition; + children: ReactNode; + isAppShell?: boolean; + } & AcmAppShellProps +> = ({ resourceDef, children, isAppShell = true, ...appShellProps }) => { const { service, validRoles } = resourceDef; const { baseEndpoint, authCheckRoute, friendlyName } = getRunEnvironmentConfig().ServiceConfiguration[service]; @@ -80,6 +82,10 @@ export const AuthGuard: React.FC<{ setIsAuthenticated(true); return; } + if (validRoles.length === 0) { + setIsAuthenticated(true); + return; + } // Check for cached response first const cachedData = getCachedResponse(service, authCheckRoute); @@ -163,12 +169,7 @@ export const AuthGuard: React.FC<{ } if (isAppShell) { - return ( - - {friendlyName} - {children} - - ); + return {children}; } return <>{children}; diff --git a/src/ui/components/ProfileDropdown/index.tsx b/src/ui/components/ProfileDropdown/index.tsx index 1fe8f0e..71760e8 100644 --- a/src/ui/components/ProfileDropdown/index.tsx +++ b/src/ui/components/ProfileDropdown/index.tsx @@ -18,6 +18,7 @@ import { useState } from 'react'; import { AuthContextData, useAuth } from '../AuthContext/index.js'; import classes from '../Navbar/index.module.css'; +import { useNavigate } from 'react-router-dom'; interface ProfileDropdownProps { userData?: AuthContextData; @@ -26,6 +27,7 @@ interface ProfileDropdownProps { const AuthenticatedProfileDropdown: React.FC = ({ userData }) => { const [opened, setOpened] = useState(false); const theme = useMantineTheme(); + const navigate = useNavigate(); const { logout } = useAuth(); if (!userData) { return null; @@ -111,6 +113,16 @@ const AuthenticatedProfileDropdown: React.FC = ({ userData + + + + )), + ...toAdd.map((email) => ( + + + + +
+ + {email.split('@')[0]} + + + {email} + +
+
+
+ + + Queued for addition + + + + + +
+ )), + ]; + return ( - - +
+ Exec Council Group Management - {/* Member List */} - - - Current Members - - - {isLoading && } - {!isLoading && ( - - {members.map((member) => ( - - - - - {member.name} ({member.email}) - - {toRemove.includes(member.email) && ( - - Queued for removal - - )} - - handleRemoveMember(member.email)} - data-testid={`remove-exec-member-${member.email}`} - > - - - - - ))} - {toAdd.map((member) => ( - - - - {member} - - Queued for addition - - - - - ))} - - )} - - + {isLoading ? ( + + ) : ( + + + + Member + Status + Actions + + + {rows} +
+ )} - {/* Add Member */} - - setEmail(e.currentTarget.value)} - placeholder="Enter email" - label="Add Member" - /> - - + setEmail(e.currentTarget.value)} + placeholder="Enter email" + label="Add Member" + mt="md" + /> + - {/* Save Changes Button */} - {/* Confirmation Modal */} setConfirmationModal(false)} title="Confirm Changes" > - +
{toAdd.length > 0 && ( - - +
+ Members to Add: - - {toAdd.map((email) => ( - {email} - ))} - - + {toAdd.map((email) => ( + {email} + ))} +
)} {toRemove.length > 0 && ( - - +
+ Members to Remove: - - {toRemove.map((email) => ( - {email} - ))} - - + {toRemove.map((email) => ( + {email} + ))} +
)} -
+
- +
); }; diff --git a/src/ui/pages/iam/ManageIam.page.tsx b/src/ui/pages/iam/ManageIam.page.tsx index e0e3b6a..5a60d2e 100644 --- a/src/ui/pages/iam/ManageIam.page.tsx +++ b/src/ui/pages/iam/ManageIam.page.tsx @@ -10,6 +10,7 @@ import { GroupMemberGetResponse, GroupModificationPatchRequest, } from '@common/types/iam'; +import { transformCommaSeperatedName } from '@common/utils'; import { getRunEnvironmentConfig } from '@ui/config'; export const ManageIamPage = () => { @@ -37,7 +38,13 @@ export const ManageIamPage = () => { const getExecMembers = async () => { try { const response = await api.get(`/api/v1/iam/groups/${groupId}`); - return response.data as GroupMemberGetResponse; + const responseMapped = response.data + .map((x: any) => ({ + ...x, + name: transformCommaSeperatedName(x.name), + })) + .sort((x: any, y: any) => (x.name > y.name ? 1 : x.name < y.name ? -1 : 0)); + return responseMapped as GroupMemberGetResponse; } catch (error: any) { console.error('Failed to get users:', error); return []; diff --git a/src/ui/pages/profile/ManageProfile.page.tsx b/src/ui/pages/profile/ManageProfile.page.tsx new file mode 100644 index 0000000..b22da76 --- /dev/null +++ b/src/ui/pages/profile/ManageProfile.page.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { Container, Title } from '@mantine/core'; +import { AuthGuard } from '@ui/components/AuthGuard'; +import { useApi } from '@ui/util/api'; +import { UserProfileData, UserProfileDataBase } from '@common/types/msGraphApi'; +import { ManageProfileComponent } from './ManageProfileComponent'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import { useAuth } from '@ui/components/AuthContext'; + +export const ManageProfilePage: React.FC = () => { + const api = useApi('msGraphApi'); + const { setLoginStatus } = useAuth(); + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const returnTo = searchParams.get('returnTo') || undefined; + const firstTime = searchParams.get('firstTime') === 'true' || false; + const getProfile = async () => { + const raw = ( + await api.get( + '/v1.0/me?$select=userPrincipalName,givenName,surname,displayName,otherMails,mail' + ) + ).data as UserProfileDataBase; + const discordUsername = raw.otherMails?.filter((x) => x.endsWith('@discord')); + const enhanced = raw as UserProfileData; + if (discordUsername?.length === 1) { + enhanced.discordUsername = discordUsername[0].replace('@discord', ''); + enhanced.otherMails = enhanced.otherMails?.filter((x) => !x.endsWith('@discord')); + } + return enhanced; + }; + + const setProfile = async (data: UserProfileData) => { + const newOtherEmails = [data.mail || data.userPrincipalName]; + if (data.discordUsername && data.discordUsername !== '') { + newOtherEmails.push(`${data.discordUsername}@discord`); + } + data.otherMails = newOtherEmails; + delete data.discordUsername; + const response = await api.patch('/v1.0/me', data); + if (response.status < 299 && firstTime) { + setLoginStatus(true); + } + if (returnTo) { + return navigate(returnTo); + } + return response.data; + }; + + return ( + + + Edit Profile + + + + ); +}; diff --git a/src/ui/pages/profile/ManageProfileComponent.test.tsx b/src/ui/pages/profile/ManageProfileComponent.test.tsx new file mode 100644 index 0000000..f715f0f --- /dev/null +++ b/src/ui/pages/profile/ManageProfileComponent.test.tsx @@ -0,0 +1,179 @@ +import React from 'react'; +import { render, screen, act } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import { MantineProvider } from '@mantine/core'; +import { notifications } from '@mantine/notifications'; +import userEvent from '@testing-library/user-event'; +import { vi } from 'vitest'; +import { ManageProfileComponent } from './ManageProfileComponent'; + +describe('ManageProfileComponent tests', () => { + const renderComponent = async ( + getProfile: () => Promise, + setProfile: (data: any) => Promise, + firstTime: boolean = false + ) => { + await act(async () => { + render( + + + + + + ); + }); + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders loading overlay when fetching profile', async () => { + const getProfile = vi.fn().mockResolvedValue(new Promise(() => {})); // Never resolves + const setProfile = vi.fn(); + + await renderComponent(getProfile, setProfile); + + expect(screen.getByTestId('profile-loading')).toBeInTheDocument(); + }); + + it('renders profile form after successfully fetching profile', async () => { + const getProfile = vi.fn().mockResolvedValue({ + displayName: 'John Doe', + givenName: 'John', + surname: 'Doe', + mail: 'john.doe@example.com', + discordUsername: 'johndoe#1234', + }); + const setProfile = vi.fn(); + + await renderComponent(getProfile, setProfile); + + expect(screen.getByTestId('edit-displayName')).toHaveValue('John Doe'); + expect(screen.getByTestId('edit-firstName')).toHaveValue('John'); + expect(screen.getByTestId('edit-lastName')).toHaveValue('Doe'); + expect(screen.getByTestId('edit-email')).toHaveValue('john.doe@example.com'); + expect(screen.getByTestId('edit-discordUsername')).toHaveValue('johndoe#1234'); + }); + + it('handles profile fetch failure gracefully', async () => { + const notificationsMock = vi.spyOn(notifications, 'show'); + const getProfile = vi.fn().mockRejectedValue(new Error('Failed to fetch profile')); + const setProfile = vi.fn(); + + await renderComponent(getProfile, setProfile); + + expect(notificationsMock).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Failed to load user profile', + color: 'red', + }) + ); + + notificationsMock.mockRestore(); + }); + + it('allows editing profile fields and saving changes', async () => { + const notificationsMock = vi.spyOn(notifications, 'show'); + const getProfile = vi.fn().mockResolvedValue({ + displayName: 'John Doe', + givenName: 'John', + surname: 'Doe', + mail: 'john.doe@example.com', + discordUsername: '', + }); + const setProfile = vi.fn().mockResolvedValue({}); + + await renderComponent(getProfile, setProfile); + + const user = userEvent.setup(); + + // Edit fields + await user.clear(screen.getByTestId('edit-displayName')); + await user.type(screen.getByTestId('edit-displayName'), 'Jane Doe'); + await user.type(screen.getByTestId('edit-discordUsername'), 'janedoe#5678'); + + // Save changes + const saveButton = screen.getByRole('button', { name: 'Save' }); + await user.click(saveButton); + + expect(setProfile).toHaveBeenCalledWith({ + displayName: 'Jane Doe', + givenName: 'John', + surname: 'Doe', + mail: 'john.doe@example.com', + discordUsername: 'janedoe#5678', + }); + + expect(notificationsMock).toHaveBeenCalledWith( + expect.objectContaining({ + title: 'Profile updated successfully', + message: 'Changes may take some time to reflect.', + color: 'green', + }) + ); + + notificationsMock.mockRestore(); + }); + + it('shows first-time user alert when `firstTime` is true', async () => { + const getProfile = vi.fn().mockResolvedValue({ + displayName: '', + givenName: '', + surname: '', + mail: 'new.user@example.com', + discordUsername: '', + }); + const setProfile = vi.fn(); + + await renderComponent(getProfile, setProfile, true); + + expect( + screen.getByText( + 'Your profile is incomplete. Please provide us with the information below and click Save.' + ) + ).toBeInTheDocument(); + }); + + it('handles profile update failure gracefully', async () => { + const notificationsMock = vi.spyOn(notifications, 'show'); + const getProfile = vi.fn().mockResolvedValue({ + displayName: 'John Doe', + givenName: 'John', + surname: 'Doe', + mail: 'john.doe@example.com', + discordUsername: '', + }); + const setProfile = vi.fn().mockRejectedValue(new Error('Failed to update profile')); + + await renderComponent(getProfile, setProfile); + + const user = userEvent.setup(); + + // Attempt to save without any changes + const saveButton = screen.getByRole('button', { name: 'Save' }); + await user.click(saveButton); + + expect(notificationsMock).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Failed to update profile', + color: 'red', + }) + ); + + notificationsMock.mockRestore(); + }); + + it('disables the save button when no profile data is loaded', async () => { + const getProfile = vi.fn().mockResolvedValue(null); + const setProfile = vi.fn(); + + await renderComponent(getProfile, setProfile); + + expect(screen.getByRole('button', { name: 'Save' })).toBeDisabled(); + }); +}); diff --git a/src/ui/pages/profile/ManageProfileComponent.tsx b/src/ui/pages/profile/ManageProfileComponent.tsx new file mode 100644 index 0000000..9f4423b --- /dev/null +++ b/src/ui/pages/profile/ManageProfileComponent.tsx @@ -0,0 +1,142 @@ +import React, { useEffect, useState } from 'react'; +import { TextInput, Button, Group, Box, LoadingOverlay, Alert } from '@mantine/core'; +import { UserProfileData } from '@common/types/msGraphApi'; +import { notifications } from '@mantine/notifications'; +import { useNavigate } from 'react-router-dom'; +import { IconMoodSmileBeam } from '@tabler/icons-react'; + +interface ManageProfileComponentProps { + getProfile: () => Promise; + setProfile: (data: UserProfileData) => Promise; + firstTime: boolean; +} + +export const ManageProfileComponent: React.FC = ({ + getProfile, + setProfile, + firstTime, +}) => { + const navigate = useNavigate(); + const [userProfile, setUserProfile] = useState(undefined); + const [loading, setLoading] = useState(false); + const fetchProfile = async () => { + setLoading(true); + try { + const profile = await getProfile(); + setUserProfile(profile); + } catch (e) { + console.error(e); + setUserProfile(null); + notifications.show({ + color: 'red', + message: 'Failed to load user profile', + }); + } finally { + setLoading(false); + } + }; + useEffect(() => { + fetchProfile(); + }, [getProfile]); + + const handleSubmit = async () => { + if (!userProfile) return; + setLoading(true); + try { + await setProfile(userProfile); + notifications.show({ + color: 'green', + title: 'Profile updated successfully', + message: 'Changes may take some time to reflect.', + }); + await fetchProfile(); + } catch (e) { + console.error(e); + notifications.show({ + color: 'red', + message: 'Failed to update profile', + }); + } finally { + setLoading(false); + } + }; + + if (userProfile === undefined) { + return ; + } + + return ( + <> + {firstTime && ( + } + title="Welcome to ACM @ UIUC Management Portal" + color="yellow" + > + Your profile is incomplete. Please provide us with the information below and click Save. + + )} + +
{ + e.preventDefault(); + handleSubmit(); + }} + > + + setUserProfile((prev) => prev && { ...prev, displayName: e.target.value }) + } + placeholder={userProfile?.displayName} + required + data-testId="edit-displayName" + /> + + setUserProfile((prev) => prev && { ...prev, givenName: e.target.value }) + } + placeholder={userProfile?.givenName} + required + data-testId="edit-firstName" + /> + setUserProfile((prev) => prev && { ...prev, surname: e.target.value })} + placeholder={userProfile?.surname} + required + data-testId="edit-lastName" + /> + setUserProfile((prev) => prev && { ...prev, mail: e.target.value })} + placeholder={userProfile?.mail} + required + disabled + data-testId="edit-email" + /> + + + setUserProfile((prev) => prev && { ...prev, discordUsername: e.target.value }) + } + data-testId="edit-discordUsername" + /> + + + + + +
+ + ); +}; diff --git a/tests/e2e/events.spec.ts b/tests/e2e/events.spec.ts index 2b6bad0..c1b28ac 100644 --- a/tests/e2e/events.spec.ts +++ b/tests/e2e/events.spec.ts @@ -9,9 +9,7 @@ describe("Events tests", () => { }) => { await becomeUser(page); await page.locator("a").filter({ hasText: "Events" }).click(); - await expect(page.getByRole("heading")).toContainText( - "Core Management Service (NonProd)", - ); + await expect(page.getByRole("heading")).toContainText("Event Management"); await expect( page.getByRole("button", { name: "New Calendar Event" }), ).toBeVisible(); diff --git a/tests/e2e/login.spec.ts b/tests/e2e/login.spec.ts index e0aeab5..b0fa3b2 100644 --- a/tests/e2e/login.spec.ts +++ b/tests/e2e/login.spec.ts @@ -20,14 +20,11 @@ describe("Login tests", () => { page.getByRole("link", { name: "ACM Logo Management Portal" }), ).toBeVisible(); await expect( - page.getByRole("link", { name: "P", exact: true }), + page.getByRole("link", { name: "PC", exact: true }), ).toBeVisible(); - await page.getByRole("link", { name: "P", exact: true }).click(); - await expect(page.getByLabel("PMy Account")).toContainText( - "Name Playwright Core User", - ); - await expect(page.getByLabel("PMy Account")).toContainText( - "Emailcore-e2e-testing@acm.illinois.edu", + await page.getByRole("link", { name: "PC", exact: true }).click(); + await expect(page.getByLabel("PCMy Account")).toContainText( + "NamePlaywright Core UserEmailcore-e2e-testing@acm.illinois.eduEdit ProfileLog Out", ); expect(page.url()).toEqual("https://manage.qa.acmuiuc.org/home"); });