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..e52c5fbc
--- /dev/null
+++ b/ui/src/app/(index)/_components/AfterLogin.tsx
@@ -0,0 +1,102 @@
+import User from '@/access/User';
+import Role from '@/access/Role';
+import Image from 'next/image';
+import { KeyRound } from 'lucide-react';
+import PageInfoCard from '@/app/(index)/_components/PageInfoCard';
+
+const AfterLogin = ({
+ currentUser,
+ numberOfGroupings,
+ numberOfMemberships
+}: {
+ currentUser: User,
+ numberOfGroupings: number,
+ numberOfMemberships: number
+}) => {
+ const isAdmin = currentUser.roles.includes(Role.ADMIN);
+ const isOwner = currentUser.roles.includes(Role.OWNER);
+ const getHighestRole = () => {
+ if (isAdmin) return 'Admin';
+ else if (isOwner) return 'Owner';
+ else return 'Member';
+ }
+
+ return (
+
+
+
+
+
+
+
Welcome, {currentUser.firstName} !
+ Role: {getHighestRole()}
+
+
+
+
+
+
+ {isAdmin &&
+
+ }
+
+
+
+
+
+ {isOwner &&
+
+ }
+
+
+ );
+}
+
+export default AfterLogin;
diff --git a/ui/src/app/(index)/_components/Announcement.tsx b/ui/src/app/(index)/_components/Announcement.tsx
new file mode 100644
index 00000000..2e00816f
--- /dev/null
+++ b/ui/src/app/(index)/_components/Announcement.tsx
@@ -0,0 +1,19 @@
+'use client';
+import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
+import { AlertCircle } from 'lucide-react';
+
+const Announcement = ({
+ announcement
+}: {
+ announcement: string
+}) => (
+
+
+ Announcement
+
+ {announcement}
+
+
+);
+
+export default Announcement;
diff --git a/ui/src/app/(index)/_components/BeforeLogin.tsx b/ui/src/app/(index)/_components/BeforeLogin.tsx
new file mode 100644
index 00000000..d47e4a2f
--- /dev/null
+++ b/ui/src/app/(index)/_components/BeforeLogin.tsx
@@ -0,0 +1,22 @@
+import { Button } from '@/components/ui/button';
+import { ArrowRight } from 'lucide-react';
+import UHGroupingsInfo from '@/components/UHGroupingsInfo';
+
+const BeforeLogin = () => {
+ return (
+
+
+
+
+ );
+};
+
+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)/_components/PageInfoCard.tsx b/ui/src/app/(index)/_components/PageInfoCard.tsx
new file mode 100644
index 00000000..586ecd35
--- /dev/null
+++ b/ui/src/app/(index)/_components/PageInfoCard.tsx
@@ -0,0 +1,37 @@
+import { ReactNode } from 'react';
+import Link from 'next/link';
+import { Button } from '@/components/ui/button';
+
+
+const PageInfoCard = ({
+ title,
+ description,
+ href,
+ children,
+ number
+}: {
+ title: string,
+ description: string,
+ href: string,
+ children?: ReactNode,
+ number?: number
+}) => {
+ return (
+
+
+
+ {children}
+ {number && {number} }
+
+
{title}
+
{description}
+
+
+
{title}
+
+
+ );
+
+}
+
+export default PageInfoCard
diff --git a/ui/src/app/(index)/page.tsx b/ui/src/app/(index)/page.tsx
index b0223723..f1920f2f 100644
--- a/ui/src/app/(index)/page.tsx
+++ b/ui/src/app/(index)/page.tsx
@@ -1,6 +1,74 @@
-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 { getAnnouncements, getNumberOfGroupings, getNumberOfMemberships } from '@/services/GroupingsApiService';
+import Announcement from '@/app/(index)/_components/Announcement';
+
+const Home = async () => {
+ const [currentUser, numberOfGroupings, numberOfMemberships, announcements] = await Promise.all([
+ getCurrentUser(),
+ getNumberOfGroupings(),
+ getNumberOfMemberships(),
+ getAnnouncements()
+ ]);
+
+ const activeAnnouncements = () => {
+ if (!announcements) {
+ return []
+ }
+ return announcements.announcements
+ .filter((announcement) => announcement.state === 'Active')
+ .map((announcement) => announcement.message)
+ }
+
return (
- null
+
+
+ {activeAnnouncements().map((announcement: string, index: number) => (
+
+ ))}
+
+
+
+
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/AboutInfoItem.tsx b/ui/src/components/AboutInfoItem.tsx
new file mode 100644
index 00000000..749a7b36
--- /dev/null
+++ b/ui/src/components/AboutInfoItem.tsx
@@ -0,0 +1,30 @@
+import Image from 'next/image';
+
+const AboutInfoItem = ({
+ src,
+ alt,
+ description,
+ variant
+}: {
+ src: string,
+ alt: string,
+ description: string,
+ variant?: 'default' | 'home'
+}) => {
+ const size = variant === 'home' ? 'text-[1.2rem]' : 'text-base';
+
+ return (
+
+ );
+}
+
+export default AboutInfoItem;
diff --git a/ui/src/components/UHGroupingsInfo.tsx b/ui/src/components/UHGroupingsInfo.tsx
new file mode 100644
index 00000000..0b8b41f8
--- /dev/null
+++ b/ui/src/components/UHGroupingsInfo.tsx
@@ -0,0 +1,49 @@
+import AboutInfoItem from '@/components/AboutInfoItem';
+
+const UHGroupingsInfo = ({
+ variant
+}: {
+ variant?: 'default' | 'home';
+}) => {
+ const color = variant === 'home' ? 'text-text-color' : 'text-uh-black';
+ variant = variant ?? 'default';
+
+ return (
+
+
+
+
+
What is a UH Grouping?
+
A grouping is a collection of members
+ (e.g., all full-time
+ Hilo faculty).
+
+
+
+
+
+ );
+
+}
+
+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.tsx b/ui/src/components/ui/alert.tsx
new file mode 100644
index 00000000..68984663
--- /dev/null
+++ b/ui/src/components/ui/alert.tsx
@@ -0,0 +1,63 @@
+import * as React from 'react'
+import { cva, type VariantProps } from 'class-variance-authority'
+
+import { cn } from '@/components/ui/utils'
+
+const alertVariants = cva(
+
+ `relative w-full rounded-lg border border-slate-200 p-4
+ [&>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..14bda1c3
--- /dev/null
+++ b/ui/tests/app/(index)/_components/AfterLogin.test.tsx
@@ -0,0 +1,140 @@
+import Role from '@/access/Role'
+import { render, screen } from '@testing-library/react';
+import AfterLogin from '@/app/(index)/_components/AfterLogin';
+import User from '@/access/User';
+
+describe('AfterLogin', () => {
+ const numberOfGroupings = 18
+ const numberOfMemberships = 17
+ const Admin: User = {
+ name: '',
+ firstName: 'John',
+ lastName: '',
+ uid: '',
+ uhUuid: '',
+ roles: [Role.UH, Role.OWNER, Role.ADMIN] as const
+ }
+
+ const Owner: User = {
+ name: '',
+ firstName: 'John',
+ lastName: '',
+ uid: '',
+ uhUuid: '',
+ roles: [Role.UH, Role.OWNER] as const
+ }
+
+ const UHUser: User = {
+ name: '',
+ firstName: 'John',
+ lastName: '',
+ uid: '',
+ uhUuid: '',
+ roles: [Role.UH] as const
+ }
+
+ const getHighestRole = (user: User) => {
+ const isAdmin = user.roles.includes(Role.ADMIN);
+ const isOwner = user.roles.includes(Role.OWNER);
+ if (isAdmin) return 'Admin';
+ else if (isOwner) return 'Owner';
+ else return 'Member';
+ }
+
+ const Welcome = (User: User) => {
+ expect(screen.getByAltText('user-solid')).toHaveAttribute('src', '/uhgroupings/user-solid.svg');
+ expect(screen.getByTestId('key-round')).toBeInTheDocument();
+ expect(screen.getByTestId('welcome-message')).toHaveTextContent(`Welcome, ${User.firstName}!`)
+ expect(screen.getByTestId('role')).toHaveTextContent(`Role: ${getHighestRole(User)}`)
+ };
+
+ const Administration = (isAdmin: boolean) => {
+ if (isAdmin) {
+ expect(screen.getByRole('img', {name: 'key-solid'})).toHaveAttribute('src', 'uhgroupings/key-solid.svg')
+ expect(screen.queryByText('0')).toBeNull()
+ 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'})).toBeNull()
+ expect(screen.queryByText('0')).toBeNull()
+ expect(screen.queryByRole('heading', {name: 'Admin'})).toBeNull()
+ expect(screen.queryByText('Manage the list of Administrators for this service. ' +
+ 'Search for and manage any grouping on behalf of the owner.')).toBeNull()
+ expect(screen.queryByRole('link', {name: 'Admin'})).toBeNull()
+ expect(screen.queryByRole('button', {name: 'Admin'})).toBeNull()
+
+
+ }
+ }
+
+ const Memberships = () => {
+ 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 Groupings = (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'})).toBeNull()
+ expect(screen.queryByText(numberOfGroupings)).toBeNull()
+ expect(screen.queryByRole('heading', {name: 'Groupings'})).toBeNull()
+ expect(screen.queryByText('Review members, manage Include and Exclude lists, ' +
+ 'configure preferences, and export members.')).toBeNull()
+ expect(screen.queryByRole('link', {name: 'Groupings'})).toBeNull()
+ expect(screen.queryByRole('button', {name: 'Groupings'})).toBeNull()
+ }
+
+ }
+
+ it('Should render correctly when logged in as an admin', () => {
+ render(
+
+ );
+ Welcome(Admin);
+ Administration(true);
+ Memberships();
+ Groupings(true);
+ });
+
+ it('Should render correctly when logged in as Owner', () => {
+ render()
+ Welcome(Owner)
+ Administration(false)
+ Memberships();
+ Groupings(true);
+ })
+
+ it('Should render correctly when logged in as a user with a UH account', () => {
+ render()
+ Welcome(UHUser);
+ Administration(false)
+ Memberships()
+ Groupings(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..a146cee2
--- /dev/null
+++ b/ui/tests/app/(index)/_components/Announcement.test.tsx
@@ -0,0 +1,12 @@
+import { render, screen } from '@testing-library/react';
+import Announcement from '@/app/(index)/_components/Announcement';
+
+const announcement = 'old message'
+describe('Announcement Component', ()=> {
+ it('renders announcement correctly', () => {
+ render()
+ expect(screen.getByTestId('circle-alert-icon')).toBeInTheDocument()
+ expect(screen.getByText('Announcement')).toBeInTheDocument()
+ expect(screen.getByText(announcement)).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..25daedc4
--- /dev/null
+++ b/ui/tests/app/(index)/_components/BeforeLogin.test.tsx
@@ -0,0 +1,28 @@
+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/app/(index)/_components/PageInfoCard.test.tsx b/ui/tests/app/(index)/_components/PageInfoCard.test.tsx
new file mode 100644
index 00000000..aef26720
--- /dev/null
+++ b/ui/tests/app/(index)/_components/PageInfoCard.test.tsx
@@ -0,0 +1,102 @@
+import { render, screen } from '@testing-library/react';
+import PageInfoCard from '@/app/(index)/_components/PageInfoCard';
+import Image from 'next/image';
+
+describe('UserInfoItem', () => {
+ const altAdmin = 'key-solid';
+ const altMemberships = 'id-card';
+ const altGroupings = 'wrench-solid';
+ const srcAdmin = 'uhgroupings/key-solid.svg';
+ const srcMemberships = 'uhgroupings/id-card-solid.svg';
+ const srcGroupings = 'uhgroupings/wrench-solid.svg';
+
+ it('render PageInfoCard for Admin', () => {
+ const description = 'Manage the list of Administrators for this service. ' +
+ 'Search for and manage any grouping on behalf of the owner.';
+ const href = '/admin';
+ const title = 'Admin';
+
+ render(
+
+
+
+
+ );
+
+ expect(screen.getByAltText(altAdmin)).toHaveAttribute('src', srcAdmin);
+ expect(screen.getByRole('heading', { name: 'Admin' })).toBeInTheDocument();
+ expect(screen.getByText(description)).toBeInTheDocument();
+ expect(screen.getByRole('link', { name: title })).toHaveAttribute('href', href);
+ expect(screen.getByRole('button', { name: title })).toBeInTheDocument();
+ });
+
+ it('renders PageInfoCard for Memberships', () => {
+ const description = 'View and manage my memberships. ' +
+ 'Search for new groupings to join as a member.';
+ const href = '/memberships';
+ const title = 'Memberships';
+ const numberOfMemberships = 18;
+
+ render(
+
+
+
+
+ );
+
+ expect(screen.getByAltText(altMemberships)).toHaveAttribute('src', srcMemberships);
+ expect(screen.getByRole('heading', { name: 'Memberships' })).toBeInTheDocument();
+ expect(screen.getByText(numberOfMemberships)).toBeInTheDocument();
+ expect(screen.getByText(description)).toBeInTheDocument();
+ expect(screen.getByRole('link', { name: title })).toHaveAttribute('href', href);
+ expect(screen.getByRole('button', { name: title })).toBeInTheDocument();
+ });
+
+ it('renders PageInfoCard for Groupings', () => {
+ const description = 'Review members, manage Include and Exclude lists, ' +
+ 'configure preferences, and export members.';
+ const href = '/groupings';
+ const title = 'Groupings';
+ const numberOfGroupings = 17;
+
+ render(
+
+
+
+ );
+
+ expect(screen.getByAltText(altGroupings)).toHaveAttribute('src', srcGroupings);
+ expect(screen.getByRole('heading', { name: 'Groupings' })).toBeInTheDocument();
+ expect(screen.getByText(numberOfGroupings)).toBeInTheDocument();
+ expect(screen.getByText(description)).toBeInTheDocument();
+ expect(screen.getByRole('link', { name: title})).toHaveAttribute('href', href);
+ expect(screen.getByRole('button', { name: title})).toBeInTheDocument();
+ });
+});
diff --git a/ui/tests/app/(index)/page.test.tsx b/ui/tests/app/(index)/page.test.tsx
new file mode 100644
index 00000000..d46c2d4e
--- /dev/null
+++ b/ui/tests/app/(index)/page.test.tsx
@@ -0,0 +1,69 @@
+import { render, screen } from '@testing-library/react';
+import * as GroupingsApiService from '@/services/GroupingsApiService';
+import * as AuthenticationService from '@/access/AuthenticationService';
+import { Announcements } from '@/groupings/GroupingsApiResults';
+import User, { AnonymousUser } from '@/access/User';
+import Home from '@/app/(index)/page';
+import Role from '@/access/Role';
+
+jest.mock('@/services/GroupingsApiService');
+jest.mock('@/access/AuthenticationService');
+const testUser: User = JSON.parse(process.env.TEST_USER_A as string);
+
+describe('Home', () => {
+ const message = 'test announcement';
+ const announcements: Announcements = {
+ resultCode: '200',
+ announcements: [{
+ message: message, state: 'Active', start: '4/11/2024', end: '4/15/2024'
+ }]
+ };
+
+ const commonExpect = () => {
+ expect(screen.getByText(message)).toBeInTheDocument()
+ expect(screen.getByTestId('circle-alert-icon')).toBeInTheDocument()
+ expect(screen.getByText('Announcement')).toBeInTheDocument()
+ expect(screen.getByText('UH Groupings')).toBeInTheDocument()
+ expect(screen.getByText('Manage your groupings in one place, use them in many.')).toBeInTheDocument()
+ expect(screen.getByAltText('UH Groupings logotype')).toHaveAttribute('src', 'uhgroupings/uh-groupings-text.svg')
+ };
+ beforeEach(() => {
+ jest.spyOn(GroupingsApiService, 'getNumberOfGroupings').mockResolvedValue(1);
+ jest.spyOn(GroupingsApiService, 'getNumberOfMemberships').mockResolvedValue(2);
+ jest.spyOn(GroupingsApiService, 'getAnnouncements').mockResolvedValue(announcements);
+ });
+
+ test('renders Home before Login', async () => {
+ jest.spyOn(AuthenticationService, 'getCurrentUser').mockResolvedValue(AnonymousUser);
+ render(await Home())
+ const linkUrl = 'https://uhawaii.atlassian.net/wiki/spaces/UHIAM/pages/13403213/UH+Groupings'
+ commonExpect();
+ expect(screen.getByRole('button', {name: 'Login Here'})).toBeInTheDocument()
+
+ expect(screen.getByRole('heading', {name: 'What is a UH Grouping?'})).toBeInTheDocument()
+ 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.')).toBeInTheDocument()
+
+ 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..')).toBeInTheDocument()
+
+ 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.')).toBeInTheDocument()
+
+ expect(screen.getByRole('link')).toHaveAttribute('href', linkUrl)
+ expect(screen.getByRole('button', {name: 'Learn More'})).toBeInTheDocument()
+ });
+
+ test('renders Home after login', async () => {
+ testUser.roles.push(Role.UH)
+ testUser.roles.push(Role.ADMIN)
+ jest.spyOn(AuthenticationService, 'getCurrentUser').mockResolvedValue(testUser);
+ render(await Home())
+ commonExpect()
+ })
+});
diff --git a/ui/tests/components/AboutInfoItem.test.tsx b/ui/tests/components/AboutInfoItem.test.tsx
new file mode 100644
index 00000000..9a1007c4
--- /dev/null
+++ b/ui/tests/components/AboutInfoItem.test.tsx
@@ -0,0 +1,24 @@
+import AboutInfoItem from '@/components/AboutInfoItem';
+import { render, screen } from '@testing-library/react';
+
+describe('AboutInfoItem', () => {
+ const src = '/uhgroupings/cogs.svg'
+ const alt = 'Cogs icon'
+ const description = 'Create groupings, manage grouping memberships, control members\' ' +
+ 'self-service options, designate sync destinations, and more.'
+ it('renders with variant prop', () => {
+
+ render( )
+ expect(screen.getByAltText(alt)).toHaveAttribute('src', src)
+ expect(screen.getByText(description)).toBeInTheDocument()
+ expect(screen.getByText(description)).toHaveClass('text-[1.2rem]')
+ })
+
+ it('renders without variant prop', () => {
+ render()
+ expect(screen.getByAltText(alt)).toHaveAttribute('src', src)
+ expect(screen.getByText(description)).toBeInTheDocument()
+ expect(screen.getByText(description)).toHaveClass('text-base')
+
+ })
+})
diff --git a/ui/tests/components/UHGroupingsInfo.test.tsx b/ui/tests/components/UHGroupingsInfo.test.tsx
new file mode 100644
index 00000000..beaa139d
--- /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 cogs = (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 email = (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 watch = (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()
+ cogs('text-[1.2rem]')
+ email('text-[1.2rem]')
+ watch('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()
+ cogs('text-base')
+ email('text-base')
+ watch('text-base')
+ })
+})