diff --git a/ui/public/cogs.svg b/ui/public/cogs.svg
old mode 100755
new mode 100644
diff --git a/ui/public/id-card-solid.svg b/ui/public/id-card-solid.svg
new file mode 100644
index 00000000..8abeb2c8
--- /dev/null
+++ b/ui/public/id-card-solid.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/public/id-email.svg b/ui/public/id-email.svg
old mode 100755
new mode 100644
diff --git a/ui/public/key-solid.svg b/ui/public/key-solid.svg
new file mode 100644
index 00000000..7555f7ed
--- /dev/null
+++ b/ui/public/key-solid.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/public/uh-groupings-text.svg b/ui/public/uh-groupings-text.svg
new file mode 100644
index 00000000..f323aed7
--- /dev/null
+++ b/ui/public/uh-groupings-text.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/ui/public/user-solid.svg b/ui/public/user-solid.svg
new file mode 100644
index 00000000..30cbd7d3
--- /dev/null
+++ b/ui/public/user-solid.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/public/watch.svg b/ui/public/watch.svg
old mode 100755
new mode 100644
diff --git a/ui/public/wrench-solid.svg b/ui/public/wrench-solid.svg
new file mode 100644
index 00000000..280b3bd0
--- /dev/null
+++ b/ui/public/wrench-solid.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/app/(index)/_components/AfterLogin.tsx b/ui/src/app/(index)/_components/AfterLogin.tsx
new file mode 100644
index 00000000..f2851a75
--- /dev/null
+++ b/ui/src/app/(index)/_components/AfterLogin.tsx
@@ -0,0 +1,126 @@
+import Role from '@/access/Role';
+import Image from 'next/image';
+import { KeyRound } from 'lucide-react';
+import Link from 'next/link';
+import { Button } from '@/components/ui/button';
+import { getNumberOfGroupings, getNumberOfMemberships } from '@/services/GroupingsApiService';
+import { getCurrentUser } from '@/access/AuthenticationService';
+
+const AfterLogin = async ()=>{
+ const [currentUser, numberOfGroupings, numberOfMemberships] = await Promise.all([
+ getCurrentUser(),
+ getNumberOfGroupings(),
+ getNumberOfMemberships()
+ ]);
+
+ const getHighestRole = () => {
+ if (currentUser.roles.includes(Role.ADMIN)) return 'Admin';
+ else if (currentUser.roles.includes(Role.OWNER)) return 'Owner';
+ else return 'Member';
+ };
+
+ const pageInfoItems = [
+ {
+ title: 'Admin',
+ description: 'Manage the list of Administrators for this service.' +
+ ' Search for and manage any grouping on behalf of the owner.',
+ href: '/admin',
+ icon: {
+ alt: 'key-solid',
+ src: 'uhgroupings/key-solid.svg',
+ width: 48,
+ height: 48,
+ },
+ role: Role.ADMIN
+ },
+ {
+ title: 'Memberships',
+ description: 'View and manage my memberships. Search for new groupings to join as a member.',
+ href: '/memberships',
+ icon: {
+ src: 'uhgroupings/id-card-solid.svg',
+ alt: 'id-card',
+ width: 54,
+ height: 48,
+ },
+ number: numberOfMemberships,
+ role: Role.UH
+ },
+ {
+ title: 'Groupings',
+ description: 'Review members, manage Include and Exclude lists, ' +
+ 'configure preferences, and export members.',
+ href: '/groupings',
+ icon: {
+ alt: 'wrench-solid',
+ src: 'uhgroupings/wrench-solid.svg',
+ width: 48,
+ height: 48
+ },
+ number: numberOfGroupings,
+ role: Role.OWNER
+ }
+ ];
+
+ return (
+
+
+
+
+
+
+
Welcome, {currentUser.firstName} !
+ Role: {getHighestRole()}
+
+
+
+
+
+
+ {pageInfoItems
+ .filter((pageInfoItem) => currentUser.roles.includes(pageInfoItem.role))
+ .map((pageInfoItem, index) => (
+
+
+
+
+ {pageInfoItem.number
+ && {pageInfoItem.number} }
+
+
{pageInfoItem.title}
+
{pageInfoItem.description}
+
+
+
{pageInfoItem.title}
+
+
+ ))}
+
+
+ );
+};
+
+export default AfterLogin;
diff --git a/ui/src/app/(index)/_components/Announcements.tsx b/ui/src/app/(index)/_components/Announcements.tsx
new file mode 100644
index 00000000..ee796ea8
--- /dev/null
+++ b/ui/src/app/(index)/_components/Announcements.tsx
@@ -0,0 +1,29 @@
+import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
+import { AlertCircle } from 'lucide-react';
+import { getAnnouncements } from '@/services/GroupingsApiService';
+
+const Announcements = async () => {
+ const announcements = await getAnnouncements();
+ const activeAnnouncements = () => {
+ if (!announcements || !announcements.announcements) {
+ return []
+ }
+ return announcements.announcements.filter((announcement) => announcement.state === 'Active')
+ .map((announcement) => announcement.message)
+ };
+
+ return (
+
+ {activeAnnouncements().map((announcement:string, index:number) => (
+
+
+ Announcement
+
+ {announcement}
+
+ ))}
+
+ );
+};
+
+export default Announcements;
diff --git a/ui/src/app/(index)/_components/BeforeLogin.tsx b/ui/src/app/(index)/_components/BeforeLogin.tsx
new file mode 100644
index 00000000..2a1c0ef1
--- /dev/null
+++ b/ui/src/app/(index)/_components/BeforeLogin.tsx
@@ -0,0 +1,20 @@
+import { Button } from '@/components/ui/button';
+import { ArrowRight } from 'lucide-react';
+import UHGroupingsInfo from '@/components/UHGroupingsInfo';
+
+const BeforeLogin = () => (
+
+
+
+
+);
+
+export default BeforeLogin;
diff --git a/ui/src/app/(index)/_components/LoginButton.tsx b/ui/src/app/(index)/_components/LoginButton.tsx
new file mode 100644
index 00000000..544f01c9
--- /dev/null
+++ b/ui/src/app/(index)/_components/LoginButton.tsx
@@ -0,0 +1,31 @@
+'use client';
+import { Button } from '@/components/ui/button';
+import Role from '@/access/Role';
+import User from '@/access/User';
+import { login, logout } from '@/access/AuthenticationService';
+
+const LoginButton = ({
+ currentUser
+}: {
+ currentUser: User;
+}) => (
+ <>
+ {!currentUser.roles.includes(Role.UH) ? (
+ login()}>
+ Login Here
+
+ ) : (
+ logout()}>
+ Logout
+
+ )}
+ >
+);
+
+export default LoginButton;
diff --git a/ui/src/app/(index)/page.tsx b/ui/src/app/(index)/page.tsx
index b0223723..ff6bde38 100644
--- a/ui/src/app/(index)/page.tsx
+++ b/ui/src/app/(index)/page.tsx
@@ -1,6 +1,54 @@
-const Home = () => {
+import Image from 'next/image';
+import BeforeLogin from '@/app/(index)/_components/BeforeLogin';
+import AfterLogin from '@/app/(index)/_components/AfterLogin';
+import { getCurrentUser } from '@/access/AuthenticationService';
+import Role from '@/access/Role';
+import LoginButton from '@/app/(index)/_components/LoginButton';
+import Announcements from '@/app/(index)/_components/Announcements';
+
+const Home = async () => {
+ const currentUser = await getCurrentUser();
+
return (
- null
+
+
+
+
+
+
+
UH Groupings
+
+
+
Manage your groupings in one place, use them in many.
+
+ {!currentUser.roles.includes(Role.UH) &&
+ }
+
+
+
+
+
+
+
+
+
+ {currentUser.roles.includes(Role.UH) ? (
+
+ ) : (
+
+ )}
+
);
}
diff --git a/ui/src/app/about/page.tsx b/ui/src/app/about/page.tsx
index c8d17f53..92d02ce0 100644
--- a/ui/src/app/about/page.tsx
+++ b/ui/src/app/about/page.tsx
@@ -1,58 +1,8 @@
-import Image from 'next/image';
+import UHGroupingsInfo from '@/components/UHGroupingsInfo';
const About = () => (
-
-
-
-
What is a UH Grouping?
-
A grouping is a collection of members
- (e.g., all full-time
- Hilo faculty).
-
-
-
-
-
-
-
Create groupings, manage grouping memberships,
- control members' self-service
- options, designate sync destinations, and more.
-
-
-
-
-
-
-
Synchronize groupings email LISTSERV lists, attributes for access
- control via
- CAS and LDAP, etc.
-
-
-
-
-
-
-
Leverage group data from official sources, which can
- substantially reduce the
- manual overhead of membership management.
-
-
-
-
-
+
GENERAL INFO
@@ -60,8 +10,10 @@ const About = () => (
How do I request a new grouping?
A request form is available .
+ href={'https://uhawaii.atlassian.net/wiki/spaces/UHIAM/' +
+ 'pages/13402308/UH+Groupings+Request+Form'}
+ aria-label="A request form is available">A request form is available
+ .
Exactly what is a grouping?
diff --git a/ui/src/components/UHGroupingsInfo.tsx b/ui/src/components/UHGroupingsInfo.tsx
new file mode 100644
index 00000000..c454ef8d
--- /dev/null
+++ b/ui/src/components/UHGroupingsInfo.tsx
@@ -0,0 +1,65 @@
+import Image from 'next/image';
+
+const UHGroupingsInfo = ({
+ size
+}: {
+ size?: 'lg' | 'default';
+}) => {
+ const color = size === 'lg' ? 'text-text-color' : 'text-uh-black';
+ const textSize = size === 'lg' ? 'text-[1.2rem]' : 'text-base';
+
+ const infoItems = [
+ {
+ description: 'Create groupings, manage grouping memberships, control members\' self-service options, ' +
+ 'designate sync destinations, and more.',
+ icon: {
+ src: '/uhgroupings/cogs.svg',
+ alt: 'Cogs icon'
+ }
+ },
+ {
+ description: 'Synchronize groupings email LISTSERV lists, ' +
+ 'attributes for access control via CAS and LDAP, etc..',
+ icon: {
+ src: '/uhgroupings/id-email.svg',
+ alt: 'Email icon'
+ }
+ },
+ {
+ description: 'Leverage group data from official sources, ' +
+ 'which can substantially reduce the manual overhead of membership management.',
+ icon: {
+ src: '/uhgroupings/watch.svg',
+ alt: 'Watch icon'
+ }
+ }
+ ];
+
+ return (
+
+
+
+
+
What is a UH Grouping?
+
A grouping is a collection of members
+ (e.g., all full-time
+ Hilo faculty).
+
+
+ {infoItems.map((infoItem, index) => (
+
+
+
+
+
{infoItem.description}
+
+ ))}
+
+
+
+
+ );
+};
+
+export default UHGroupingsInfo;
diff --git a/ui/src/components/layout/navbar/Navbar.tsx b/ui/src/components/layout/navbar/Navbar.tsx
index a9a5921c..12da83ed 100644
--- a/ui/src/components/layout/navbar/Navbar.tsx
+++ b/ui/src/components/layout/navbar/Navbar.tsx
@@ -9,11 +9,10 @@ import TimeoutModal from '@/components/modal/TimeoutModal';
const Navbar = async () => {
const currentUser = await getCurrentUser();
-
- return (
+ return (
<>
-
+
diff --git a/ui/src/components/ui/alert-dialog.tsx b/ui/src/components/ui/alert-dialog.tsx
index 23dc59af..59521254 100644
--- a/ui/src/components/ui/alert-dialog.tsx
+++ b/ui/src/components/ui/alert-dialog.tsx
@@ -18,7 +18,8 @@ const AlertDialogOverlay = React.forwardRef<
>(({ className, ...props }, ref) => (
svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute
+ [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-slate-950 dark:border-slate-800 dark:[&>svg]:text-slate-50`,
+ {
+ variants: {
+ variant: {
+ default: 'bg-white text-slate-950 dark:bg-slate-950 dark:text-slate-50',
+ destructive:
+ `border-red-500/50 text-red-500 dark:border-red-500 [&>svg]:text-red-500 dark:border-red-900/50
+ dark:text-red-900 dark:dark:border-red-900 dark:[&>svg]:text-red-900`,
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ },
+ }
+)
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+))
+Alert.displayName = 'Alert'
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertTitle.displayName = 'AlertTitle'
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertDescription.displayName = 'AlertDescription'
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/ui/src/components/ui/button.tsx b/ui/src/components/ui/button.tsx
index 587f9ce3..b83044e0 100644
--- a/ui/src/components/ui/button.tsx
+++ b/ui/src/components/ui/button.tsx
@@ -12,7 +12,9 @@ const buttonVariants = cva(
{
variants: {
variant: {
- default: `bg-[#5a9cb4] hover:bg-green-blue [text-shadow:_0_1px_1px_#444] text-slate-50`,
+ default: `bg-[#6fa9be] border-transparent text-slate-50 [text-shadow:_0_1px_1px_#444] bg-gradient-to-b
+ from-[#7db1c4] to-[#5a9cb4] border hover:from-green-blue hover:to-green-blue
+ border-x-black/[.0.1] border-t-black/[.0.1] border-b-black/[.0.25]`,
destructive: `bg-red-500 text-slate-50 hover:bg-red-500/90 dark:bg-red-900 dark:text-slate-50
dark:hover:bg-red-900/90`,
outline: `border border-green-blue bg-white hover:bg-green-blue hover:text-white text-uh-teal`,
@@ -22,8 +24,8 @@ const buttonVariants = cva(
},
size: {
default: 'h-10 px-2.5 py-2',
- sm: 'h-9 rounded-md px-2',
- lg: 'h-11 rounded-md px-8',
+ sm: 'h-9 px-2',
+ lg: 'h-12 px-4 text-xl',
icon: 'h-10 w-10',
},
},
diff --git a/ui/src/services/GroupingsApiService.ts b/ui/src/services/GroupingsApiService.ts
index 9f6944ac..66ea81a4 100644
--- a/ui/src/services/GroupingsApiService.ts
+++ b/ui/src/services/GroupingsApiService.ts
@@ -42,7 +42,7 @@ const baseUrl = process.env.NEXT_PUBLIC_API_2_1_BASE_URL as string;
*
* @returns The promise of announcements or ApiError type
*/
-export const getAnnouncements = (): Promise => {
+export const getAnnouncements = (): Promise => {
const endpoint = `${baseUrl}/announcements`;
return getRequest(endpoint);
}
@@ -435,7 +435,7 @@ export const managePersonResults = async (
*
* @returns The promise of the number of memberships or ApiError type
*/
-export const getNumberOfMemberships = async (): Promise => {
+export const getNumberOfMemberships = async (): Promise => {
const currentUser = await getCurrentUser();
const endpoint = `${baseUrl}/members/${currentUser.uid}/memberships/count`;
return getRequest(endpoint, currentUser.uid);
@@ -543,7 +543,7 @@ export const ownerGroupings = async (): Promise => {
*
* @returns The promise of the number of groupings or ApiError type
*/
-export const getNumberOfGroupings = async (): Promise => {
+export const getNumberOfGroupings = async (): Promise => {
const currentUser = await getCurrentUser();
const endpoint = `${baseUrl}/owners/${currentUser.uid}/groupings/count`;
return getRequest(endpoint, currentUser.uid);
diff --git a/ui/tailwind.config.ts b/ui/tailwind.config.ts
index c46c6b2e..21cf3651 100644
--- a/ui/tailwind.config.ts
+++ b/ui/tailwind.config.ts
@@ -28,8 +28,10 @@ const config = {
'uh-teal': '#0d7078',
'seafoam': '#e3f2ef',
'text-color': '#1c6070',
+ 'text-primary': '#004252',
'link-color' : '#006ffa',
- 'link-hover-color' : '#0056b3'
+ 'link-hover-color' : '#0056b3',
+ 'blue-background': '#00a6b2'
},
fontFamily: {
'source-sans-3': ['var(--font-source-sans-3)', 'Helvetica', 'Arial', 'sans-serif'],
diff --git a/ui/tests/app/(index)/_components/AfterLogin.test.tsx b/ui/tests/app/(index)/_components/AfterLogin.test.tsx
new file mode 100644
index 00000000..69551a69
--- /dev/null
+++ b/ui/tests/app/(index)/_components/AfterLogin.test.tsx
@@ -0,0 +1,121 @@
+import Role from '@/access/Role'
+import { render, screen } from '@testing-library/react';
+import User from '@/access/User';
+import * as GroupingsApiService from '@/services/GroupingsApiService';
+import * as AuthenticationService from '@/access/AuthenticationService';
+import afterLogin from '@/app/(index)/_components/AfterLogin';
+
+jest.mock('@/services/GroupingsApiService');
+jest.mock('@/access/AuthenticationService');
+const testUser: User = JSON.parse(process.env.TEST_USER_A as string);
+
+describe('AfterLogin', () => {
+ const numberOfGroupings = 18;
+ const numberOfMemberships = 17;
+
+ const admin: User = {
+ ...testUser,
+ roles:[Role.UH, Role.OWNER,Role.ADMIN] as const
+ };
+
+ const owner: User = {
+ ...testUser,
+ roles: [Role.UH, Role.OWNER] as const
+ };
+
+ const uhUser: User = {
+ ...testUser,
+ roles: [Role.UH] as const
+ };
+
+ const expectWelcome = (User: User, role:string) => {
+ expect(screen.getByAltText('user-solid')).toHaveAttribute('src', '/uhgroupings/user-solid.svg');
+ expect(screen.getByLabelText('key-round')).toBeInTheDocument();
+ expect(screen.getByTestId('welcome-message')).toHaveTextContent(`Welcome, ${User.firstName}!`);
+ expect(screen.getByTestId('role')).toHaveTextContent(`Role: ${role}`);
+ };
+
+ const expectAdministration = (isAdmin: boolean) => {
+ if (isAdmin) {
+ expect(screen.getByRole('img', {name: 'key-solid'})).toHaveAttribute('src', 'uhgroupings/key-solid.svg');
+ expect(screen.queryByText('0')).not.toBeInTheDocument();
+ expect(screen.getByRole('heading', {name: 'Admin'})).toBeInTheDocument();
+ expect(screen.getByText('Manage the list of Administrators for this service. ' +
+ 'Search for and manage any grouping on behalf of the owner.')).toBeInTheDocument();
+ expect(screen.getByRole('link', {name: 'Admin'})).toHaveAttribute('href', '/admin');
+ expect(screen.getByRole('button', {name: 'Admin'})).toBeInTheDocument();
+ } else {
+ expect(screen.queryByRole('img', {name: 'key-solid'})).not.toBeInTheDocument();
+ expect(screen.queryByText('0')).not.toBeInTheDocument();
+ expect(screen.queryByRole('heading', {name: 'Admin'})).not.toBeInTheDocument();
+ expect(screen.queryByText('Manage the list of Administrators for this service. ' +
+ 'Search for and manage any grouping on behalf of the owner.')).not.toBeInTheDocument();
+ expect(screen.queryByRole('link', {name: 'Admin'})).not.toBeInTheDocument();
+ expect(screen.queryByRole('button', {name: 'Admin'})).not.toBeInTheDocument();
+ }
+ };
+
+ const expectMemberships = () => {
+ expect(screen.getByRole('img', {name: 'id-card'})).toHaveAttribute('src', 'uhgroupings/id-card-solid.svg');
+ expect(screen.getByText(numberOfMemberships)).toBeInTheDocument();
+ expect(screen.getByRole('heading', {name: 'Memberships'})).toBeInTheDocument();
+ expect(screen.getByText('View and manage my memberships. ' +
+ 'Search for new groupings to join as a member.')).toBeInTheDocument();
+ expect(screen.getByRole('link', {name: 'Memberships'})).toHaveAttribute('href', '/memberships');
+ expect(screen.getByRole('button', {name: 'Memberships'})).toBeInTheDocument();
+ }
+
+ const expectGroupings = (isOwner: boolean) => {
+ if (isOwner) {
+ expect(screen.getByRole('img', {name: 'wrench-solid'}))
+ .toHaveAttribute('src', 'uhgroupings/wrench-solid.svg');
+ expect(screen.getByText(numberOfGroupings)).toBeInTheDocument();
+ expect(screen.getByRole('heading', {name: 'Groupings'})).toBeInTheDocument();
+ expect(screen.getByText('Review members, manage Include and Exclude lists, ' +
+ 'configure preferences, and export members.')).toBeInTheDocument();
+ expect(screen.getByRole('link', {name: 'Groupings'})).toHaveAttribute('href', '/groupings');
+ expect(screen.getByRole('button', {name: 'Groupings'})).toBeInTheDocument();
+ } else {
+ expect(screen.queryByRole('img', {name: 'wrench-solid'})).not.toBeInTheDocument();
+ expect(screen.queryByText(numberOfGroupings)).not.toBeInTheDocument();
+ expect(screen.queryByRole('heading', {name: 'Groupings'})).not.toBeInTheDocument();
+ expect(screen.queryByText('Review members, manage Include and Exclude lists, ' +
+ 'configure preferences, and export members.')).not.toBeInTheDocument();
+ expect(screen.queryByRole('link', {name: 'Groupings'})).not.toBeInTheDocument();
+ expect(screen.queryByRole('button', {name: 'Groupings'})).not.toBeInTheDocument();
+ }
+
+ };
+
+ beforeEach(() => {
+ jest.spyOn(GroupingsApiService, 'getNumberOfGroupings').mockResolvedValue(numberOfGroupings);
+ jest.spyOn(GroupingsApiService, 'getNumberOfMemberships').mockResolvedValue(numberOfMemberships);
+ })
+
+ it('Should render correctly when logged in as an admin', async () => {
+ jest.spyOn(AuthenticationService, 'getCurrentUser').mockResolvedValue(admin);
+ render(await afterLogin());
+ expectWelcome(admin, 'Admin');
+ expectAdministration(true);
+ expectMemberships();
+ expectGroupings(true);
+ });
+
+ it('Should render correctly when logged in as Owner', async () => {
+ jest.spyOn(AuthenticationService, 'getCurrentUser').mockResolvedValue(owner);
+ render(await afterLogin());
+ expectWelcome(owner, 'Owner');
+ expectAdministration(false);
+ expectMemberships();
+ expectGroupings(true);
+ });
+
+ it('Should render correctly when logged in as a user with a UH account', async () => {
+ jest.spyOn(AuthenticationService, 'getCurrentUser').mockResolvedValue(uhUser);
+ render(await afterLogin());
+ expectWelcome(uhUser, 'Member');
+ expectAdministration(false);
+ expectMemberships();
+ expectGroupings(false);
+ });
+})
diff --git a/ui/tests/app/(index)/_components/Announcement.test.tsx b/ui/tests/app/(index)/_components/Announcement.test.tsx
new file mode 100644
index 00000000..093d43ac
--- /dev/null
+++ b/ui/tests/app/(index)/_components/Announcement.test.tsx
@@ -0,0 +1,30 @@
+import { render, screen } from '@testing-library/react';
+import Announcements from '@/app/(index)/_components/Announcements';
+import * as GroupingsApiService from '@/services/GroupingsApiService';
+
+jest.mock('@/services/GroupingsApiService');
+
+const message = 'test announcement';
+const message1 = 'test1 announcement';
+const oldMessage = 'expired announcement';
+const announcements = {
+ resultCode: '200',
+ announcements: [
+ { message: message, state: 'Active', start: '4/11/2024', end: '4/15/2024' },
+ { message: message1, state: 'Active', start: '4/11/2024', end: '4/15/2024' },
+ { message: oldMessage, state: 'Expired', start: '4/11/2021', end: '4/15/2021' },
+ ]
+};
+
+describe('Announcements Component', () => {
+ it('renders announcement correctly', async () => {
+ jest.spyOn(GroupingsApiService, 'getAnnouncements').mockResolvedValue(announcements);
+ render(await Announcements());
+
+ expect(screen.getAllByLabelText('icon')).toHaveLength(2);
+ expect(screen.getAllByText('Announcement')).toHaveLength(2);
+ expect(screen.getByText(message)).toBeInTheDocument();
+ expect(screen.getByText(message1)).toBeInTheDocument();
+ expect(screen.queryByText(oldMessage)).not.toBeInTheDocument();
+ });
+});
diff --git a/ui/tests/app/(index)/_components/BeforeLogin.test.tsx b/ui/tests/app/(index)/_components/BeforeLogin.test.tsx
new file mode 100644
index 00000000..8f134a50
--- /dev/null
+++ b/ui/tests/app/(index)/_components/BeforeLogin.test.tsx
@@ -0,0 +1,29 @@
+import { render, screen } from '@testing-library/react';
+import BeforeLogin from '@/app/(index)/_components/BeforeLogin';
+describe('BeforeLogin Component', () => {
+
+ it('renders UHGroupingsInfo and a button with correct text and link', ()=> {
+ render( );
+ const size = 'text-[1.2rem]';
+ const linkUrl = 'https://uhawaii.atlassian.net/wiki/spaces/UHIAM/pages/13403213/UH+Groupings';
+ expect(screen.getByRole('heading', {name:'What is a UH Grouping?'}))
+ .toHaveClass('text-text-color');
+ expect(screen.getByTestId('description')).toBeInTheDocument();
+
+ expect(screen.getByAltText('Cogs icon')).toHaveAttribute('src', '/uhgroupings/cogs.svg');
+ expect(screen.getByText('Create groupings, manage grouping memberships, control members\' ' +
+ 'self-service options, designate sync destinations, and more.')).toHaveClass(size);
+
+ expect(screen.getByAltText('Email icon')).toHaveAttribute('src', '/uhgroupings/id-email.svg');
+ expect(screen.getByText('Synchronize groupings email LISTSERV lists,' +
+ ' attributes for access control via CAS and LDAP, etc..')).toHaveClass(size);
+
+ expect(screen.getByAltText('Watch icon')).toHaveAttribute('src', '/uhgroupings/watch.svg');
+ expect(screen.getByText('Leverage group data from official sources,' +
+ ' which can substantially reduce the manual overhead of membership management.')).toHaveClass(size);
+
+ expect(screen.getByRole('link')).toHaveAttribute('href', linkUrl);
+ expect(screen.getByRole('button', {name:'Learn More'})).toBeInTheDocument();
+ });
+});
+
diff --git a/ui/tests/app/(index)/_components/LoginButton.test.tsx b/ui/tests/app/(index)/_components/LoginButton.test.tsx
new file mode 100644
index 00000000..a7b84bca
--- /dev/null
+++ b/ui/tests/app/(index)/_components/LoginButton.test.tsx
@@ -0,0 +1,47 @@
+import { render, screen, fireEvent } from '@testing-library/react';
+import { redirect } from 'next/navigation';
+import User, { AnonymousUser } from '@/access/User';
+import Role from '@/access/Role';
+import LoginButton from '@/app/(index)/_components/LoginButton';
+
+const casUrl = process.env.NEXT_PUBLIC_CAS_URL as string;
+const baseUrl = process.env.NEXT_PUBLIC_BASE_URL as string;
+const testUser: User = JSON.parse(process.env.TEST_USER_A as string);
+
+describe('LoginButton Component', () => {
+
+ describe('User is not logged in', () => {
+ it('should render a Login button', () => {
+ render();
+
+ expect(screen.getByRole('button', {name: 'Login Here'})).toBeInTheDocument;
+ });
+ it('should visit the CAS login url on click', () => {
+ render();
+
+ const casLoginUrl = `${casUrl}/login?service=${encodeURIComponent(`${baseUrl}/api/cas/login`)}`;
+ fireEvent.click(screen.getByRole('button', {name: 'Login Here'}));
+ expect(redirect).toHaveBeenCalledWith(casLoginUrl);
+ });
+ });
+
+ describe('User is logged in', () => {
+ beforeAll(() => {
+ testUser.roles.push(Role.UH);
+ });
+
+ it('should render a Logout button with the uid of the logged-in user', () => {
+ render();
+
+ expect(screen.getByRole('button', {name: 'Logout'})).toBeInTheDocument;
+ });
+
+ it('should visit the CAS logout url on click', () => {
+ render();
+
+ const casLogoutUrl = `${casUrl}/logout?service=${encodeURIComponent(`${baseUrl}/api/cas/logout`)}`;
+ fireEvent.click(screen.getByRole('button', {name: 'Logout'}));
+ expect(redirect).toHaveBeenCalledWith(casLogoutUrl);
+ });
+ });
+});
diff --git a/ui/tests/components/UHGroupingsInfo.test.tsx b/ui/tests/components/UHGroupingsInfo.test.tsx
new file mode 100644
index 00000000..a70daa9f
--- /dev/null
+++ b/ui/tests/components/UHGroupingsInfo.test.tsx
@@ -0,0 +1,47 @@
+import { render, screen } from '@testing-library/react';
+import UHGroupingsInfo from '@/components/UHGroupingsInfo';
+
+describe('UHGroupings Component', () => {
+ const expectCogs = (size: 'text-[1.2rem]' | 'text-base') => {
+ const description = 'Create groupings, manage grouping memberships, control members\' ' +
+ 'self-service options, designate sync destinations, and more.';
+ expect(screen.getByAltText('Cogs icon')).toHaveAttribute('src', '/uhgroupings/cogs.svg');
+ expect(screen.getByText(description)).toHaveClass(size);
+
+ };
+
+ const expectEmail = (size: 'text-[1.2rem]' | 'text-base') => {
+ const description = 'Synchronize groupings email LISTSERV lists,' +
+ ' attributes for access control via CAS and LDAP, etc..';
+ expect(screen.getByAltText('Email icon')).toHaveAttribute('src', '/uhgroupings/id-email.svg');
+ expect(screen.getByText(description)).toHaveClass(size);
+ };
+
+ const expectWatch = (size: 'text-[1.2rem]' | 'text-base') => {
+ const description = 'Leverage group data from official sources,' +
+ ' which can substantially reduce the manual overhead of membership management.';
+ expect(screen.getByAltText('Watch icon')).toHaveAttribute('src', '/uhgroupings/watch.svg');
+ expect(screen.getByText(description)).toHaveClass(size);
+ };
+
+ it('renders with variant prop', () => {
+ render( );
+ expect(screen.getByRole('heading', {name: 'What is a UH Grouping?'}))
+ .toHaveClass('text-text-color');
+ expect(screen.getByTestId('description')).toBeInTheDocument();
+ expectCogs('text-[1.2rem]');
+ expectEmail('text-[1.2rem]');
+ expectWatch('text-[1.2rem]');
+
+ });
+
+ it('renders without variant prop', () => {
+ render( );
+ expect(screen.getByRole('heading', {name: 'What is a UH Grouping?'}))
+ .toHaveClass('text-uh-black');
+ expect(screen.getByTestId('description')).toBeInTheDocument();
+ expectCogs('text-base');
+ expectEmail('text-base');
+ expectWatch('text-base');
+ });
+});