Skip to content

Commit 9757d9e

Browse files
fix: handle external routes correctly (#182)
1 parent 14592d2 commit 9757d9e

2 files changed

Lines changed: 145 additions & 13 deletions

File tree

src/instructorNav/InstructorNav.test.tsx

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@ import userEvent from '@testing-library/user-event';
88

99
jest.mock('react-router-dom', () => ({
1010
useParams: jest.fn(),
11+
Link: ({ to, children, ...props }: any) => (
12+
<a href={to} data-testid="react-router-link" {...props} role="button">
13+
{children}
14+
</a>
15+
),
1116
}));
17+
1218
jest.mock('@src/data/apiHook', () => ({
1319
useCourseInfo: jest.fn(),
1420
}));
@@ -126,4 +132,125 @@ describe('InstructorNav', () => {
126132
expect(tab2Link.closest('a')).toHaveAttribute('aria-current', 'page');
127133
});
128134
});
135+
136+
describe('Simplified navigation type detection', () => {
137+
const courseId = 'course-v1:edX+DemoX+Demo_Course';
138+
139+
beforeEach(() => {
140+
(useParams as jest.Mock).mockReturnValue({ courseId, tabId: 'tab1' });
141+
(useCourseInfo as jest.Mock).mockReturnValue({
142+
data: { tabs: [] },
143+
isLoading: false,
144+
});
145+
});
146+
147+
it('uses internal navigation for URLs starting with "/"', () => {
148+
(useWidgetProps as jest.Mock).mockReturnValue([
149+
{ tabId: 'tab1', url: '/instructor-dashboard/course123/course_info', title: 'Course Info', sortOrder: 1 },
150+
{ tabId: 'tab2', url: '/courses/course123/courseware', title: 'Courseware', sortOrder: 2 },
151+
{ tabId: 'tab3', url: '/authoring/course123', title: 'Authoring', sortOrder: 3 },
152+
{ tabId: 'tab4', url: '/admin/dashboard', title: 'Admin', sortOrder: 4 },
153+
]);
154+
155+
render(<InstructorNav />);
156+
157+
const courseInfoLink = screen.getByText('Course Info');
158+
const coursewareLink = screen.getByText('Courseware');
159+
const authoringLink = screen.getByText('Authoring');
160+
const adminLink = screen.getByText('Admin');
161+
162+
// All URLs starting with "/" should use React Router Link (internal navigation)
163+
// Our mock Link component has role="button", so we can check for that to confirm it's using our internal navigation
164+
expect(courseInfoLink).toHaveAttribute('role', 'button');
165+
expect(coursewareLink).toHaveAttribute('role', 'button');
166+
expect(authoringLink).toHaveAttribute('role', 'button');
167+
expect(adminLink).toHaveAttribute('role', 'button');
168+
169+
// Verify the links are accessible and clickable
170+
expect(courseInfoLink).toBeInTheDocument();
171+
expect(coursewareLink).toBeInTheDocument();
172+
expect(authoringLink).toBeInTheDocument();
173+
expect(adminLink).toBeInTheDocument();
174+
});
175+
176+
it('uses external navigation for URLs NOT starting with "/"', () => {
177+
(useWidgetProps as jest.Mock).mockReturnValue([
178+
{ tabId: 'tab1', url: 'https://external.edx.org/courses', title: 'External Courses', sortOrder: 1 },
179+
{ tabId: 'tab2', url: 'http://learning.edx.org', title: 'Learning', sortOrder: 2 },
180+
{ tabId: 'tab3', url: 'course_info', title: 'Relative Tab', sortOrder: 3 },
181+
{ tabId: 'tab4', url: 'mailto:support@edx.org', title: 'Contact', sortOrder: 4 },
182+
]);
183+
184+
render(<InstructorNav />);
185+
186+
const externalLink = screen.getByText('External Courses');
187+
const learningLink = screen.getByText('Learning');
188+
const relativeLink = screen.getByText('Relative Tab');
189+
const contactLink = screen.getByText('Contact');
190+
191+
// All URLs NOT starting with "/" should use anchor tags (external navigation)
192+
// Our mock Link component has role="button", so if it doesn't have that, it's an external link
193+
expect(externalLink.closest('a')).not.toHaveAttribute('role', 'button');
194+
expect(learningLink.closest('a')).not.toHaveAttribute('role', 'button');
195+
expect(relativeLink.closest('a')).not.toHaveAttribute('role', 'button');
196+
expect(contactLink.closest('a')).not.toHaveAttribute('role', 'button');
197+
198+
// Verify the links are accessible and clickable
199+
expect(externalLink).toBeInTheDocument();
200+
expect(learningLink).toBeInTheDocument();
201+
expect(relativeLink).toBeInTheDocument();
202+
expect(contactLink).toBeInTheDocument();
203+
});
204+
205+
it('handles mixed internal and external URLs in same navigation', () => {
206+
(useWidgetProps as jest.Mock).mockReturnValue([
207+
{ tabId: 'tab1', url: '/internal/path', title: 'Internal', sortOrder: 1 },
208+
{ tabId: 'tab2', url: 'external-relative', title: 'External', sortOrder: 2 },
209+
{ tabId: 'tab3', url: '/another/internal', title: 'Another Internal', sortOrder: 3 },
210+
{ tabId: 'tab4', url: 'https://example.com', title: 'Absolute External', sortOrder: 4 },
211+
]);
212+
213+
render(<InstructorNav />);
214+
215+
const internalLink = screen.getByText('Internal');
216+
const externalLink = screen.getByText('External');
217+
const anotherInternalLink = screen.getByText('Another Internal');
218+
const absoluteExternalLink = screen.getByText('Absolute External');
219+
220+
// All links should be present and clickable regardless of navigation type
221+
expect(internalLink.closest('a')).toHaveAttribute('role', 'button');
222+
expect(externalLink.closest('a')).not.toHaveAttribute('role', 'button');
223+
expect(anotherInternalLink.closest('a')).toHaveAttribute('role', 'button');
224+
expect(absoluteExternalLink.closest('a')).not.toHaveAttribute('role', 'button');
225+
226+
// Verify the links are accessible
227+
expect(internalLink).toBeInTheDocument();
228+
expect(externalLink).toBeInTheDocument();
229+
expect(anotherInternalLink).toBeInTheDocument();
230+
expect(absoluteExternalLink).toBeInTheDocument();
231+
});
232+
233+
it('maintains clearAlerts functionality for both navigation types', async () => {
234+
(useWidgetProps as jest.Mock).mockReturnValue([
235+
{ tabId: 'tab1', url: '/internal', title: 'Internal Tab', sortOrder: 1 },
236+
{ tabId: 'tab2', url: 'external', title: 'External Tab', sortOrder: 2 },
237+
]);
238+
239+
render(<InstructorNav />);
240+
const user = userEvent.setup();
241+
242+
const internalTab = screen.getByText('Internal Tab');
243+
const externalTab = screen.getByText('External Tab');
244+
245+
// Click internal navigation tab
246+
await user.click(internalTab);
247+
expect(mockClearAlerts).toHaveBeenCalled();
248+
249+
mockClearAlerts.mockClear();
250+
251+
// Click external navigation tab
252+
await user.click(externalTab);
253+
expect(mockClearAlerts).toHaveBeenCalled();
254+
});
255+
});
129256
});

src/instructorNav/InstructorNav.tsx

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useParams, Link } from 'react-router-dom';
33
import { Nav, Navbar, Skeleton } from '@openedx/paragon';
44
import { useCourseInfo } from '@src/data/apiHook';
55
import { useAlert } from '@src/providers/AlertProvider';
6-
import { useWidgetProps } from '../slots/SlotUtils';
6+
import { useWidgetProps } from '@src/slots/SlotUtils';
77

88
export interface TabProps {
99
tabId: string,
@@ -58,18 +58,23 @@ const InstructorNav = () => {
5858
<Navbar.Toggle aria-controls="instructor-nav" />
5959
<Navbar.Collapse id="instructor-nav">
6060
{
61-
sortedTabs.map((tab) => (
62-
<Nav.Item key={tab.tabId}>
63-
<Nav.Link
64-
as={Link}
65-
to={tab.url}
66-
active={tab.tabId === tabId}
67-
onClick={() => clearAlerts()}
68-
>
69-
{tab.title}
70-
</Nav.Link>
71-
</Nav.Item>
72-
))
61+
sortedTabs.map((tab) => {
62+
const isInternal = tab.url.startsWith('/');
63+
return (
64+
<Nav.Item key={tab.tabId}>
65+
<Nav.Link
66+
{...(isInternal
67+
? { to: tab.url, as: Link }
68+
: { href: tab.url }
69+
)}
70+
active={tab.tabId === tabId}
71+
onClick={() => clearAlerts()}
72+
>
73+
{tab.title}
74+
</Nav.Link>
75+
</Nav.Item>
76+
);
77+
})
7378
}
7479
</Navbar.Collapse>
7580
</Nav>

0 commit comments

Comments
 (0)