diff --git a/services/web/frontend/js/features/settings/components/settings-modal.tsx b/services/web/frontend/js/features/settings/components/settings-modal.tsx index 429448cea2..e6d2605687 100644 --- a/services/web/frontend/js/features/settings/components/settings-modal.tsx +++ b/services/web/frontend/js/features/settings/components/settings-modal.tsx @@ -11,6 +11,7 @@ import { useSettingsModalContext, } from '../context/settings-modal-context' import useFocusOnSetting from '../hooks/use-focus-on-setting' +import useOpenSettingsViaQueryParam from '../hooks/use-open-settings-via-query-param' const SettingsModalWrapper = () => { return ( @@ -26,6 +27,7 @@ const SettingsModal = () => { useSettingsModalContext() useFocusOnSetting() + useOpenSettingsViaQueryParam() return ( { + const inNotificationsSplitTest = isSplitTestEnabled('email-notifications') + if (!inNotificationsSplitTest) { + return + } + + const params = new URLSearchParams(window.location.search) + if (params.get('open') !== 'project-notifications') { + return + } + + setShow(true) + setActiveTab('project_notifications') + + const url = new URL(window.location.href) + url.searchParams.delete('open') + window.history.replaceState(window.history.state, '', url.toString()) + }, [setShow, setActiveTab]) +} diff --git a/services/web/test/frontend/features/settings-modal/settings-modal.test.tsx b/services/web/test/frontend/features/settings-modal/settings-modal.test.tsx index 29998ccb77..d96ebc1e11 100644 --- a/services/web/test/frontend/features/settings-modal/settings-modal.test.tsx +++ b/services/web/test/frontend/features/settings-modal/settings-modal.test.tsx @@ -1,4 +1,4 @@ -import { screen, render } from '@testing-library/react' +import { screen, render, waitFor } from '@testing-library/react' import { expect } from 'chai' import fetchMock from 'fetch-mock' import { EditorProviders } from '../../helpers/editor-providers' @@ -131,4 +131,60 @@ describe('', function () { settings.forEach(setting => assertSettingIsVisible(setting)) }) }) + + describe('when open=project-notifications query param is present', function () { + beforeEach(function () { + window.metaAttributesCache.set('ol-splitTestVariants', { + 'email-notifications': 'enabled', + }) + fetchMock.get(/\/notifications\/preferences\/project\//, { + trackedChangesOnOwnProject: false, + trackedChangesOnInvitedProject: false, + commentOnOwnProject: false, + commentOnInvitedProject: false, + repliesOnOwnProject: false, + repliesOnInvitedProject: false, + repliesOnAuthoredThread: false, + repliesOnParticipatingThread: false, + }) + window.history.pushState({}, '', '?open=project-notifications') + }) + + afterEach(function () { + window.history.pushState({}, '', window.location.pathname) + window.metaAttributesCache.delete('ol-splitTestVariants') + }) + + it('opens the modal and selects the Project notifications tab', async function () { + render( + + + + ) + + await waitFor( + () => + expect( + screen.getByRole('tab', { + name: /Project notifications/, + selected: true, + }) + ).to.exist + ) + }) + + it('removes the open param from the URL', async function () { + render( + + + + ) + + await waitFor(() => { + expect(window.location.search).to.not.include( + 'open=project-notifications' + ) + }) + }) + }) }) diff --git a/services/web/test/frontend/features/settings-modal/settings/project-notifications-setting.test.tsx b/services/web/test/frontend/features/settings-modal/settings/project-notifications-setting.test.tsx new file mode 100644 index 0000000000..efbb179af0 --- /dev/null +++ b/services/web/test/frontend/features/settings-modal/settings/project-notifications-setting.test.tsx @@ -0,0 +1,221 @@ +import { screen, render, waitFor } from '@testing-library/react' +import { expect } from 'chai' +import fetchMock from 'fetch-mock' +import userEvent from '@testing-library/user-event' +import { EditorProviders, PROJECT_ID } from '../../../helpers/editor-providers' +import { SettingsModalProvider } from '@/features/settings/context/settings-modal-context' +import ProjectNotificationsSetting from '@/features/settings/components/editor-settings/project-notifications-setting' + +const preferencesUrl = `/notifications/preferences/project/${PROJECT_ID}` + +const allNotificationsOn = { + trackedChangesOnOwnProject: true, + trackedChangesOnInvitedProject: true, + commentOnOwnProject: true, + commentOnInvitedProject: true, + repliesOnOwnProject: true, + repliesOnInvitedProject: true, + repliesOnAuthoredThread: true, + repliesOnParticipatingThread: true, +} + +const repliesOnlyPreferences = { + trackedChangesOnOwnProject: false, + trackedChangesOnInvitedProject: false, + commentOnOwnProject: false, + commentOnInvitedProject: false, + repliesOnOwnProject: false, + repliesOnInvitedProject: false, + repliesOnAuthoredThread: true, + repliesOnParticipatingThread: true, +} + +const allNotificationsOff = { + trackedChangesOnOwnProject: false, + trackedChangesOnInvitedProject: false, + commentOnOwnProject: false, + commentOnInvitedProject: false, + repliesOnOwnProject: false, + repliesOnInvitedProject: false, + repliesOnAuthoredThread: false, + repliesOnParticipatingThread: false, +} + +function renderComponent() { + return render( + + + + + + ) +} + +describe('', function () { + afterEach(function () { + fetchMock.removeRoutes().clearHistory() + }) + + it('selects "All project activity" when all notifications are on', async function () { + fetchMock.get(preferencesUrl, allNotificationsOn) + + renderComponent() + + await waitFor( + () => + expect( + ( + screen.getByLabelText('All project activity', { + exact: false, + }) as HTMLInputElement + ).checked + ).to.be.true + ) + expect( + ( + screen.getByLabelText('Replies to your activity only', { + exact: false, + }) as HTMLInputElement + ).checked + ).to.be.false + expect( + (screen.getByLabelText('Off', { exact: false }) as HTMLInputElement) + .checked + ).to.be.false + }) + + it('selects "Replies to your activity only" when only reply notifications are on', async function () { + fetchMock.get(preferencesUrl, repliesOnlyPreferences) + + renderComponent() + + await waitFor( + () => + expect( + ( + screen.getByLabelText('Replies to your activity only', { + exact: false, + }) as HTMLInputElement + ).checked + ).to.be.true + ) + expect( + ( + screen.getByLabelText('All project activity', { + exact: false, + }) as HTMLInputElement + ).checked + ).to.be.false + expect( + (screen.getByLabelText('Off', { exact: false }) as HTMLInputElement) + .checked + ).to.be.false + }) + + it('selects "Off" when all notifications are off', async function () { + fetchMock.get(preferencesUrl, allNotificationsOff) + + renderComponent() + + await waitFor( + () => + expect( + (screen.getByLabelText('Off', { exact: false }) as HTMLInputElement) + .checked + ).to.be.true + ) + expect( + ( + screen.getByLabelText('All project activity', { + exact: false, + }) as HTMLInputElement + ).checked + ).to.be.false + expect( + ( + screen.getByLabelText('Replies to your activity only', { + exact: false, + }) as HTMLInputElement + ).checked + ).to.be.false + }) + + it('POSTs "replies" preferences when "Replies to your activity only" is selected', async function () { + fetchMock.get(preferencesUrl, allNotificationsOn) + const saveMock = fetchMock.post(preferencesUrl, { status: 200 }) + + renderComponent() + + await waitFor( + () => + expect( + ( + screen.getByLabelText('All project activity', { + exact: false, + }) as HTMLInputElement + ).checked + ).to.be.true + ) + + await userEvent.click( + screen.getByLabelText('Replies to your activity only', { exact: false }) + ) + + expect( + saveMock.callHistory.called(preferencesUrl, { + body: repliesOnlyPreferences, + }) + ).to.be.true + }) + + it('POSTs "off" preferences when "Off" is selected', async function () { + fetchMock.get(preferencesUrl, allNotificationsOn) + const saveMock = fetchMock.post(preferencesUrl, { status: 200 }) + + renderComponent() + + await waitFor( + () => + expect( + ( + screen.getByLabelText('All project activity', { + exact: false, + }) as HTMLInputElement + ).checked + ).to.be.true + ) + + await userEvent.click(screen.getByLabelText('Off', { exact: false })) + + expect( + saveMock.callHistory.called(preferencesUrl, { + body: allNotificationsOff, + }) + ).to.be.true + }) + + it('POSTs "all" preferences when "All project activity" is selected', async function () { + fetchMock.get(preferencesUrl, allNotificationsOff) + const saveMock = fetchMock.post(preferencesUrl, { status: 200 }) + + renderComponent() + + await waitFor( + () => + expect( + (screen.getByLabelText('Off', { exact: false }) as HTMLInputElement) + .checked + ).to.be.true + ) + + await userEvent.click( + screen.getByLabelText('All project activity', { exact: false }) + ) + + expect( + saveMock.callHistory.called(preferencesUrl, { + body: allNotificationsOn, + }) + ).to.be.true + }) +})