diff --git a/src/instructor-toolbar/masquerade-widget/MasqueradeWidget.jsx b/src/instructor-toolbar/masquerade-widget/MasqueradeWidget.jsx index 554703fcfd..71df83cea3 100644 --- a/src/instructor-toolbar/masquerade-widget/MasqueradeWidget.jsx +++ b/src/instructor-toolbar/masquerade-widget/MasqueradeWidget.jsx @@ -22,7 +22,8 @@ class MasqueradeWidget extends Component { this.state = { autoFocus: false, masquerade: 'Staff', - options: [], + active: {}, + available: [], shouldShowUserNameInput: false, masqueradeUsername: null, }; @@ -58,21 +59,42 @@ class MasqueradeWidget extends Component { } onSuccess(data) { - const options = this.parseAvailableOptions(data); + const { active, available } = this.parseAvailableOptions(data); this.setState({ - options, + active, + available, }); } + getOptions() { + const options = this.state.available.map((group) => ( + this.toggle(...args)} + onSubmit={(payload) => this.onSubmit(payload)} + /> + )); + return options; + } + clearError() { this.props.onError(''); } - toggle(show) { + toggle(show, groupId, groupName, role, userName, userPartitionId) { this.setState(prevState => ({ autoFocus: true, - masquerade: 'Specific Student...', + masquerade: groupName, shouldShowUserNameInput: show === undefined ? !prevState.shouldShowUserNameInput : show, + active: { + ...prevState.active, groupId, role, userName, userPartitionId, + }, })); } @@ -80,19 +102,6 @@ class MasqueradeWidget extends Component { const data = postData || {}; const active = data.active || {}; const available = data.available || []; - const options = available.map((group) => ( - this.toggle(...args)} - onSubmit={(payload) => this.onSubmit(payload)} - /> - )); if (active.userName) { this.setState({ autoFocus: false, @@ -105,14 +114,13 @@ class MasqueradeWidget extends Component { } else if (active.role === 'student') { this.setState({ masquerade: 'Learner' }); } - return options; + return { active, available }; } render() { const { autoFocus, masquerade, - options, shouldShowUserNameInput, masqueradeUsername, } = this.state; @@ -126,7 +134,7 @@ class MasqueradeWidget extends Component { {masquerade} - {options} + {this.getOptions()} diff --git a/src/instructor-toolbar/masquerade-widget/MasqueradeWidget.test.jsx b/src/instructor-toolbar/masquerade-widget/MasqueradeWidget.test.jsx new file mode 100644 index 0000000000..7349858a37 --- /dev/null +++ b/src/instructor-toolbar/masquerade-widget/MasqueradeWidget.test.jsx @@ -0,0 +1,137 @@ +import React from 'react'; +import { getAllByRole } from '@testing-library/dom'; +import { act } from '@testing-library/react'; +import { getConfig } from '@edx/frontend-platform'; +import MockAdapter from 'axios-mock-adapter'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import MasqueradeWidget from './MasqueradeWidget'; +import { + render, screen, fireEvent, initializeTestStore, waitFor, logUnhandledRequests, +} from '../../setupTest'; + +const originalConfig = jest.requireActual('@edx/frontend-platform').getConfig(); +jest.mock('@edx/frontend-platform', () => ({ + ...jest.requireActual('@edx/frontend-platform'), + getConfig: jest.fn(), +})); +getConfig.mockImplementation(() => originalConfig); + +describe('Masquerade Widget Dropdown', () => { + let mockData; + let courseware; + let mockResponse; + let axiosMock; + let masqueradeUrl; + const masqueradeOptions = [ + { + name: 'Staff', + role: 'staff', + }, + { + name: 'Specific Student...', + role: 'student', + user_name: '', + }, + { + group_id: 1, + name: 'Audit', + role: 'student', + user_partition_id: 50, + }, + ]; + + beforeAll(async () => { + const store = await initializeTestStore(); + courseware = store.getState().courseware; + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + masqueradeUrl = `${getConfig().LMS_BASE_URL}/courses/${courseware.courseId}/masquerade`; + mockData = { + courseId: courseware.courseId, + onError: () => {}, + }; + }); + + beforeEach(() => { + mockResponse = { + success: true, + active: { + course_key: courseware.courseId, + group_id: null, + role: 'staff', + user_name: null, + user_partition_id: null, + group_name: null, + }, + available: masqueradeOptions, + }; + axiosMock.reset(); + axiosMock.onGet(masqueradeUrl).reply(200, mockResponse); + logUnhandledRequests(axiosMock); + }); + + it('renders masquerade name correctly', async () => { + render(); + await waitFor(() => expect(axiosMock.history.get).toHaveLength(1)); + expect(screen.getByRole('button')).toHaveTextContent('Staff'); + }); + + masqueradeOptions.forEach((option) => { + it(`marks role ${option.role} as active`, async () => { + const active = { + course_key: courseware.courseId, + group_id: option.group_id ?? null, + role: option.role, + user_name: option.user_name ?? null, + user_partition_id: option.user_partition_id ?? null, + group_name: null, + }; + + mockResponse = { + success: true, + active, + available: masqueradeOptions, + }; + + axiosMock.reset(); + axiosMock.onGet(masqueradeUrl).reply(200, mockResponse); + + const { container } = render(); + const dropdownToggle = container.querySelector('.dropdown-toggle'); + await act(async () => { + await fireEvent.click(dropdownToggle); + }); + const dropdownMenu = container.querySelector('.dropdown-menu'); + getAllByRole(dropdownMenu, 'button', { hidden: true }).forEach(button => { + if (button.textContent === option.name) { + expect(button).toHaveClass('active'); + } else { + expect(button).not.toHaveClass('active'); + } + }); + }); + }); + + it('handles the clicks with toggle', async () => { + const { container } = render(); + await waitFor(() => expect(axiosMock.history.get).toHaveLength(1)); + + const dropdownToggle = container.querySelector('.dropdown-toggle'); + await act(async () => { + await fireEvent.click(dropdownToggle); + }); + const dropdownMenu = container.querySelector('.dropdown-menu'); + const studentOption = getAllByRole(dropdownMenu, 'button', { hidden: true }).filter( + button => (button.textContent === 'Specific Student...'), + )[0]; + await act(async () => { + await fireEvent.click(studentOption); + }); + getAllByRole(dropdownMenu, 'button', { hidden: true }).forEach(button => { + if (button.textContent === 'Specific Student...') { + expect(button).toHaveClass('active'); + } else { + expect(button).not.toHaveClass('active'); + } + }); + }); +}); diff --git a/src/instructor-toolbar/masquerade-widget/MasqueradeWidgetOption.jsx b/src/instructor-toolbar/masquerade-widget/MasqueradeWidgetOption.jsx index 83a55801f7..5a7107a6b8 100644 --- a/src/instructor-toolbar/masquerade-widget/MasqueradeWidgetOption.jsx +++ b/src/instructor-toolbar/masquerade-widget/MasqueradeWidgetOption.jsx @@ -16,6 +16,7 @@ class MasqueradeWidgetOption extends Component { event.target.parentNode.parentNode.click(); const { groupId, + groupName, role, userName, userPartitionId, @@ -23,7 +24,7 @@ class MasqueradeWidgetOption extends Component { } = this.props; const payload = {}; if (userName || userName === '') { - userNameInputToggle(true); + userNameInputToggle(true, groupId, groupName, role, userName, userPartitionId); return false; } if (role) { diff --git a/src/instructor-toolbar/masquerade-widget/MasqueradeWidgetOption.test.jsx b/src/instructor-toolbar/masquerade-widget/MasqueradeWidgetOption.test.jsx new file mode 100644 index 0000000000..4e6dde1476 --- /dev/null +++ b/src/instructor-toolbar/masquerade-widget/MasqueradeWidgetOption.test.jsx @@ -0,0 +1,97 @@ +import React from 'react'; +import { getAllByRole } from '@testing-library/dom'; +import { act } from '@testing-library/react'; +import { getConfig } from '@edx/frontend-platform'; +import MasqueradeWidgetOption from './MasqueradeWidgetOption'; +import { + render, fireEvent, initializeTestStore, +} from '../../setupTest'; + +const originalConfig = jest.requireActual('@edx/frontend-platform').getConfig(); +jest.mock('@edx/frontend-platform', () => ({ + ...jest.requireActual('@edx/frontend-platform'), + getConfig: jest.fn(), +})); +getConfig.mockImplementation(() => originalConfig); + +describe('Masquerade Widget Dropdown', () => { + let courseware; + let mockDataStaff; + let mockDataStudent; + let active; + + beforeAll(async () => { + const store = await initializeTestStore(); + courseware = store.getState().courseware; + active = { + courseKey: courseware.courseId, + groupId: null, + role: 'staff', + userName: null, + userPartitionId: null, + groupName: null, + }; + mockDataStaff = { + groupId: null, + groupName: 'Staff', + key: 'Staff', + role: 'staff', + selected: active, + userName: null, + userPartitionId: null, + userNameInputToggle: () => {}, + onSubmit: () => {}, + }; + mockDataStudent = { + groupId: null, + groupName: 'Specific Student...', + key: 'Specific Student...', + role: 'student', + selected: active, + userName: '', + userPartitionId: null, + userNameInputToggle: () => {}, + onSubmit: () => {}, + }; + Object.defineProperty(global, 'location', { + configurable: true, + value: { reload: jest.fn() }, + }); + }); + + it('renders masquerade active option correctly', async () => { + const { container } = render(); + const button = getAllByRole(container, 'button', { hidden: true })[0]; + expect(button).toHaveTextContent('Staff'); + expect(button).toHaveClass('active'); + }); + + it('renders masquerade inactive option correctly', async () => { + const { container } = render(); + const button = getAllByRole(container, 'button', { hidden: true })[0]; + expect(button).toHaveTextContent('Specific Student...'); + expect(button).not.toHaveClass('active'); + }); + + it('handles the clicks regular option', () => { + const onSubmit = jest.fn().mockImplementation(() => Promise.resolve()); + const { container } = render(); + const button = getAllByRole(container, 'button', { hidden: true })[0]; + act(() => { + fireEvent.click(button); + }); + expect(onSubmit).toHaveBeenCalled(); + }); + + it('handles the clicks student option', () => { + const userNameInputToggle = jest.fn().mockImplementation(() => Promise.resolve()); + const { container } = render( + , + ); + const button = getAllByRole(container, 'button', { hidden: true })[0]; + act(() => { + fireEvent.click(button); + }); + expect(userNameInputToggle).toHaveBeenCalled(); + }); +});