Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions shell/constants.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export const homeRole = 'org.openedx.frontend.role.home';
export const providesChromelessRolesId = 'org.openedx.frontend.provides.chromelessRoles.v1';
export const loginRole = 'org.openedx.frontend.role.login';
export const registerRole = 'org.openedx.frontend.role.register';
61 changes: 61 additions & 0 deletions shell/header/anonymous-menu/LoginButton.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';

import { getUrlByRouteRole, useSiteConfig } from '../../../runtime';
import { IntlProvider } from '../../../runtime/i18n';
import LoginButton from './LoginButton';

jest.mock('../../../runtime', () => ({
...jest.requireActual('../../../runtime'),
getUrlByRouteRole: jest.fn(),
useSiteConfig: jest.fn(),
}));

const mockGetUrlByRouteRole = getUrlByRouteRole as jest.MockedFunction<typeof getUrlByRouteRole>;
const mockUseSiteConfig = useSiteConfig as jest.MockedFunction<typeof useSiteConfig>;

function renderLoginButton() {
return render(
<IntlProvider locale="en">
<MemoryRouter>
<LoginButton />
</MemoryRouter>
</IntlProvider>
);
}

describe('LoginButton', () => {
beforeEach(() => {
mockUseSiteConfig.mockReturnValue({ loginUrl: 'http://localhost:18000/login' } as any);
});

afterEach(() => {
jest.clearAllMocks();
});

it('links to the route provided by an authentication app', () => {
mockGetUrlByRouteRole.mockReturnValue('/authn/login');

renderLoginButton();

expect(mockGetUrlByRouteRole).toHaveBeenCalledWith('org.openedx.frontend.role.login');
expect(screen.getByRole('link', { name: 'Login' })).toHaveAttribute('href', '/authn/login');
});

it('links to an external login route when one is configured', () => {
mockGetUrlByRouteRole.mockReturnValue('https://auth.example.com/login');

renderLoginButton();

expect(screen.getByRole('link', { name: 'Login' })).toHaveAttribute('href', 'https://auth.example.com/login');
});

it('falls back to loginUrl when no app provides the login role', () => {
mockGetUrlByRouteRole.mockReturnValue(null);

renderLoginButton();

expect(screen.getByRole('link', { name: 'Login' })).toHaveAttribute('href', 'http://localhost:18000/login');
});
});
10 changes: 8 additions & 2 deletions shell/header/anonymous-menu/LoginButton.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { Button } from '@openedx/paragon';
import { useSiteConfig, useIntl } from '../../../runtime';
import { getUrlByRouteRole, useSiteConfig, useIntl } from '../../../runtime';
import { loginRole } from '../../constants';
import messages from '../../Shell.messages';
import { getLinkProps } from './utils';

export default function LoginButton({ ...props }) {
const config = useSiteConfig();
const intl = useIntl();

// Prefer the route provided by an installed authentication app, falling back
// to the login service configured for the site.
const url = getUrlByRouteRole(loginRole) ?? config.loginUrl;

return (
<Button variant="link" href={config.loginUrl} {...props}>
<Button variant="link" {...getLinkProps(url)} {...props}>
{intl.formatMessage(messages['header.user.menu.login'])}
</Button>
);
Expand Down
53 changes: 53 additions & 0 deletions shell/header/anonymous-menu/RegisterButton.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';

import { getUrlByRouteRole, useSiteConfig } from '../../../runtime';
import { IntlProvider } from '../../../runtime/i18n';
import RegisterButton from './RegisterButton';

jest.mock('../../../runtime', () => ({
...jest.requireActual('../../../runtime'),
getUrlByRouteRole: jest.fn(),
useSiteConfig: jest.fn(),
}));

const mockGetUrlByRouteRole = getUrlByRouteRole as jest.MockedFunction<typeof getUrlByRouteRole>;
const mockUseSiteConfig = useSiteConfig as jest.MockedFunction<typeof useSiteConfig>;

function renderRegisterButton() {
return render(
<IntlProvider locale="en">
<MemoryRouter>
<RegisterButton />
</MemoryRouter>
</IntlProvider>
);
}

describe('RegisterButton', () => {
beforeEach(() => {
mockUseSiteConfig.mockReturnValue({ lmsBaseUrl: 'http://localhost:18000' } as any);
});

afterEach(() => {
jest.clearAllMocks();
});

it('links to the route provided by an authentication app', () => {
mockGetUrlByRouteRole.mockReturnValue('/authn/register');

renderRegisterButton();

expect(mockGetUrlByRouteRole).toHaveBeenCalledWith('org.openedx.frontend.role.register');
expect(screen.getByRole('link', { name: 'Sign Up' })).toHaveAttribute('href', '/authn/register');
});

it('falls back to the LMS registration page when no app provides the register role', () => {
mockGetUrlByRouteRole.mockReturnValue(null);

renderRegisterButton();

expect(screen.getByRole('link', { name: 'Sign Up' })).toHaveAttribute('href', 'http://localhost:18000/register');
});
});
10 changes: 8 additions & 2 deletions shell/header/anonymous-menu/RegisterButton.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import { Button } from '@openedx/paragon';

import { useSiteConfig, useIntl } from '../../../runtime';
import { getUrlByRouteRole, useSiteConfig, useIntl } from '../../../runtime';
import { registerRole } from '../../constants';
import messages from '../../Shell.messages';
import { getLinkProps } from './utils';

export default function RegisterButton({ ...props }) {
const config = useSiteConfig();
const intl = useIntl();

// Prefer the route provided by an installed authentication app, falling back
// to the registration page served by the LMS.
const url = getUrlByRouteRole(registerRole) ?? `${config.lmsBaseUrl}/register`;

return (
<Button variant="outline-primary" href={`${config.lmsBaseUrl}/register`} {...props}>
<Button variant="outline-primary" {...getLinkProps(url)} {...props}>
{intl.formatMessage(messages['header.user.menu.register'])}
</Button>
);
Expand Down
19 changes: 19 additions & 0 deletions shell/header/anonymous-menu/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ElementType } from 'react';
import { Link } from 'react-router-dom';

interface LinkProps {
as?: ElementType;
to?: string;
href?: string;
}

/**
* Builds the props needed to link to a URL, keeping navigation inside the
* client when the URL is a route in this site rather than an external one.
*/
export function getLinkProps(url: string): LinkProps {
if (url.startsWith('/')) {
return { as: Link, to: url };
}
return { href: url };
}