Skip to content
51 changes: 49 additions & 2 deletions src/course-outline/outline-sidebar/AddSidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
} from '@src/course-outline/outline-sidebar/OutlineSidebarContext';
import fetchMock from 'fetch-mock-jest';
import type { ContainerType } from '@src/generic/key-utils';
import { XBlock } from '@src/data/types';
import { SelectionState, XBlock } from '@src/data/types';
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
import { CourseOutlineProvider } from '@src/course-outline/CourseOutlineContext';
import { snakeCaseKeys } from '@src/editors/utils';
Expand Down Expand Up @@ -70,17 +70,21 @@ jest.mock('@src/studio-home/hooks', () => ({
let currentFlow: OutlineFlow | null = null;
let isCurrentFlowOn = false;
let currentItemData: Partial<XBlock> | null;
let selectedContainerState: SelectionState | undefined;
const clearSelection = jest.fn();
const stopCurrentFlow = jest.fn();
const openContainerSidebar = jest.fn();
jest.mock('../outline-sidebar/OutlineSidebarContext', () => ({
...jest.requireActual('../outline-sidebar/OutlineSidebarContext'),
useOutlineSidebarContext: () => ({
...jest.requireActual('../outline-sidebar/OutlineSidebarContext').useOutlineSidebarContext(),
currentFlow,
isCurrentFlowOn,
currentItemData,
selectedContainerState,
clearSelection,
stopCurrentFlow,
openContainerSidebar,
}),
}));

Expand Down Expand Up @@ -132,6 +136,10 @@ describe('AddSidebar', () => {
return newMockResult;
});
outlineChildren = courseOutlineIndexMock.courseStructure.childInfo.children;
selectedContainerState = undefined;
clearSelection.mockClear();
stopCurrentFlow.mockClear();
openContainerSidebar.mockClear();
});

it('renders the AddSidebar component without any errors', async () => {
Expand Down Expand Up @@ -399,7 +407,46 @@ describe('AddSidebar', () => {

const back = await screen.findByRole('button', { name: 'Back' });
await user.click(back);
expect(clearSelection).toHaveBeenCalled();
expect(stopCurrentFlow).toHaveBeenCalled();
});

it('back from unit sets subsection index in selection', async () => {
const user = userEvent.setup();
const section = outlineChildren[0];
const subsection = section.childInfo.children[0];
const unit = subsection.childInfo.children[0];
selectedContainerState = {
currentId: unit.id,
subsectionId: subsection.id,
sectionId: section.id,
};
renderComponent();

const back = await screen.findByRole('button', { name: 'Back' });
await user.click(back);

expect(openContainerSidebar).toHaveBeenCalledWith(
subsection.id,
subsection.id,
section.id,
0,
);
});

it('back from subsection without section clears selection', async () => {
const user = userEvent.setup();
const section = outlineChildren[0];
const subsection = section.childInfo.children[0];
selectedContainerState = {
currentId: subsection.id,
subsectionId: subsection.id,
};
renderComponent();

const back = await screen.findByRole('button', { name: 'Back' });
await user.click(back);

expect(clearSelection).toHaveBeenCalled();
expect(openContainerSidebar).not.toHaveBeenCalled();
});
});
9 changes: 7 additions & 2 deletions src/course-outline/outline-sidebar/AddSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { COURSE_BLOCK_NAMES } from '@src/constants';
import { BlockCardButton } from '@src/generic/sidebar/BlockCardButton';
import AlertMessage from '@src/generic/alert-message';
import { useCourseItemData } from '@src/course-outline/data/apiHooks';
import { useBackNavigation } from './back-navigation';
import { useOutlineSidebarContext } from './OutlineSidebarContext';
import messages from './messages';

Expand Down Expand Up @@ -363,9 +364,9 @@ export const AddSidebar = () => {
isCurrentFlowOn,
currentFlow,
currentItemData,
clearSelection,
stopCurrentFlow,
selectedContainerState,
openContainerSidebar,
} = useOutlineSidebarContext();
const { data: flowData } = useCourseItemData(currentFlow?.parentLocator);
const titleAndIcon = useMemo(() => {
Expand All @@ -386,9 +387,13 @@ export const AddSidebar = () => {
courseDetails,
]);

const handleSelectionBack = useBackNavigation({
openContainer: openContainerSidebar,
});

const handleBack = () => {
clearSelection();
stopCurrentFlow();
handleSelectionBack();
};

return (
Expand Down
75 changes: 74 additions & 1 deletion src/course-outline/outline-sidebar/OutlineAlignSidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,18 @@ jest.mock('@src/content-tags-drawer', () => ({
}));

describe('OutlineAlignSidebar', () => {
const setCurrentSelection = jest.fn();
const clearSelection = jest.fn();
const openContainerSidebar = jest.fn();
const sectionId = 'block-v1:test+course+run+type@chapter+block@section-1';
const subsectionId = 'block-v1:test+course+run+type@sequential+block@subsection-1';
const unitId = 'block-v1:test+course+run+type@vertical+block@unit-1';

beforeEach(() => {
initializeMocks();
setCurrentSelection.mockReset();
clearSelection.mockReset();
openContainerSidebar.mockReset();
jest
.spyOn(CourseAuthoringContext, 'useCourseAuthoringContext')
.mockReturnValue({
Expand All @@ -26,14 +36,26 @@ describe('OutlineAlignSidebar', () => {
jest
.spyOn(CourseOutlineContext, 'useCourseOutlineContext')
.mockReturnValue({
setCurrentSelection: jest.fn(),
setCurrentSelection,
sections: [
{
id: sectionId,
childInfo: {
children: [
{ id: subsectionId, childInfo: { children: [{ id: unitId }] } },
],
},
},
],
} as any);
jest
.spyOn(OutlineSidebarContext, 'useOutlineSidebarContext')
.mockReturnValue({
selectedContainerState: {
currentId: 'block-v1:test+course+run+type@sequential+block@seq1',
},
clearSelection,
openContainerSidebar,
} as any);
jest
.spyOn(CourseDetailsApi, 'useCourseDetails')
Expand Down Expand Up @@ -67,6 +89,8 @@ describe('OutlineAlignSidebar', () => {
.spyOn(OutlineSidebarContext, 'useOutlineSidebarContext')
.mockReturnValue({
selectedContainerState: undefined,
clearSelection,
openContainerSidebar,
} as any);
jest
.spyOn(CourseDetailsApi, 'useCourseDetails')
Expand All @@ -82,4 +106,53 @@ describe('OutlineAlignSidebar', () => {

expect(await screen.findByText('Test Course')).toBeInTheDocument();
});

it('back button selects parent block in align sidebar', async () => {
jest
.spyOn(OutlineSidebarContext, 'useOutlineSidebarContext')
.mockReturnValue({
selectedContainerState: {
currentId: unitId,
subsectionId,
sectionId,
},
clearSelection,
openContainerSidebar,
} as any);

render(<OutlineAlignSidebar />);

const backButton = await screen.findByRole('button', { name: /back/i });
backButton.click();

expect(openContainerSidebar).toHaveBeenCalledWith(subsectionId, subsectionId, sectionId, 0);
expect(setCurrentSelection).toHaveBeenCalledWith({
currentId: subsectionId,
subsectionId,
sectionId,
index: 0,
});
});

it('back button clears align selection when parent selection does not exist', async () => {
jest
.spyOn(OutlineSidebarContext, 'useOutlineSidebarContext')
.mockReturnValue({
selectedContainerState: {
currentId: sectionId,
sectionId,
},
clearSelection,
openContainerSidebar,
} as any);

render(<OutlineAlignSidebar />);

const backButton = await screen.findByRole('button', { name: /back/i });
backButton.click();

expect(clearSelection).toHaveBeenCalled();
expect(setCurrentSelection).toHaveBeenCalledWith(undefined);
expect(openContainerSidebar).not.toHaveBeenCalled();
});
});
12 changes: 6 additions & 6 deletions src/course-outline/outline-sidebar/OutlineAlignSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useContentData } from '@src/content-tags-drawer/data/apiHooks';
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
import { useCourseOutlineContext } from '@src/course-outline/CourseOutlineContext';
import { AlignSidebar } from '@src/generic/sidebar/AlignSidebar';
import { useBackNavigation } from './back-navigation';
import { useOutlineSidebarContext } from './OutlineSidebarContext';

/**
Expand All @@ -10,17 +11,16 @@ import { useOutlineSidebarContext } from './OutlineSidebarContext';
export const OutlineAlignSidebar = () => {
const { courseId } = useCourseAuthoringContext();
const { setCurrentSelection } = useCourseOutlineContext();
const { selectedContainerState, clearSelection } = useOutlineSidebarContext();
const { selectedContainerState, openContainerSidebar } = useOutlineSidebarContext();

const sidebarContentId = selectedContainerState?.currentId || courseId;

const { data: contentData } = useContentData(sidebarContentId);

// istanbul ignore next
const handleBack = () => {
clearSelection();
setCurrentSelection(undefined);
};
const handleBack = useBackNavigation({
openContainer: openContainerSidebar,
onSelectionChange: setCurrentSelection,
});

return (
<AlignSidebar
Expand Down
119 changes: 119 additions & 0 deletions src/course-outline/outline-sidebar/back-navigation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { SelectionState } from '@src/data/types';
import { getBackSelectionState, openSelectionState } from './back-navigation';

describe('back-navigation', () => {
const sections = [
{
id: 'section-1',
childInfo: {
children: [
{ id: 'subsection-1' },
{ id: 'subsection-2' },
],
},
},
] as any;

describe('getBackSelectionState', () => {
it('returns undefined when currentId missing', () => {
expect(getBackSelectionState(undefined, sections)).toBeUndefined();
});

it('returns section selection when current is subsection', () => {
const state: SelectionState = {
currentId: 'subsection-1',
subsectionId: 'subsection-1',
sectionId: 'section-1',
};

expect(getBackSelectionState(state, sections)).toEqual({
currentId: 'section-1',
sectionId: 'section-1',
index: 0,
});
});

it('returns undefined when subsection has no section', () => {
const state: SelectionState = {
currentId: 'subsection-1',
subsectionId: 'subsection-1',
};

expect(getBackSelectionState(state, sections)).toBeUndefined();
});

it('returns undefined when current is section', () => {
const state: SelectionState = {
currentId: 'section-1',
sectionId: 'section-1',
};

expect(getBackSelectionState(state, sections)).toBeUndefined();
});

it('returns subsection selection when current is unit', () => {
const state: SelectionState = {
currentId: 'unit-1',
subsectionId: 'subsection-2',
sectionId: 'section-1',
};

expect(getBackSelectionState(state, sections)).toEqual({
currentId: 'subsection-2',
subsectionId: 'subsection-2',
sectionId: 'section-1',
index: 1,
});
});

it('returns subsection selection with undefined index when section missing', () => {
const state: SelectionState = {
currentId: 'unit-1',
subsectionId: 'subsection-2',
sectionId: 'missing-section',
};

expect(getBackSelectionState(state, sections)).toEqual({
currentId: 'subsection-2',
subsectionId: 'subsection-2',
sectionId: 'missing-section',
index: undefined,
});
});

it('uses selectedSection for index when sections list lacks children', () => {
const state: SelectionState = {
currentId: 'unit-1',
subsectionId: 'subsection-2',
sectionId: 'section-1',
};

const selectedSection = {
id: 'section-1',
childInfo: { children: [{ id: 'subsection-1' }, { id: 'subsection-2' }] },
} as any;

expect(getBackSelectionState(state, [{ id: 'section-1' }] as any, selectedSection)).toEqual({
currentId: 'subsection-2',
subsectionId: 'subsection-2',
sectionId: 'section-1',
index: 1,
});
});
});

describe('openSelectionState', () => {
it('opens container with SelectionState payload', () => {
const openContainer = jest.fn();

openSelectionState(openContainer, {
currentId: 'subsection-1',
subsectionId: 'subsection-1',
sectionId: 'section-1',
index: 0,
});

expect(openContainer).toHaveBeenCalledWith('subsection-1', 'subsection-1', 'section-1', 0);
});
});
});
Loading