From 5982eed3fa8321db74f30b9597cc356a3b738bf2 Mon Sep 17 00:00:00 2001 From: David <33458145+davidmcpowell@users.noreply.github.com> Date: Tue, 2 Dec 2025 09:49:12 +0000 Subject: [PATCH] Merge pull request #29821 from overleaf/dp-editor-redesign-opt-out Prepare editor redesign for opt-out phase GitOrigin-RevId: 5831970ff27e5c20f22c68b83471b6b832c2b2b4 --- .../Features/Project/ProjectController.mjs | 1 + .../Features/Project/UserSettingsHelper.mjs | 24 +++- .../Features/Tutorial/TutorialController.mjs | 2 + .../app/src/Features/User/UserController.mjs | 14 +- services/web/app/src/models/User.mjs | 4 + .../web/frontend/extracted-translations.json | 5 + .../try-new-editor-button.tsx | 17 ++- .../ide-react/components/modals/modals.tsx | 14 +- .../new-editor-opt-out-intro-modal.tsx | 81 ++++++++++++ .../components/old-editor-warning-tooltip.tsx | 82 ++++++++++++ .../use-switch-enable-new-editor-state.ts | 13 +- .../ide-redesign/utils/new-editor-utils.ts | 7 + .../shared/context/user-settings-context.tsx | 1 + .../web/frontend/stylesheets/pages/all.scss | 1 + .../editor/old-editor-warning-tooltip.scss | 15 +++ services/web/locales/en.json | 5 + .../unit/src/User/UserController.test.mjs | 120 ++++++++++++++---- services/web/types/user-settings.ts | 1 + 18 files changed, 372 insertions(+), 35 deletions(-) create mode 100644 services/web/frontend/js/features/ide-redesign/components/new-editor-opt-out-intro-modal.tsx create mode 100644 services/web/frontend/js/features/ide-redesign/components/old-editor-warning-tooltip.tsx create mode 100644 services/web/frontend/stylesheets/pages/editor/old-editor-warning-tooltip.scss diff --git a/services/web/app/src/Features/Project/ProjectController.mjs b/services/web/app/src/Features/Project/ProjectController.mjs index 0e0acfcd9d..5626ca5833 100644 --- a/services/web/app/src/Features/Project/ProjectController.mjs +++ b/services/web/app/src/Features/Project/ProjectController.mjs @@ -458,6 +458,7 @@ const _ProjectController = { 'writefull-figure-generator', 'writefull-asymetric-queue-size-per-model', 'pdf-dark-mode', + 'editor-redesign-opt-out', ].filter(Boolean) const getUserValues = async userId => diff --git a/services/web/app/src/Features/Project/UserSettingsHelper.mjs b/services/web/app/src/Features/Project/UserSettingsHelper.mjs index daae396d8d..d7ef8cb433 100644 --- a/services/web/app/src/Features/Project/UserSettingsHelper.mjs +++ b/services/web/app/src/Features/Project/UserSettingsHelper.mjs @@ -4,7 +4,7 @@ import SplitTestHandler from '../SplitTests/SplitTestHandler.mjs' const SPLIT_TEST_USER_CUTOFF_DATE = new Date(Date.UTC(2025, 8, 23, 13, 0, 0)) // 2pm British Summer Time on September 23, 2025 const NEW_USER_CUTOFF_DATE = new Date(Date.UTC(2025, 10, 12, 12, 0, 0)) // 12pm GMT on November 12, 2025 -async function getEnableNewEditorDefault(req, res, user) { +async function getEnableNewEditorLegacyDefault(req, res, user) { if (req.query['existing-user-override'] === 'true') { return false } @@ -31,7 +31,22 @@ async function getEnableNewEditorDefault(req, res, user) { } async function buildUserSettings(req, res, user) { - const defaultEnableNewEditor = await getEnableNewEditorDefault(req, res, user) + const defaultLegacyEnableNewEditor = await getEnableNewEditorLegacyDefault( + req, + res, + user + ) + + const enableNewEditorStageFour = user.ace.enableNewEditorStageFour ?? true + const enableNewEditorLegacy = + user.ace.enableNewEditor ?? defaultLegacyEnableNewEditor + + const assignment = await SplitTestHandler.promises.getAssignment( + req, + res, + 'editor-redesign-opt-out' + ) + const isOptOutEnabled = assignment.variant === 'enabled' return { mode: user.ace.mode, @@ -47,7 +62,10 @@ async function buildUserSettings(req, res, user) { mathPreview: user.ace.mathPreview, breadcrumbs: user.ace.breadcrumbs, referencesSearchMode: user.ace.referencesSearchMode, - enableNewEditor: user.ace.enableNewEditor ?? defaultEnableNewEditor, + enableNewEditor: isOptOutEnabled + ? enableNewEditorStageFour + : enableNewEditorLegacy, + enableNewEditorLegacy, darkModePdf: user.ace.darkModePdf ?? false, } } diff --git a/services/web/app/src/Features/Tutorial/TutorialController.mjs b/services/web/app/src/Features/Tutorial/TutorialController.mjs index 8513a186be..41c1cbb983 100644 --- a/services/web/app/src/Features/Tutorial/TutorialController.mjs +++ b/services/web/app/src/Features/Tutorial/TutorialController.mjs @@ -26,6 +26,8 @@ const VALID_KEYS = [ 'groups-enterprise-banner-repeat', 'new-editor-opt-in', 'new-editor-intro', + 'new-editor-intro-2', + 'old-editor-warning-tooltip', ] async function completeTutorial(req, res, next) { diff --git a/services/web/app/src/Features/User/UserController.mjs b/services/web/app/src/Features/User/UserController.mjs index f9c1e6dad1..e5f22180f3 100644 --- a/services/web/app/src/Features/User/UserController.mjs +++ b/services/web/app/src/Features/User/UserController.mjs @@ -22,6 +22,7 @@ import { expressify } from '@overleaf/promise-utils' import { acceptsJson } from '../../infrastructure/RequestContentTypeDetection.mjs' import Modules from '../../infrastructure/Modules.mjs' import OneTimeTokenHandler from '../Security/OneTimeTokenHandler.mjs' +import SplitTestHandler from '../SplitTests/SplitTestHandler.mjs' async function _sendSecurityAlertClearedSessions(user) { const emailOptions = { @@ -405,7 +406,18 @@ async function updateUserSettings(req, res, next) { user.ace.referencesSearchMode = mode } if (body.enableNewEditor != null) { - user.ace.enableNewEditor = Boolean(body.enableNewEditor) + const assignment = await SplitTestHandler.promises.getAssignment( + req, + res, + 'editor-redesign-opt-out' + ) + const isOptOutStageEnabled = assignment.variant === 'enabled' + + if (isOptOutStageEnabled) { + user.ace.enableNewEditorStageFour = Boolean(body.enableNewEditor) + } else { + user.ace.enableNewEditor = Boolean(body.enableNewEditor) + } } if (body.darkModePdf != null) { user.ace.darkModePdf = Boolean(body.darkModePdf) diff --git a/services/web/app/src/models/User.mjs b/services/web/app/src/models/User.mjs index 6b4a426430..54aac48a4a 100644 --- a/services/web/app/src/models/User.mjs +++ b/services/web/app/src/models/User.mjs @@ -100,7 +100,11 @@ export const UserSchema = new Schema( mathPreview: { type: Boolean, default: true }, breadcrumbs: { type: Boolean, default: true }, referencesSearchMode: { type: String, default: 'advanced' }, // 'advanced' or 'simple' + // enableNewEditor is being phased out in favor of enableNewEditorStageFour + // when moving the new editor to opt out (stage 4). However, we need to keep the + // old field for determining whether to show promotional material to users. enableNewEditor: { type: Boolean }, + enableNewEditorStageFour: { type: Boolean }, darkModePdf: { type: Boolean, default: false }, }, features: { diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index ca5a51b730..b1c9ddabc2 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -1777,6 +1777,7 @@ "suggested_fix_for_error_in_path": "", "suggestion_applied": "", "suggests_code_completions_while_typing": "", + "support_for_the_old_editor_is_ending_soon": "", "support_for_your_browser_is_ending_soon": "", "supports_up_to_x_licenses": "", "sure_you_want_to_cancel_plan_change": "", @@ -1787,6 +1788,8 @@ "switch_compile_mode_for_faster_draft_compilation": "", "switch_easily_between_your_files_comments_track_changes_and_more": "", "switch_to_editor": "", + "switch_to_new_editor_design": "", + "switch_to_new_look": "", "switch_to_pdf": "", "switch_to_personal_email_to_keep_your_accounts_separate": "", "switch_to_standard_plan": "", @@ -1844,6 +1847,7 @@ "the_following_folder_already_exists_in_this_project": "", "the_following_folder_already_exists_in_this_project_plural": "", "the_latex_engine_used_for_compiling": "", + "the_new_and_improved_overleaf_editor_design": "", "the_new_overleaf_editor_info": "", "the_next_payment_will_be_collected_on": "", "the_original_text_has_changed": "", @@ -2156,6 +2160,7 @@ "we_do_not_share_personal_information": "", "we_got_your_request": "", "we_logged_you_in": "", + "we_recommend_switching_to_the_new_editor_design_now_so_you_have_time_to_get_to_know_it": "", "we_sent_code": "", "we_sent_new_code": "", "we_will_charge_you_now_for_the_cost_of_your_additional_licenses_based_on_remaining_months": "", diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/try-new-editor-button.tsx b/services/web/frontend/js/features/editor-navigation-toolbar/try-new-editor-button.tsx index 8a835ab2a1..215d089d17 100644 --- a/services/web/frontend/js/features/editor-navigation-toolbar/try-new-editor-button.tsx +++ b/services/web/frontend/js/features/editor-navigation-toolbar/try-new-editor-button.tsx @@ -1,14 +1,23 @@ -import { useCallback } from 'react' +import { useCallback, useState } from 'react' import OLButton from '../../shared/components/ol/ol-button' import { useTranslation } from 'react-i18next' import { useSwitchEnableNewEditorState } from '../ide-redesign/hooks/use-switch-enable-new-editor-state' import MaterialIcon from '@/shared/components/material-icon' import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics' +import { useFeatureFlag } from '@/shared/context/split-test-context' +import OldEditorWarningTooltip from '../ide-redesign/components/old-editor-warning-tooltip' const TryNewEditorButton = () => { const { t } = useTranslation() const { loading, setEditorRedesignStatus } = useSwitchEnableNewEditorState() const { sendEvent } = useEditorAnalytics() + const isNewEditorOptOutStage = useFeatureFlag('editor-redesign-opt-out') + const [buttonElt, setButtonElt] = useState(null) + const buttonRef = useCallback((node: HTMLButtonElement) => { + if (node !== null) { + setButtonElt(node) + } + }, []) const onClick = useCallback(() => { sendEvent('switch-to-new-editor', { @@ -25,10 +34,14 @@ const TryNewEditorButton = () => { size="sm" variant="secondary" isLoading={loading} + ref={buttonRef} > - {t('try_the_new_editor_design')} + {isNewEditorOptOutStage + ? t('switch_to_new_look') + : t('try_the_new_editor_design')} + {isNewEditorOptOutStage && } ) } diff --git a/services/web/frontend/js/features/ide-react/components/modals/modals.tsx b/services/web/frontend/js/features/ide-react/components/modals/modals.tsx index b71447dad5..d8404f58ec 100644 --- a/services/web/frontend/js/features/ide-react/components/modals/modals.tsx +++ b/services/web/frontend/js/features/ide-react/components/modals/modals.tsx @@ -4,15 +4,25 @@ import { UnsavedDocs } from '@/features/ide-react/components/unsaved-docs/unsave import SystemMessages from '@/shared/components/system-messages' import NewEditorPromoModal from '@/features/ide-redesign/components/new-editor-promo-modal' import NewEditorIntroModal from '@/features/ide-redesign/components/new-editor-intro-modal' +import NewEditorOptOutIntroModal from '@/features/ide-redesign/components/new-editor-opt-out-intro-modal' +import { useFeatureFlag } from '@/shared/context/split-test-context' export const Modals = memo(() => { + const isNewEditorOptOutStage = useFeatureFlag('editor-redesign-opt-out') + return ( <> - - + {isNewEditorOptOutStage ? ( + + ) : ( + <> + + + + )} ) }) diff --git a/services/web/frontend/js/features/ide-redesign/components/new-editor-opt-out-intro-modal.tsx b/services/web/frontend/js/features/ide-redesign/components/new-editor-opt-out-intro-modal.tsx new file mode 100644 index 0000000000..3f8c0799d9 --- /dev/null +++ b/services/web/frontend/js/features/ide-redesign/components/new-editor-opt-out-intro-modal.tsx @@ -0,0 +1,81 @@ +import { + OLModal, + OLModalBody, + OLModalFooter, + OLModalHeader, + OLModalTitle, +} from '@/shared/components/ol/ol-modal' +import { useCallback, useEffect, useState } from 'react' +import useTutorial from '@/shared/hooks/promotions/use-tutorial' +import OLButton from '@/shared/components/ol/ol-button' +import { useTranslation } from 'react-i18next' +import { useEditorContext } from '@/shared/context/editor-context' +import { useIsNewToNewEditor } from '../utils/new-editor-utils' +import { useNewEditorTourContext } from '../contexts/new-editor-tour-context' +import promoVideo from './new-editor-promo-video.mp4' + +const TUTORIAL_KEY = 'new-editor-intro-2' + +export default function NewEditorOptOutIntroModal() { + const { inactiveTutorials } = useEditorContext() + const { + tryShowingPopup, + showPopup: showModal, + dismissTutorial, + completeTutorial, + clearPopup, + } = useTutorial(TUTORIAL_KEY, { + name: TUTORIAL_KEY, + }) + const { startTour } = useNewEditorTourContext() + + const { t } = useTranslation() + + const [hasShown, setHasShown] = useState(false) + const isNewToNewEditor = useIsNewToNewEditor() + + useEffect(() => { + if ( + isNewToNewEditor && + !hasShown && + !inactiveTutorials.includes(TUTORIAL_KEY) + ) { + tryShowingPopup('notification-prompt') + setHasShown(true) + } + }, [tryShowingPopup, inactiveTutorials, isNewToNewEditor, hasShown]) + + const startProductTour = useCallback(() => { + completeTutorial({ event: 'notification-click', action: 'complete' }) + startTour() + clearPopup() + }, [completeTutorial, startTour, clearPopup]) + + const closeModal = useCallback(() => { + dismissTutorial('notification-dismiss') + clearPopup() + }, [dismissTutorial, clearPopup]) + + return ( + + + {t('introducing_overleafs_new_look')} + + +
{t('the_new_and_improved_overleaf_editor_design')}
+ {/* eslint-disable-next-line jsx-a11y/media-has-caption */} + +
+ {t('weve_made_it_easier_to_find_and_use_the_tools_you_need_today')} +
+
+ + + {t('explore_what_s_new')} + + +
+ ) +} diff --git a/services/web/frontend/js/features/ide-redesign/components/old-editor-warning-tooltip.tsx b/services/web/frontend/js/features/ide-redesign/components/old-editor-warning-tooltip.tsx new file mode 100644 index 0000000000..b98ab10a43 --- /dev/null +++ b/services/web/frontend/js/features/ide-redesign/components/old-editor-warning-tooltip.tsx @@ -0,0 +1,82 @@ +import { Overlay, Popover } from 'react-bootstrap' +import Close from '@/shared/components/close' +import OLButton from '@/shared/components/ol/ol-button' +import { useTranslation } from 'react-i18next' +import useTutorial from '@/shared/hooks/promotions/use-tutorial' +import { useCallback, useEffect, useState } from 'react' +import { useSwitchEnableNewEditorState } from '../hooks/use-switch-enable-new-editor-state' +import { useEditorContext } from '@/shared/context/editor-context' +import { canUseNewEditor } from '../utils/new-editor-utils' + +const TUTORIAL_KEY = 'old-editor-warning-tooltip' + +export default function OldEditorWarningTooltip({ + target, +}: { + target: HTMLElement | null +}) { + const { inactiveTutorials } = useEditorContext() + const { t } = useTranslation() + const { loading, setEditorRedesignStatus } = useSwitchEnableNewEditorState() + + const { + tryShowingPopup, + showPopup, + dismissTutorial, + completeTutorial, + clearPopup, + } = useTutorial(TUTORIAL_KEY, { + name: TUTORIAL_KEY, + }) + const [hasShown, setHasShown] = useState(false) + const canShow = canUseNewEditor() + + useEffect(() => { + if (canShow && !hasShown && !inactiveTutorials.includes(TUTORIAL_KEY)) { + tryShowingPopup('notification-prompt') + setHasShown(true) + } + }, [tryShowingPopup, inactiveTutorials, hasShown, canShow]) + + const onSwitch = useCallback(() => { + completeTutorial({ event: 'notification-click', action: 'complete' }) + setEditorRedesignStatus(true) + }, [setEditorRedesignStatus, completeTutorial]) + + const closePopup = useCallback(() => { + dismissTutorial('notification-dismiss') + clearPopup() + }, [dismissTutorial, clearPopup]) + + if (!showPopup) { + return null + } + + return ( + + + + {t('support_for_the_old_editor_is_ending_soon')} + + + +
+ {t( + 'we_recommend_switching_to_the_new_editor_design_now_so_you_have_time_to_get_to_know_it' + )} +
+ + {t('switch_to_new_editor_design')} + +
+
+
+ ) +} diff --git a/services/web/frontend/js/features/ide-redesign/hooks/use-switch-enable-new-editor-state.ts b/services/web/frontend/js/features/ide-redesign/hooks/use-switch-enable-new-editor-state.ts index c32578591f..b8a909cbf8 100644 --- a/services/web/frontend/js/features/ide-redesign/hooks/use-switch-enable-new-editor-state.ts +++ b/services/web/frontend/js/features/ide-redesign/hooks/use-switch-enable-new-editor-state.ts @@ -1,4 +1,5 @@ import { postJSON } from '@/infrastructure/fetch-json' +import { useFeatureFlag } from '@/shared/context/split-test-context' import { useUserSettingsContext } from '@/shared/context/user-settings-context' import { useCallback, useState } from 'react' @@ -6,12 +7,20 @@ export const useSwitchEnableNewEditorState = () => { const [loading, setLoading] = useState(false) const [error, setError] = useState('') const { setUserSettings } = useUserSettingsContext() + const isNewEditorOptOutStage = useFeatureFlag('editor-redesign-opt-out') + const setEditorRedesignStatus = useCallback( (status: boolean): Promise => { setLoading(true) setError('') return new Promise((resolve, reject) => { - postJSON('/user/settings', { body: { enableNewEditor: status } }) + postJSON( + // Ensure that feature flag overrides are preserved in the request + `/user/settings?editor-redesign-opt-out=${isNewEditorOptOutStage ? 'enabled' : 'default'}`, + { + body: { enableNewEditor: status }, + } + ) .then(() => { setUserSettings(current => ({ ...current, @@ -28,7 +37,7 @@ export const useSwitchEnableNewEditorState = () => { }) }) }, - [setUserSettings] + [setUserSettings, isNewEditorOptOutStage] ) return { loading, error, setEditorRedesignStatus } } diff --git a/services/web/frontend/js/features/ide-redesign/utils/new-editor-utils.ts b/services/web/frontend/js/features/ide-redesign/utils/new-editor-utils.ts index c2b8c06235..87c5dcca70 100644 --- a/services/web/frontend/js/features/ide-redesign/utils/new-editor-utils.ts +++ b/services/web/frontend/js/features/ide-redesign/utils/new-editor-utils.ts @@ -70,3 +70,10 @@ export const useIsNewEditorEnabled = () => { const enabled = userSettings.enableNewEditor return hasAccess && enabled } + +export const useIsNewToNewEditor = () => { + const { userSettings } = useUserSettingsContext() + const newEditor = useIsNewEditorEnabled() + + return newEditor && !userSettings.enableNewEditorLegacy +} diff --git a/services/web/frontend/js/shared/context/user-settings-context.tsx b/services/web/frontend/js/shared/context/user-settings-context.tsx index 949bdac858..8f45074925 100644 --- a/services/web/frontend/js/shared/context/user-settings-context.tsx +++ b/services/web/frontend/js/shared/context/user-settings-context.tsx @@ -24,6 +24,7 @@ const defaultSettings: UserSettings = { mathPreview: true, referencesSearchMode: 'advanced', enableNewEditor: true, + enableNewEditorLegacy: true, breadcrumbs: true, darkModePdf: false, } diff --git a/services/web/frontend/stylesheets/pages/all.scss b/services/web/frontend/stylesheets/pages/all.scss index 7e8ea6459a..de358919ea 100644 --- a/services/web/frontend/stylesheets/pages/all.scss +++ b/services/web/frontend/stylesheets/pages/all.scss @@ -37,6 +37,7 @@ @import 'editor/editor-survey'; @import 'editor/editor-tour-tooltip'; @import 'editor/new-editor-promo-modal'; +@import 'editor/old-editor-warning-tooltip'; @import 'error-pages'; @import 'website-redesign'; @import 'group-settings'; diff --git a/services/web/frontend/stylesheets/pages/editor/old-editor-warning-tooltip.scss b/services/web/frontend/stylesheets/pages/editor/old-editor-warning-tooltip.scss new file mode 100644 index 0000000000..5bf00f4a64 --- /dev/null +++ b/services/web/frontend/stylesheets/pages/editor/old-editor-warning-tooltip.scss @@ -0,0 +1,15 @@ +.old-editor-warning-tooltip { + .popover-header { + display: flex; + } + + .popover-body { + display: flex; + flex-direction: column; + gap: var(--spacing-06); + } +} + +.old-editor-warning-tooltip-switch-button { + align-self: flex-end; +} diff --git a/services/web/locales/en.json b/services/web/locales/en.json index a32563b43b..f6d85e4de0 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -2271,6 +2271,7 @@ "suggestion_applied": "Suggestion applied", "suggests_code_completions_while_typing": "Suggests code completions while typing", "support": "Support", + "support_for_the_old_editor_is_ending_soon": "Support for the old editor is ending soon", "support_for_your_browser_is_ending_soon": "Support for your browser is ending soon", "supports_up_to_x_licenses": "Supports up to <0>__count__ licenses", "sure_you_want_to_cancel_plan_change": "Are you sure you want to revert your scheduled plan change? You will remain subscribed to the <0>__planName__ plan.", @@ -2282,6 +2283,8 @@ "switch_compile_mode_for_faster_draft_compilation": "Switch compile mode for faster draft compilation", "switch_easily_between_your_files_comments_track_changes_and_more": "Switch easily between your files, comments, track changes, and more in the new left-hand menu.", "switch_to_editor": "Switch to editor", + "switch_to_new_editor_design": "Switch to new editor design", + "switch_to_new_look": "Switch to new look", "switch_to_pdf": "Switch to PDF", "switch_to_personal_email_to_keep_your_accounts_separate": "Switch to a personal email to keep your accounts separate.", "switch_to_standard_plan": "Switch to Standard plan", @@ -2357,6 +2360,7 @@ "the_following_folder_already_exists_in_this_project": "The following folder already exists in this project:", "the_following_folder_already_exists_in_this_project_plural": "The following folders already exist in this project:", "the_latex_engine_used_for_compiling": "The LaTeX engine used for compiling", + "the_new_and_improved_overleaf_editor_design": "The new and improved __appName__ editor design brings you a cleaner, less cluttered interface to help you focus on what matters—your work.", "the_new_overleaf_editor_info": "__appName__’s new look is here. Disabling this option will switch you back to the old editor design.", "the_next_payment_will_be_collected_on": "The next payment will be collected on __date__.", "the_original_text_has_changed": "The original text has changed, so this suggestion can’t be applied", @@ -2703,6 +2707,7 @@ "we_got_your_request": "We’ve got your request", "we_logged_you_in": "We have logged you in.", "we_may_also_contact_you_from_time_to_time_by_email_with_a_survey": "<0>We may also contact you from time to time by email with a survey, or to see if you would like to participate in other user research initiatives", + "we_recommend_switching_to_the_new_editor_design_now_so_you_have_time_to_get_to_know_it": "We recommend switching to the new editor design now, so you have time to get to know it.", "we_sent_code": "We’ve sent you a confirmation code", "we_sent_new_code": "We’ve sent a new code. If it doesn’t arrive, make sure to check your spam and any promotions folders.", "we_will_charge_you_now_for_the_cost_of_your_additional_licenses_based_on_remaining_months": "We’ll charge you now for the cost of your additional licenses based on the remaining months of your current subscription.", diff --git a/services/web/test/unit/src/User/UserController.test.mjs b/services/web/test/unit/src/User/UserController.test.mjs index e035b5388e..48ab2c1b29 100644 --- a/services/web/test/unit/src/User/UserController.test.mjs +++ b/services/web/test/unit/src/User/UserController.test.mjs @@ -8,6 +8,14 @@ vi.mock('../../../../app/src/Features/Errors/Errors.js', () => { return vi.importActual('../../../../app/src/Features/Errors/Errors.js') }) +vi.mock('../../../../app/src/infrastructure/Metrics.js', () => ({ + default: { + analyticsQueue: { + inc: vi.fn(), + }, + }, +})) + describe('UserController', function () { beforeEach(async function (ctx) { ctx.user_id = '323123' @@ -143,6 +151,12 @@ describe('UserController', function () { }, } + ctx.SplitTestHandler = { + promises: { + getAssignment: sinon.stub().resolves({ variant: 'default' }), + }, + } + vi.doMock('../../../../app/src/Features/Helpers/UrlHelper', () => ({ default: ctx.UrlHelper, })) @@ -239,6 +253,13 @@ describe('UserController', function () { default: ctx.Modules, })) + vi.doMock( + '../../../../app/src/Features/SplitTests/SplitTestHandler.mjs', + () => ({ + default: ctx.SplitTestHandler, + }) + ) + ctx.UserController = (await import(modulePath)).default ctx.res = { @@ -565,36 +586,85 @@ describe('UserController', function () { }) }) - it('should set enableNewEditor to true', function (ctx) { - return new Promise(resolve => { - ctx.req.body = { enableNewEditor: true } - ctx.res.sendStatus = code => { - ctx.user.ace.enableNewEditor.should.equal(true) - resolve() - } - ctx.UserController.updateUserSettings(ctx.req, ctx.res) + describe('when editor-redesign-opt-out is set to default', function () { + beforeEach(function (ctx) { + ctx.SplitTestHandler.promises.getAssignment.resolves({ + variant: 'default', + }) + }) + + it('should set enableNewEditor to true', function (ctx) { + return new Promise(resolve => { + ctx.req.body = { enableNewEditor: true } + ctx.res.sendStatus = code => { + ctx.user.ace.enableNewEditor.should.equal(true) + resolve() + } + ctx.UserController.updateUserSettings(ctx.req, ctx.res) + }) + }) + + it('should set enableNewEditor to false', function (ctx) { + return new Promise(resolve => { + ctx.req.body = { enableNewEditor: false } + ctx.res.sendStatus = code => { + ctx.user.ace.enableNewEditor.should.equal(false) + resolve() + } + ctx.UserController.updateUserSettings(ctx.req, ctx.res) + }) + }) + + it('should keep enableNewEditor a boolean', function (ctx) { + return new Promise(resolve => { + ctx.req.body = { enableNewEditor: 'foobar' } + ctx.res.sendStatus = code => { + ctx.user.ace.enableNewEditor.should.equal(true) + resolve() + } + ctx.UserController.updateUserSettings(ctx.req, ctx.res) + }) }) }) - it('should set enableNewEditor to false', function (ctx) { - return new Promise(resolve => { - ctx.req.body = { enableNewEditor: false } - ctx.res.sendStatus = code => { - ctx.user.ace.enableNewEditor.should.equal(false) - resolve() - } - ctx.UserController.updateUserSettings(ctx.req, ctx.res) + describe('when editor-redesign-opt-out is set to enabled', function () { + beforeEach(function (ctx) { + ctx.SplitTestHandler.promises.getAssignment.resolves({ + variant: 'enabled', + }) }) - }) - it('should keep enableNewEditor a boolean', function (ctx) { - return new Promise(resolve => { - ctx.req.body = { enableNewEditor: 'foobar' } - ctx.res.sendStatus = code => { - ctx.user.ace.enableNewEditor.should.equal(true) - resolve() - } - ctx.UserController.updateUserSettings(ctx.req, ctx.res) + it('should set enableNewEditorStageFour to true', function (ctx) { + return new Promise(resolve => { + ctx.req.body = { enableNewEditor: true } + ctx.res.sendStatus = code => { + ctx.user.ace.enableNewEditorStageFour.should.equal(true) + resolve() + } + ctx.UserController.updateUserSettings(ctx.req, ctx.res) + }) + }) + + it('should set enableNewEditorStageFour to false', function (ctx) { + return new Promise(resolve => { + ctx.req.body = { enableNewEditor: false } + ctx.res.sendStatus = code => { + ctx.user.ace.enableNewEditorStageFour.should.equal(false) + resolve() + } + ctx.UserController.updateUserSettings(ctx.req, ctx.res) + }) + }) + + it('should keep enableNewEditorStageFour a boolean', function (ctx) { + return new Promise(resolve => { + ctx.req.body = { enableNewEditor: 'foobar' } + ctx.res.sendStatus = code => { + ctx.user.ace.enableNewEditorStageFour.should.equal(true) + resolve() + } + ctx.UserController.updateUserSettings(ctx.req, ctx.res) + }) }) }) diff --git a/services/web/types/user-settings.ts b/services/web/types/user-settings.ts index 7f3e411daf..9b0ee2f066 100644 --- a/services/web/types/user-settings.ts +++ b/services/web/types/user-settings.ts @@ -17,6 +17,7 @@ export type UserSettings = { mathPreview: boolean referencesSearchMode: 'advanced' | 'simple' enableNewEditor: boolean + enableNewEditorLegacy: boolean breadcrumbs: boolean darkModePdf: boolean }