Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions e2e/home.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ test.describe('Homepage', () => {
await page.goto('/');
});

test('hero renders with heading', async ({ page }) => {
const heading = page.locator('h1');
await expect(heading).toContainText("UIUC's Hub for");
await expect(heading).toContainText('Everything CS.');
test('Hero renders with heading', async ({ page }) => {
const heading = page.getByRole('heading', {
name: "UIUC's Hub for Everything CS.",
});
expect(heading).toBeVisible();
});

test('Join ACM and Donate buttons are visible', async ({ page }) => {
Expand All @@ -29,4 +30,10 @@ test.describe('Homepage', () => {
await expect(page.locator(`img[src*="${sponsor}"]`)).toBeVisible();
}
});
test('Copyright section is visible', async ({ page }) => {
const currentYear = new Date().getFullYear();
await expect(
page.getByText(`© ${currentYear} ACM @ UIUC. All rights reserved.`)
).toBeVisible();
});
});
33 changes: 33 additions & 0 deletions e2e/identitySync.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { test, expect } from '@playwright/test';

test.describe('Identity Sync', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/admin/sync');
});

test('Page renders with heading', async ({ page }) => {
const heading = page.locator('h1');
await expect(heading).toContainText('Sync Identity');
const desc = page.locator('p').first();
await expect(desc).toContainText(
'Use this tool to setup your account, or update your ACM account with details from your Illinois NetID.'
);
});
test('Page provides sync button which leads to login redirect', async ({
page,
}) => {
await expect(
page.getByRole('button', { name: 'Sync Identity' })
).toBeVisible();
await page.getByRole('button', { name: 'Sync Identity' }).click();
await page.waitForURL(
'https://login.microsoftonline.com/44467e6f-462c-4ea2-823f-7800de5434e3/oauth2/v2.0/authorize?client_id=d64e9c50-d144-4b4a-a315-ad2ed456c37**'
);
});
test('Copyright section is visible', async ({ page }) => {
const currentYear = new Date().getFullYear();
await expect(
page.getByText(`© ${currentYear} ACM @ UIUC. All rights reserved.`)
).toBeVisible();
});
});
56 changes: 56 additions & 0 deletions e2e/membership.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { test, expect } from '@playwright/test';

test.describe('Membership Purchase', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/membership');
});

test('Page renders with heading and perks', async ({ page }) => {
const heading = page.locator('h1');
await expect(heading).toContainText('Become a Paid Member');

const perksList = page.getByTestId('membership-perks');
await expect(perksList).toBeVisible();
const listItems = perksList.locator('li');
expect((await listItems.all()).length).toBeGreaterThan(0);
});
test('Page provides purchase button which leads to login redirect', async ({
page,
}) => {
await expect(
page.getByRole('button', { name: 'Purchase Membership' })
).toBeVisible();
await page.getByRole('button', { name: 'Purchase Membership' }).click();
await page.waitForURL(
'https://login.microsoftonline.com/44467e6f-462c-4ea2-823f-7800de5434e3/oauth2/v2.0/authorize?client_id=d64e9c50-d144-4b4a-a315-ad2ed456c37**'
);
});
});

test.describe('Membership Check', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/membership/check');
});

test('Page renders with heading', async ({ page }) => {
const heading = page.locator('h1');
await expect(heading).toContainText('Check Paid Membership');
});
test('Page provides check button which leads to login redirect', async ({
page,
}) => {
await expect(
page.getByRole('button', { name: 'Check Membership' })
).toBeVisible();
await page.getByRole('button', { name: 'Check Membership' }).click();
await page.waitForURL(
'https://login.microsoftonline.com/44467e6f-462c-4ea2-823f-7800de5434e3/oauth2/v2.0/authorize?client_id=d64e9c50-d144-4b4a-a315-ad2ed456c37**'
);
});
test('Copyright section is visible', async ({ page }) => {
const currentYear = new Date().getFullYear();
await expect(
page.getByText(`© ${currentYear} ACM @ UIUC. All rights reserved.`)
).toBeVisible();
});
});
19 changes: 10 additions & 9 deletions e2e/navigation.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { test, expect } from '@playwright/test';

const internalPages = [
{ href: '/about', heading: /About/i },
{ href: '/calendar', heading: /Calendar/i },
{ href: '/store', heading: /All Products/i },
];

test.describe('Navigation', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
Expand All @@ -24,21 +30,16 @@ test.describe('Navigation', () => {
await expect(page.locator('nav a[href*="illinoiscs.wiki"]')).toBeVisible();
});

test('internal nav links navigate to correct pages', async ({ page }) => {
const pages = [
{ href: '/about', heading: /About/i },
{ href: '/calendar', heading: /Calendar/i },
];

for (const p of pages) {
for (const p of internalPages) {
test(`Internal nav link navigates to ${p.href}`, async ({ page }) => {
await page.goto('/');
await page.locator(`nav a[href="${p.href}"]`).click();
await expect(page).toHaveURL(new RegExp(p.href));
await expect(
page.getByRole('heading', { name: p.heading }).first()
).toBeVisible();
}
});
});
}

test('Join Now button opens Join modal', async ({ page }) => {
await page.getByRole('button', { name: /Join Now/i }).click();
Expand Down
29 changes: 29 additions & 0 deletions e2e/resources.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { test, expect } from '@playwright/test';

const expectedSections = ['ACM Paid Member Guide', 'CS Cares', 'Feedback'];
test.describe('Resources', () => {
test.beforeEach(async ({ page }) => {
await page.goto('resources/');
});

test('Heading renders with description', async ({ page }) => {
const heading = page.getByTestId('content-title');
const description = page.getByTestId('content-description');
await expect(heading).toContainText('Resources');
await expect(description).toContainText(
'Learn about resources provided to ACM @ UIUC members'
);
});
for (const heading of expectedSections) {
test(`${heading} section is present`, async ({ page }) => {
const header = page.getByRole('heading', { name: heading });
expect(header).toContainText(heading);
});
}
test('Copyright section is visible', async ({ page }) => {
const currentYear = new Date().getFullYear();
await expect(
page.getByText(`© ${currentYear} ACM @ UIUC. All rights reserved.`)
).toBeVisible();
});
});
44 changes: 9 additions & 35 deletions src/authConfig.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import {
BrowserAuthError,
createStandardPublicClientApplication,
InteractionRequiredAuthError,
type IPublicClientApplication,
type PopupRequest,
type RedirectRequest,
} from '@azure/msal-browser';

export const loginRequest = {
Expand All @@ -27,54 +26,29 @@ export const initMsalClient = async () => {
};

export const getUserAccessToken = async (
pca: IPublicClientApplication
pca: IPublicClientApplication,
returnPath?: string
): Promise<string> => {
const account = pca.getActiveAccount() || pca.getAllAccounts()[0];
const request: PopupRequest = {
const request: RedirectRequest = {
...loginRequest,
account: account || undefined,
redirectUri: '/auth-redirect',
state: returnPath ?? window.location.pathname + window.location.search,
};

if (!account) {
try {
const response = await pca.loginPopup(request);
pca.setActiveAccount(response.account);
return response.accessToken;
} catch (error) {
if (
error instanceof BrowserAuthError &&
error.errorCode === 'popup_window_error'
) {
alert(
'Your browser is blocking popups. Please allow popups for this site and then try logging in again.'
);
}
console.error('MSAL login failed:', error);
throw error;
}
await pca.loginRedirect(request);
throw new Error('Redirecting to login');
}

try {
const response = await pca.acquireTokenSilent(request);
return response.accessToken;
} catch (e) {
if (e instanceof InteractionRequiredAuthError) {
try {
const response = await pca.acquireTokenPopup(request);
return response.accessToken;
} catch (popupError) {
if (
popupError instanceof BrowserAuthError &&
popupError.errorCode === 'popup_window_error'
) {
alert(
'Your browser is blocking popups, which are required for this action. Please allow popups for this site and try again.'
);
}
console.error('MSAL popup token acquisition failed:', popupError);
throw popupError;
}
await pca.acquireTokenRedirect(request);
throw new Error('Redirecting to login', { cause: e });
}
throw e;
}
Expand Down
20 changes: 18 additions & 2 deletions src/components/AuthActionButton.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { IPublicClientApplication } from '@azure/msal-browser';
import type { LucideIcon } from 'lucide-react';
import { Loader2 } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';

import { getUserAccessToken, initMsalClient } from '../authConfig.ts';
import ErrorPopup, { useErrorPopup } from './ErrorPopup.tsx';
Expand All @@ -19,6 +19,7 @@ interface AuthActionButtonProps {
accessToken: string,
showError: ShowErrorFunction
) => Promise<void>;
returnPath?: string;
buttonClassName?: string;
bgColorClass?: string;
textColorClass?: string;
Expand All @@ -30,6 +31,7 @@ export default function AuthActionButton({
defaultText,
workingText,
onAction,
returnPath,
buttonClassName,
bgColorClass = 'bg-tangerine-600 hover:bg-tangerine-700',
textColorClass = 'text-white',
Expand All @@ -39,17 +41,31 @@ export default function AuthActionButton({
const [isWorking, setIsWorking] = useState(false);
const [pca, setPca] = useState<IPublicClientApplication>();

const autoTriggered = useRef(false);

useEffect(() => {
initMsalClient().then(setPca).catch(console.error);
}, []);

useEffect(() => {
if (!pca || autoTriggered.current) return;
const params = new URLSearchParams(window.location.search);
if (!params.has('authButtonClick')) return;
autoTriggered.current = true;
params.delete('authButtonClick');
const qs = params.toString();
const newUrl = window.location.pathname + (qs ? `?${qs}` : '');
window.history.replaceState({}, '', newUrl);
handleClick();
}, [pca]);

const handleClick = async () => {
if (!pca) {
return;
}
setIsWorking(true);
try {
const accessToken = await getUserAccessToken(pca);
const accessToken = await getUserAccessToken(pca, returnPath);
await onAction(accessToken, showError);
} finally {
setIsWorking(false);
Expand Down
1 change: 1 addition & 0 deletions src/components/MembershipCheckButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export default function MembershipCheckButton({ class: className }: Props) {
icon={IdCard}
defaultText="Check Membership"
workingText="Checking..."
returnPath={`${window.location.href}?authButtonClick`}
onAction={async (accessToken, showError) => {
try {
const response = await membershipApiClient.apiV1MembershipGet({
Expand Down
1 change: 1 addition & 0 deletions src/components/MembershipPurchaseButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export default function MembershipPurchaseButton({ class: className }: Props) {
icon={ShoppingCart}
defaultText="Purchase Membership"
workingText="Loading..."
returnPath="/membership?authButtonClick"
onAction={async (accessToken, showError) => {
try {
const checkoutUrl =
Expand Down
1 change: 1 addition & 0 deletions src/components/NetIdSyncButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export default function NetIdSyncButton() {
icon={RefreshCcw}
defaultText="Sync Identity"
workingText="Syncing..."
returnPath="/admin/sync?authButtonClick"
onAction={async (accessToken, showError) => {
try {
await genericApiClient.apiV1SyncIdentityPostRaw({
Expand Down
6 changes: 5 additions & 1 deletion src/components/calendar/CalendarControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ export default function CalendarControls({

function changeDate(offset: number) {
const unit = view === Views.AGENDA ? Views.MONTH : view;
const candidate = localizer.add(displayDate, offset, unit as string);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const candidate = localizer.add(displayDate, offset, unit as any);
if (candidate > maxDate) {
setDisplayDate(maxDate);
} else {
Expand Down Expand Up @@ -63,20 +64,23 @@ export default function CalendarControls({
<button
onClick={() => setDisplayDate(new Date())}
className="hidden rounded-md bg-navy-800 px-3 py-1.5 text-sm font-medium text-white hover:bg-navy-700 md:block"
data-testid="calendar-today"
>
Today
</button>
<div className="flex">
<button
onClick={() => changeDate(-1)}
className="rounded-l-md bg-navy-800 p-2 text-white hover:bg-navy-700"
data-testid="calendar-back"
>
<ChevronLeft size={16} />
</button>
<button
onClick={() => changeDate(1)}
disabled={nextDisabled}
className="rounded-r-md bg-navy-800 p-2 text-white hover:bg-navy-700 disabled:opacity-50"
data-testid="calendar-forward"
>
<ChevronRight size={16} />
</button>
Expand Down
Loading
Loading