Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 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
9 changes: 9 additions & 0 deletions src/authz-module/authz-home/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ jest.mock('@openedx/paragon', () => ({
Tabs: ({ children }: { children: React.ReactNode }) => <div data-testid="tabs">{children}</div>,
}));

jest.mock('@src/authz-module/team-members/TeamMembersTable', () => function MockTeamMembersTable() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets try to avoid mocking components, you can setup the test:

import { screen } from '@testing-library/react';
import { renderWithAllProviders } from '@src/setupTest';
import { useAllRoleAssignments, useOrgs, useScopes } from '@src/authz-module/data/hooks';
import { ToastManagerProvider } from '@src/authz-module/libraries-manager/ToastManagerContext';
import AuthzHome from './index';

jest.mock('@src/authz-module/data/hooks', () => ({
  useAllRoleAssignments: jest.fn(),
  useOrgs: jest.fn(),
  useScopes: jest.fn(),
}));

const emptyResponse = {
  data: { results: [], count: 0, next: null, previous: null },
  error: null,
  isLoading: false,
  refetch: jest.fn(),
};

beforeEach(() => {
  (useAllRoleAssignments as jest.Mock).mockReturnValue(emptyResponse);
  (useOrgs as jest.Mock).mockReturnValue(emptyResponse);
  (useScopes as jest.Mock).mockReturnValue(emptyResponse);
});

const renderAuthzHome = () => renderWithAllProviders(
  <ToastManagerProvider>
    <AuthzHome />
  </ToastManagerProvider>,
);

And just test the accessibility assertions

expect(screen.getByRole('heading', { name: 'Library Team Management' })).toBeInTheDocument();

// Or we can use messages to make it more dynamic, it seems the final page header still need some modifications in terms of title

expect(screen.getByText(messages['authz.breadcrumb.root'].defaultMessage)).toBeInTheDocument();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have refactored the tests to stop mocking components and focus on user centric assertions.

return <div>Team Members Table Content</div>;
});

describe('AuthzHome', () => {
it('renders without crashing', () => {
renderWrapper(<AuthzHome />);
Expand All @@ -37,4 +41,9 @@ describe('AuthzHome', () => {
renderWrapper(<AuthzHome />);
expect(screen.getByTestId('roles-permissions')).toBeInTheDocument();
});

it('renders the TeamMembersTable component in the team members tab', () => {
renderWrapper(<AuthzHome />);
expect(screen.getByText('Team Members Table Content')).toBeInTheDocument();
});
});
17 changes: 8 additions & 9 deletions src/authz-module/authz-home/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { Tab, Tabs } from '@openedx/paragon';
import { useLocation } from 'react-router-dom';
import { useLocation, useSearchParams } from 'react-router-dom';
import TeamMembersTable from '@src/authz-module/team-members/TeamMembersTable';
import AddRoleButton from '@src/authz-module/components/AddRoleButton';
import RolesPermissions from '../roles-permissions/RolesPermissions';
import AuthZLayout from '../components/AuthZLayout';

Expand All @@ -9,8 +11,10 @@ import messages from './messages';
const AuthzHome = () => {
const { hash } = useLocation();
const intl = useIntl();
const [searchParams] = useSearchParams();
const presetScope = searchParams.get('scope') || undefined;

const rootBreadcrumb = intl.formatMessage(messages['authz.breadcrumb.root']) || '';
const rootBreadcrumb = intl.formatMessage(messages['authz.breadcrumb.root']);
const pageTitle = intl.formatMessage(messages['authz.manage.page.title']);

return (
Expand All @@ -22,11 +26,7 @@ const AuthzHome = () => {
pageTitle={pageTitle}
pageSubtitle=""
actions={
[]
// this needs to be enable again once is refactored to be used outside of library context
// [
// <AddNewTeamMemberTrigger libraryId="" key="add-new-member" />,
// ]
[<AddRoleButton key="add-role-button" />]
}
>
<Tabs
Expand All @@ -35,8 +35,7 @@ const AuthzHome = () => {
className="bg-light-100 px-5"
>
<Tab eventKey="team" title={intl.formatMessage(messages['authz.tabs.team'])} className="p-5">
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<Tab eventKey="team" title={intl.formatMessage(messages['authz.tabs.team'])} className="p-5">
<Tab eventKey="team" title={intl.formatMessage(messages['authz.tabs.team'])} className="p-5 bg-light-200">

{/* TODO: once TeamTable is refactored we can call it here. For now, this tab will be empty. */}
{/* <TeamTable /> */}
<TeamMembersTable presetScope={presetScope} />
</Tab>
<Tab id="libraries-permissions-roles-tab" eventKey="permissionsRoles" title={intl.formatMessage(messages['authz.tabs.permissionsRoles'])}>
<RolesPermissions />
Expand Down
134 changes: 134 additions & 0 deletions src/authz-module/components/AddRoleButton.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useNavigate } from 'react-router-dom';
import { initializeMockApp } from '@edx/frontend-platform/testing';
import { renderWrapper } from '@src/setupTest';
import AddRoleButton from './AddRoleButton';

// Mock react-router-dom navigation
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: jest.fn(),
}));

describe('AddRoleButton', () => {
const mockNavigate = jest.fn();

beforeAll(() => {
initializeMockApp({
authenticatedUser: {
userId: 1,
username: 'testuser',
email: '[email protected]',
},
});
});

beforeEach(() => {
(useNavigate as jest.Mock).mockReturnValue(mockNavigate);
});

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

describe('rendering', () => {
it('renders the assign role button with correct text', () => {
renderWrapper(<AddRoleButton />);

const button = screen.getByRole('button', { name: /assign role/i });
expect(button).toBeInTheDocument();
});

it('displays the plus icon', () => {
renderWrapper(<AddRoleButton />);

const button = screen.getByRole('button', { name: /assign role/i });
expect(button.querySelector('svg')).toBeInTheDocument();
});

it('renders correctly when presetUsername is provided', () => {
renderWrapper(<AddRoleButton presetUsername="testuser123" />);

const button = screen.getByRole('button', { name: /assign role/i });
expect(button).toBeInTheDocument();
});
});

describe('navigation behavior', () => {
it('navigates to assign role page without username when clicked', async () => {
const user = userEvent.setup();
renderWrapper(<AddRoleButton />);

const button = screen.getByRole('button', { name: /assign role/i });
await user.click(button);

expect(mockNavigate).toHaveBeenCalledTimes(1);
expect(mockNavigate).toHaveBeenCalledWith('/authz/assign-role');
});

it('navigates to assign role page with username query parameter when presetUsername is provided', async () => {
const user = userEvent.setup();
const presetUsername = 'john.doe';
renderWrapper(<AddRoleButton presetUsername={presetUsername} />);

const button = screen.getByRole('button', { name: /assign role/i });
await user.click(button);

expect(mockNavigate).toHaveBeenCalledTimes(1);
expect(mockNavigate).toHaveBeenCalledWith(`/authz/assign-role?username=${presetUsername}`);
});

it('handles special characters in presetUsername correctly', async () => {
const user = userEvent.setup();
const presetUsername = '[email protected]';
renderWrapper(<AddRoleButton presetUsername={presetUsername} />);

const button = screen.getByRole('button', { name: /assign role/i });
await user.click(button);

expect(mockNavigate).toHaveBeenCalledTimes(1);
expect(mockNavigate).toHaveBeenCalledWith(`/authz/assign-role?username=${presetUsername}`);
});
});

describe('user interactions', () => {
it('responds to keyboard navigation', async () => {
const user = userEvent.setup();
renderWrapper(<AddRoleButton />);

const button = screen.getByRole('button', { name: /assign role/i });

await user.tab();
expect(button).toHaveFocus();

await user.keyboard('{Enter}');
expect(mockNavigate).toHaveBeenCalledWith('/authz/assign-role');
});

it('responds to spacebar activation', async () => {
const user = userEvent.setup();
renderWrapper(<AddRoleButton />);

const button = screen.getByRole('button', { name: /assign role/i });
button.focus();

await user.keyboard(' ');
expect(mockNavigate).toHaveBeenCalledWith('/authz/assign-role');
});

it('handles multiple clicks gracefully', async () => {
const user = userEvent.setup();
renderWrapper(<AddRoleButton presetUsername="testuser" />);

const button = screen.getByRole('button', { name: /assign role/i });

await user.click(button);
await user.click(button);
await user.click(button);

expect(mockNavigate).toHaveBeenCalledTimes(3);
expect(mockNavigate).toHaveBeenCalledWith('/authz/assign-role?username=testuser');
});
});
});
32 changes: 32 additions & 0 deletions src/authz-module/components/AddRoleButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon';
import { Plus } from '@openedx/paragon/icons';

import baseMessages from '@src/authz-module/messages';
import { useNavigate } from 'react-router-dom';

interface AddRoleButtonProps {
presetUsername?: string;
}

const AddRoleButton = ({ presetUsername }: AddRoleButtonProps) => {
const intl = useIntl();
const navigate = useNavigate();

const handleClick = () => {
const assignRolePath = `/authz/assign-role${presetUsername ? `?username=${presetUsername}` : ''}`;
navigate(assignRolePath);
};

return (
<Button
iconBefore={Plus}
onClick={handleClick}
>
{intl.formatMessage(baseMessages['authz.management.assign.role.title'])}
</Button>
);
};

export default AddRoleButton;
7 changes: 4 additions & 3 deletions src/authz-module/components/AuthZLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ interface AuthZLayoutProps extends AuthZTitleProps {
const AuthZLayout = ({ children, context, ...props }: AuthZLayoutProps) => (
<>
<StudioHeader
number={context.id}
org={context.org}
title={context.title}
number={context?.id || null}
org={context?.org || null}
title={context?.title || null}
isHiddenMainMenu
/>
<AuthZTitle {...props} />
{children}
Expand Down
Loading