Merge pull request #31609 from overleaf/mj-split-test-system-overall

[web] Add split test for defaulting to system theme

GitOrigin-RevId: 3f5301349074725c56f423ed51662064e52d6aeb
This commit is contained in:
Mathias Jakobsen
2026-03-06 09:06:38 +00:00
committed by Copybot
parent eca31afb4a
commit 6c1003a852
3 changed files with 194 additions and 2 deletions
@@ -30,6 +30,33 @@ async function getEnableNewEditorLegacyDefault(req, res, user) {
return false
}
const SYSTEM_THEME_USER_CUTOFF_DATE = new Date(Date.UTC(2026, 1, 15, 12, 0, 0)) // 12pm GMT on February 15, 2026
async function getOverallTheme(req, res, user) {
if (user.ace.overallTheme != null) {
return user.ace.overallTheme
}
if (user.signUpDate < SYSTEM_THEME_USER_CUTOFF_DATE) {
// default / dark
return ''
}
const systemOverallSplitTestAssignment =
await SplitTestHandler.promises.getAssignment(
req,
res,
'new-user-system-overall-theme'
)
if (systemOverallSplitTestAssignment.variant === 'system') {
return 'system'
}
// default / dark
return ''
}
async function buildUserSettings(req, res, user) {
const defaultLegacyEnableNewEditor = await getEnableNewEditorLegacyDefault(
req,
@@ -53,7 +80,7 @@ async function buildUserSettings(req, res, user) {
syntaxValidation: user.ace.syntaxValidation,
fontFamily: user.ace.fontFamily || 'lucida',
lineHeight: user.ace.lineHeight || 'normal',
overallTheme: user.ace.overallTheme,
overallTheme: await getOverallTheme(req, res, user),
mathPreview: user.ace.mathPreview,
breadcrumbs: user.ace.breadcrumbs,
referencesSearchMode: user.ace.referencesSearchMode,
+1 -1
View File
@@ -77,7 +77,7 @@ export const UserSchema = new Schema(
ace: {
mode: { type: String, default: 'none' },
theme: { type: String, default: 'textmate' },
overallTheme: { type: String, default: '' },
overallTheme: { type: String },
// When overallTheme is `system`, we switch between `lightTheme` and `darkTheme` based on system settings
// When overallTheme is `light-` or empty, we use the `theme` option.
lightTheme: { type: String, default: 'textmate' },
@@ -0,0 +1,165 @@
import { vi, expect } from 'vitest'
import sinon from 'sinon'
const modulePath = '../../../../app/src/Features/Project/UserSettingsHelper.mjs'
describe('UserSettingsHelper', function () {
beforeEach(async function (ctx) {
ctx.SplitTestHandler = {
promises: { getAssignment: sinon.stub() },
}
vi.doMock(
'../../../../app/src/Features/SplitTests/SplitTestHandler.mjs',
() => ({
default: ctx.SplitTestHandler,
})
)
ctx.req = { query: {} }
ctx.res = {}
ctx.UserSettingsHelper = (await import(modulePath)).default
})
describe('for user with overall theme set to value', function () {
beforeEach(async function (ctx) {
const user = {
ace: {
overallTheme: 'light',
},
signUpDate: new Date('2022-01-01'),
}
ctx.settings = await ctx.UserSettingsHelper.buildUserSettings(
ctx.req,
ctx.res,
user
)
})
it('should return the user settings with the correct overall theme', function (ctx) {
expect(ctx.settings.overallTheme).toBe('light')
})
it('should not check split test', function (ctx) {
expect(ctx.SplitTestHandler.promises.getAssignment).not.toHaveBeenCalled
})
})
describe('for user with no overall theme set', function () {
describe('for new users in treatment group', function () {
beforeEach(async function (ctx) {
const user = {
ace: {},
signUpDate: new Date('2026-02-16T00:00:00Z'),
}
ctx.SplitTestHandler.promises.getAssignment
.withArgs(ctx.req, ctx.res, 'new-user-system-overall-theme')
.resolves({
variant: 'system',
})
ctx.settings = await ctx.UserSettingsHelper.buildUserSettings(
ctx.req,
ctx.res,
user
)
})
it('should default to system theme', function (ctx) {
expect(ctx.settings.overallTheme).toBe('system')
})
it('should check split test', function (ctx) {
expect(ctx.SplitTestHandler.promises.getAssignment).toHaveBeenCalled
})
})
describe('for new users in control group', function () {
beforeEach(async function (ctx) {
const user = {
ace: {},
signUpDate: new Date('2026-02-16T00:00:00Z'),
}
ctx.SplitTestHandler.promises.getAssignment
.withArgs(ctx.req, ctx.res, 'new-user-system-overall-theme')
.resolves({
variant: 'default',
})
ctx.settings = await ctx.UserSettingsHelper.buildUserSettings(
ctx.req,
ctx.res,
user
)
})
it('should default to dark theme', function (ctx) {
expect(ctx.settings.overallTheme).toBe('')
})
it('should check split test', function (ctx) {
expect(ctx.SplitTestHandler.promises.getAssignment).toHaveBeenCalled
})
})
describe('for old users in control group', function () {
beforeEach(async function (ctx) {
const user = {
ace: {},
signUpDate: new Date('2025-02-15T00:00:00Z'),
}
ctx.SplitTestHandler.promises.getAssignment
.withArgs(ctx.req, ctx.res, 'new-user-system-overall-theme')
.resolves({
variant: 'default',
})
ctx.settings = await ctx.UserSettingsHelper.buildUserSettings(
ctx.req,
ctx.res,
user
)
})
it('should default to dark theme', function (ctx) {
expect(ctx.settings.overallTheme).toBe('')
})
it('should not check split test', function (ctx) {
expect(ctx.SplitTestHandler.promises.getAssignment).not.toHaveBeenCalled
})
})
describe('for old users in treatment group', function () {
beforeEach(async function (ctx) {
const user = {
ace: {},
signUpDate: new Date('2025-02-15T00:00:00Z'),
}
ctx.SplitTestHandler.promises.getAssignment
.withArgs(ctx.req, ctx.res, 'new-user-system-overall-theme')
.resolves({
variant: 'system',
})
ctx.settings = await ctx.UserSettingsHelper.buildUserSettings(
ctx.req,
ctx.res,
user
)
})
it('should default to dark theme', function (ctx) {
expect(ctx.settings.overallTheme).toBe('')
})
it('should not check split test', function (ctx) {
expect(ctx.SplitTestHandler.promises.getAssignment).not.toHaveBeenCalled
})
})
})
})