From 62465ec956b6bbf90a3eb06d084ef334e5ae9f50 Mon Sep 17 00:00:00 2001 From: Marcos Date: Thu, 12 Oct 2023 16:06:00 -0300 Subject: [PATCH] feat: added waffle flag state for Courseware Search (#1199) --- .../courseware-search/CoursewareSearch.jsx | 30 ++++++++++++ .../CoursewareSearch.test.jsx | 46 +++++++++++++++++++ src/course-home/courseware-search/index.js | 2 + src/course-home/courseware-search/messages.js | 11 +++++ src/course-home/data/api.js | 6 +++ src/course-home/data/redux.test.js | 32 +++++++++++++ src/course-home/data/thunks.js | 10 ++++ src/course-tabs/CourseTabsNavigation.jsx | 4 ++ 8 files changed, 141 insertions(+) create mode 100644 src/course-home/courseware-search/CoursewareSearch.jsx create mode 100644 src/course-home/courseware-search/CoursewareSearch.test.jsx create mode 100644 src/course-home/courseware-search/index.js create mode 100644 src/course-home/courseware-search/messages.js diff --git a/src/course-home/courseware-search/CoursewareSearch.jsx b/src/course-home/courseware-search/CoursewareSearch.jsx new file mode 100644 index 0000000000..a6bff1f23f --- /dev/null +++ b/src/course-home/courseware-search/CoursewareSearch.jsx @@ -0,0 +1,30 @@ +import React, { useState, useEffect } from 'react'; +import { useParams } from 'react-router-dom'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { Button, Icon } from '@edx/paragon'; +import { Search } from '@edx/paragon/icons'; +import messages from './messages'; +import { fetchCoursewareSearchSettings } from '../data/thunks'; + +const CoursewareSearch = ({ intl, ...rest }) => { + const { courseId } = useParams(); + const [enabled, setEnabled] = useState(false); + + useEffect(() => { + fetchCoursewareSearchSettings(courseId).then(response => setEnabled(response.enabled)); + }, [courseId]); + + if (!enabled) { return null; } + + return ( + + ); +}; + +CoursewareSearch.propTypes = { + intl: intlShape.isRequired, +}; + +export default injectIntl(CoursewareSearch); diff --git a/src/course-home/courseware-search/CoursewareSearch.test.jsx b/src/course-home/courseware-search/CoursewareSearch.test.jsx new file mode 100644 index 0000000000..a84f7ef685 --- /dev/null +++ b/src/course-home/courseware-search/CoursewareSearch.test.jsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { + act, + initializeMockApp, + render, + screen, + waitFor, +} from '../../setupTest'; +import { fetchCoursewareSearchSettings } from '../data/thunks'; +import { CoursewareSearch } from './index'; + +jest.mock('../data/thunks'); + +function renderComponent() { + const { container } = render(); + return container; +} + +describe('CoursewareSearch', () => { + beforeAll(async () => { + initializeMockApp(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('Should not render when the waffle flag is disabled', async () => { + fetchCoursewareSearchSettings.mockImplementation(() => Promise.resolve({ enabled: false })); + + await act(async () => renderComponent()); + await waitFor(() => { + expect(fetchCoursewareSearchSettings).toHaveBeenCalledTimes(1); + expect(screen.queryByTestId('courseware-search-button')).not.toBeInTheDocument(); + }); + }); + + it('Should render when the waffle flag is enabled', async () => { + fetchCoursewareSearchSettings.mockImplementation(() => Promise.resolve({ enabled: true })); + await act(async () => renderComponent()); + await waitFor(() => { + expect(fetchCoursewareSearchSettings).toHaveBeenCalledTimes(1); + expect(screen.queryByTestId('courseware-search-button')).toBeInTheDocument(); + }); + }); +}); diff --git a/src/course-home/courseware-search/index.js b/src/course-home/courseware-search/index.js new file mode 100644 index 0000000000..c773bbcbed --- /dev/null +++ b/src/course-home/courseware-search/index.js @@ -0,0 +1,2 @@ +/* eslint-disable import/prefer-default-export */ +export { default as CoursewareSearch } from './CoursewareSearch'; diff --git a/src/course-home/courseware-search/messages.js b/src/course-home/courseware-search/messages.js new file mode 100644 index 0000000000..a857249429 --- /dev/null +++ b/src/course-home/courseware-search/messages.js @@ -0,0 +1,11 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + searchOpenAction: { + id: 'learn.coursewareSerch.openAction', + defaultMessage: 'Search within this course', + description: 'Aria-label for a button that will pop up Courseware Search.', + }, +}); + +export default messages; diff --git a/src/course-home/data/api.js b/src/course-home/data/api.js index b32973ef47..ce159dcbd1 100644 --- a/src/course-home/data/api.js +++ b/src/course-home/data/api.js @@ -445,3 +445,9 @@ export async function unsubscribeFromCourseGoal(token) { return getAuthenticatedHttpClient().post(url.href) .then(res => camelCaseObject(res)); } + +export async function getCoursewareSearchEnabledFlag(courseId) { + const url = new URL(`${getConfig().LMS_BASE_URL}/courses/${courseId}/courseware-search/enabled/`); + const { data } = await getAuthenticatedHttpClient().get(url.href); + return { enabled: data.enabled || false }; +} diff --git a/src/course-home/data/redux.test.js b/src/course-home/data/redux.test.js index f96c830b83..cf03fffd36 100644 --- a/src/course-home/data/redux.test.js +++ b/src/course-home/data/redux.test.js @@ -250,4 +250,36 @@ describe('Data layer integration tests', () => { expect(axiosMock.history.post[0].data).toEqual(`{"course_id":"${courseId}"}`); }); }); + + describe('Test fetchCoursewareSearchSettings', () => { + it('Should return enabled as true when enabled', async () => { + const apiUrl = `${getConfig().LMS_BASE_URL}/courses/${courseId}/courseware-search/enabled/`; + axiosMock.onGet(apiUrl).reply(200, { enabled: true }); + + const { enabled } = await thunks.fetchCoursewareSearchSettings(courseId); + + expect(axiosMock.history.get[0].url).toEqual(apiUrl); + expect(enabled).toBe(true); + }); + + it('Should return enabled as false when disabled', async () => { + const apiUrl = `${getConfig().LMS_BASE_URL}/courses/${courseId}/courseware-search/enabled/`; + axiosMock.onGet(apiUrl).reply(200, { enabled: false }); + + const { enabled } = await thunks.fetchCoursewareSearchSettings(courseId); + + expect(axiosMock.history.get[0].url).toEqual(apiUrl); + expect(enabled).toBe(false); + }); + + it('Should return enabled as false on error', async () => { + const apiUrl = `${getConfig().LMS_BASE_URL}/courses/${courseId}/courseware-search/enabled/`; + axiosMock.onGet(apiUrl).networkError(); + + const { enabled } = await thunks.fetchCoursewareSearchSettings(courseId); + + expect(axiosMock.history.get[0].url).toEqual(apiUrl); + expect(enabled).toBe(false); + }); + }); }); diff --git a/src/course-home/data/thunks.js b/src/course-home/data/thunks.js index b7c1c3f9a2..47339137fd 100644 --- a/src/course-home/data/thunks.js +++ b/src/course-home/data/thunks.js @@ -12,6 +12,7 @@ import { postDismissWelcomeMessage, postRequestCert, getLiveTabIframe, + getCoursewareSearchEnabledFlag, } from './api'; import { @@ -139,3 +140,12 @@ export function processEvent(eventData, getTabData) { } }; } + +export async function fetchCoursewareSearchSettings(courseId) { + try { + const { enabled } = await getCoursewareSearchEnabledFlag(courseId); + return { enabled }; + } catch (e) { + return { enabled: false }; + } +} diff --git a/src/course-tabs/CourseTabsNavigation.jsx b/src/course-tabs/CourseTabsNavigation.jsx index 838a558ad6..2157c9f9cc 100644 --- a/src/course-tabs/CourseTabsNavigation.jsx +++ b/src/course-tabs/CourseTabsNavigation.jsx @@ -5,11 +5,15 @@ import classNames from 'classnames'; import messages from './messages'; import Tabs from '../generic/tabs/Tabs'; +import { CoursewareSearch } from '../course-home/courseware-search'; const CourseTabsNavigation = ({ activeTabSlug, className, tabs, intl, }) => (
+
+ +