mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-15 14:43:46 +01:00
Merge pull request #32244 from overleaf/ab-labs-in-feature-flags
[web] Merge Labs programme into the Feature flags system GitOrigin-RevId: db75e07bf3272becc11ef1eeda3850098b3daa9d
This commit is contained in:
committed by
Copybot
parent
6f3e0963b8
commit
671df33da3
@@ -0,0 +1,30 @@
|
||||
import { expect } from 'chai'
|
||||
import { render } from '@testing-library/react'
|
||||
|
||||
import { LabsDescription } from '@/shared/components/labs/labs-description'
|
||||
|
||||
describe('<LabsDescription />', function () {
|
||||
it('adds rel and target attributes to rendered links', function () {
|
||||
const { container } = render(
|
||||
<LabsDescription description="A [link](https://example.com)." />
|
||||
)
|
||||
|
||||
const link = container.querySelector('a')
|
||||
expect(link).to.not.equal(null)
|
||||
expect(link?.getAttribute('href')).to.equal('https://example.com')
|
||||
expect(link?.getAttribute('rel')).to.equal('noreferrer noopener')
|
||||
expect(link?.getAttribute('target')).to.equal('_BLANK')
|
||||
})
|
||||
|
||||
it('preserves href sanitization for unsafe links', function () {
|
||||
const { container } = render(
|
||||
<LabsDescription description="A [link](javascript:alert(1))." />
|
||||
)
|
||||
|
||||
const link = container.querySelector('a')
|
||||
expect(link).to.not.equal(null)
|
||||
expect(link?.getAttribute('href')).to.equal('')
|
||||
expect(link?.getAttribute('rel')).to.equal('noreferrer noopener')
|
||||
expect(link?.getAttribute('target')).to.equal('_BLANK')
|
||||
})
|
||||
})
|
||||
@@ -49,6 +49,12 @@ describe('ProjectHelper', function () {
|
||||
],
|
||||
}
|
||||
|
||||
ctx.SplitTestHandler = {
|
||||
promises: {
|
||||
getAssignmentForUser: vi.fn().mockResolvedValue({ variant: 'default' }),
|
||||
},
|
||||
}
|
||||
|
||||
vi.doMock('mongodb-legacy', () => ({
|
||||
default: { ObjectId },
|
||||
}))
|
||||
@@ -57,6 +63,13 @@ describe('ProjectHelper', function () {
|
||||
default: ctx.Settings,
|
||||
}))
|
||||
|
||||
vi.doMock(
|
||||
'../../../../app/src/Features/SplitTests/SplitTestHandler.mjs',
|
||||
() => ({
|
||||
default: ctx.SplitTestHandler,
|
||||
})
|
||||
)
|
||||
|
||||
ctx.ProjectHelper = (await import(MODULE_PATH)).default
|
||||
})
|
||||
|
||||
@@ -145,8 +158,8 @@ describe('ProjectHelper', function () {
|
||||
})
|
||||
|
||||
describe('getAllowedImagesForUser', function () {
|
||||
it('marks alpha only images as not allowed when the user is anonymous', function (ctx) {
|
||||
const images = ctx.ProjectHelper.getAllowedImagesForUser(null)
|
||||
it('marks alpha only images as not allowed when the user is anonymous', async function (ctx) {
|
||||
const images = await ctx.ProjectHelper.getAllowedImagesForUser(null)
|
||||
const imageNames = _mapToAllowed(images)
|
||||
expect(imageNames).to.deep.equal([
|
||||
{ imageName: 'texlive-full:2018.1', allowed: true },
|
||||
@@ -156,8 +169,8 @@ describe('ProjectHelper', function () {
|
||||
])
|
||||
})
|
||||
|
||||
it('marks monthly labs images as not allowed when the user is anonymous', function (ctx) {
|
||||
const images = ctx.ProjectHelper.getAllowedImagesForUser(null)
|
||||
it('marks monthly labs images as not allowed when the user is anonymous', async function (ctx) {
|
||||
const images = await ctx.ProjectHelper.getAllowedImagesForUser(null)
|
||||
const imageNames = _mapToAllowed(images)
|
||||
expect(imageNames).to.deep.equal([
|
||||
{ imageName: 'texlive-full:2018.1', allowed: true },
|
||||
@@ -167,8 +180,11 @@ describe('ProjectHelper', function () {
|
||||
])
|
||||
})
|
||||
|
||||
it('marks monthly labs images as allowed when the user is enrolled', function (ctx) {
|
||||
const images = ctx.ProjectHelper.getAllowedImagesForUser(ctx.user)
|
||||
it('marks monthly labs images as allowed when the user is enrolled', async function (ctx) {
|
||||
ctx.SplitTestHandler.promises.getAssignmentForUser.mockResolvedValue({
|
||||
variant: 'enabled',
|
||||
})
|
||||
const images = await ctx.ProjectHelper.getAllowedImagesForUser(ctx.user)
|
||||
const imageNames = _mapToAllowed(images)
|
||||
expect(imageNames).to.deep.equal([
|
||||
{ imageName: 'texlive-full:2018.1', allowed: true },
|
||||
@@ -178,19 +194,21 @@ describe('ProjectHelper', function () {
|
||||
])
|
||||
})
|
||||
|
||||
it('marks alpha only images as not allowed when when the user is not admin', function (ctx) {
|
||||
const images = ctx.ProjectHelper.getAllowedImagesForUser(ctx.user)
|
||||
it('marks alpha only images as not allowed when when the user is not admin', async function (ctx) {
|
||||
const images = await ctx.ProjectHelper.getAllowedImagesForUser(ctx.user)
|
||||
const imageNames = _mapToAllowed(images)
|
||||
expect(imageNames).to.deep.equal([
|
||||
{ imageName: 'texlive-full:2018.1', allowed: true },
|
||||
{ imageName: 'texlive-full:2019.1', allowed: true },
|
||||
{ imageName: 'texlive-full:2020.1', allowed: false },
|
||||
{ imageName: 'texlive-full:2021.1', allowed: true },
|
||||
{ imageName: 'texlive-full:2021.1', allowed: false },
|
||||
])
|
||||
})
|
||||
|
||||
it('returns all images when the user is admin', function (ctx) {
|
||||
const images = ctx.ProjectHelper.getAllowedImagesForUser(ctx.adminUser)
|
||||
it('returns all images when the user is admin', async function (ctx) {
|
||||
const images = await ctx.ProjectHelper.getAllowedImagesForUser(
|
||||
ctx.adminUser
|
||||
)
|
||||
const imageNames = _mapToAllowed(images)
|
||||
expect(imageNames).to.deep.equal([
|
||||
{ imageName: 'texlive-full:2018.1', allowed: true },
|
||||
|
||||
@@ -499,21 +499,322 @@ describe('SplitTestHandler', function () {
|
||||
|
||||
expect(assignment.variant).to.equal('variant-1')
|
||||
})
|
||||
})
|
||||
|
||||
it('should assign to default if userCount is undefined', async function (ctx) {
|
||||
describe('labs phase assignment (split test)', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.AnalyticsManager.getIdsFromSession.returns({
|
||||
userId: 'abc123abc123',
|
||||
})
|
||||
ctx.cachedSplitTests.set(
|
||||
'active-test',
|
||||
makeSplitTest('active-test', { userLimit: 100, userCount: undefined })
|
||||
'labs-experiment',
|
||||
makeSplitTest('labs-experiment', { phase: 'labs' })
|
||||
)
|
||||
})
|
||||
|
||||
it('should assign to variant when user is in labs program and has opted into the experiment', async function (ctx) {
|
||||
ctx.SplitTestUserGetter.promises.getUser.resolves({
|
||||
_id: new ObjectId('abc123abc123abc123abc123'),
|
||||
splitTests: {},
|
||||
labsProgram: true,
|
||||
labsExperiments: ['labs-experiment'],
|
||||
})
|
||||
|
||||
const assignment = await ctx.SplitTestHandler.promises.getAssignment(
|
||||
ctx.req,
|
||||
ctx.res,
|
||||
'active-test'
|
||||
'labs-experiment'
|
||||
)
|
||||
|
||||
expect(assignment.variant).to.equal('variant-1')
|
||||
})
|
||||
|
||||
it('should assign to default when user is in labs program but has not opted into the experiment', async function (ctx) {
|
||||
ctx.SplitTestUserGetter.promises.getUser.resolves({
|
||||
_id: new ObjectId('abc123abc123abc123abc123'),
|
||||
splitTests: {},
|
||||
labsProgram: true,
|
||||
labsExperiments: ['some-other-experiment'],
|
||||
})
|
||||
|
||||
const assignment = await ctx.SplitTestHandler.promises.getAssignment(
|
||||
ctx.req,
|
||||
ctx.res,
|
||||
'labs-experiment'
|
||||
)
|
||||
|
||||
expect(assignment.variant).to.equal('default')
|
||||
})
|
||||
|
||||
it('should assign to default when user is not in labs program', async function (ctx) {
|
||||
ctx.SplitTestUserGetter.promises.getUser.resolves({
|
||||
_id: new ObjectId('abc123abc123abc123abc123'),
|
||||
splitTests: {},
|
||||
labsProgram: false,
|
||||
labsExperiments: [],
|
||||
})
|
||||
|
||||
const assignment = await ctx.SplitTestHandler.promises.getAssignment(
|
||||
ctx.req,
|
||||
ctx.res,
|
||||
'labs-experiment'
|
||||
)
|
||||
|
||||
expect(assignment.variant).to.equal('default')
|
||||
})
|
||||
|
||||
it('should assign to default when user has no labsExperiments field', async function (ctx) {
|
||||
ctx.SplitTestUserGetter.promises.getUser.resolves({
|
||||
_id: new ObjectId('abc123abc123abc123abc123'),
|
||||
splitTests: {},
|
||||
labsProgram: true,
|
||||
})
|
||||
|
||||
const assignment = await ctx.SplitTestHandler.promises.getAssignment(
|
||||
ctx.req,
|
||||
ctx.res,
|
||||
'labs-experiment'
|
||||
)
|
||||
|
||||
expect(assignment.variant).to.equal('default')
|
||||
})
|
||||
|
||||
it('should assign to variant when under user limit', async function (ctx) {
|
||||
ctx.cachedSplitTests.set(
|
||||
'labs-experiment',
|
||||
makeSplitTest('labs-experiment', {
|
||||
phase: 'labs',
|
||||
userLimit: 10,
|
||||
userCount: 5,
|
||||
})
|
||||
)
|
||||
ctx.SplitTestUserGetter.promises.getUser.resolves({
|
||||
_id: new ObjectId('abc123abc123abc123abc123'),
|
||||
splitTests: {},
|
||||
labsProgram: true,
|
||||
labsExperiments: ['labs-experiment'],
|
||||
})
|
||||
|
||||
const assignment = await ctx.SplitTestHandler.promises.getAssignment(
|
||||
ctx.req,
|
||||
ctx.res,
|
||||
'labs-experiment'
|
||||
)
|
||||
|
||||
expect(assignment.variant).to.equal('variant-1')
|
||||
})
|
||||
|
||||
it('should assign enrolled user to variant even when user limit is reached', async function (ctx) {
|
||||
ctx.cachedSplitTests.set(
|
||||
'labs-experiment',
|
||||
makeSplitTest('labs-experiment', {
|
||||
phase: 'labs',
|
||||
userLimit: 10,
|
||||
userCount: 10,
|
||||
})
|
||||
)
|
||||
ctx.SplitTestUserGetter.promises.getUser.resolves({
|
||||
_id: new ObjectId('abc123abc123abc123abc123'),
|
||||
splitTests: {},
|
||||
labsProgram: true,
|
||||
labsExperiments: ['labs-experiment'],
|
||||
})
|
||||
|
||||
const assignment = await ctx.SplitTestHandler.promises.getAssignment(
|
||||
ctx.req,
|
||||
ctx.res,
|
||||
'labs-experiment'
|
||||
)
|
||||
|
||||
expect(assignment.variant).to.equal('variant-1')
|
||||
})
|
||||
|
||||
it('should allow already-assigned user even when limit is reached', async function (ctx) {
|
||||
ctx.cachedSplitTests.set(
|
||||
'labs-experiment',
|
||||
makeSplitTest('labs-experiment', {
|
||||
phase: 'labs',
|
||||
userLimit: 10,
|
||||
userCount: 10,
|
||||
})
|
||||
)
|
||||
ctx.SplitTestUserGetter.promises.getUser.resolves({
|
||||
_id: new ObjectId('abc123abc123abc123abc123'),
|
||||
splitTests: {
|
||||
'labs-experiment': [
|
||||
{
|
||||
variantName: 'variant-1',
|
||||
versionNumber: 1,
|
||||
assignedAt: new Date(),
|
||||
phase: 'labs',
|
||||
},
|
||||
],
|
||||
},
|
||||
labsProgram: true,
|
||||
labsExperiments: ['labs-experiment'],
|
||||
})
|
||||
|
||||
const assignment = await ctx.SplitTestHandler.promises.getAssignment(
|
||||
ctx.req,
|
||||
ctx.res,
|
||||
'labs-experiment'
|
||||
)
|
||||
|
||||
expect(assignment.variant).to.equal('variant-1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('labs phase assignment (gradual rollout)', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.AnalyticsManager.getIdsFromSession.returns({
|
||||
userId: 'abc123abc123',
|
||||
})
|
||||
ctx.cachedSplitTests.set(
|
||||
'labs-experiment-gr',
|
||||
makeSplitTest('labs-experiment-gr', {
|
||||
phase: 'labs',
|
||||
analyticsEnabled: false,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should assign to variant when user is in labs program and has opted in', async function (ctx) {
|
||||
ctx.SplitTestUserGetter.promises.getUser.resolves({
|
||||
_id: new ObjectId('abc123abc123abc123abc123'),
|
||||
splitTests: {},
|
||||
labsProgram: true,
|
||||
labsExperiments: ['labs-experiment-gr'],
|
||||
})
|
||||
|
||||
const assignment = await ctx.SplitTestHandler.promises.getAssignment(
|
||||
ctx.req,
|
||||
ctx.res,
|
||||
'labs-experiment-gr'
|
||||
)
|
||||
|
||||
expect(assignment.variant).to.equal('variant-1')
|
||||
})
|
||||
|
||||
it('should assign to default when user has not opted in', async function (ctx) {
|
||||
ctx.SplitTestUserGetter.promises.getUser.resolves({
|
||||
_id: new ObjectId('abc123abc123abc123abc123'),
|
||||
splitTests: {},
|
||||
labsProgram: true,
|
||||
labsExperiments: [],
|
||||
})
|
||||
|
||||
const assignment = await ctx.SplitTestHandler.promises.getAssignment(
|
||||
ctx.req,
|
||||
ctx.res,
|
||||
'labs-experiment-gr'
|
||||
)
|
||||
|
||||
expect(assignment.variant).to.equal('default')
|
||||
})
|
||||
|
||||
it('should assign to default when user is not in labs program', async function (ctx) {
|
||||
ctx.SplitTestUserGetter.promises.getUser.resolves({
|
||||
_id: new ObjectId('abc123abc123abc123abc123'),
|
||||
splitTests: {},
|
||||
labsProgram: false,
|
||||
labsExperiments: [],
|
||||
})
|
||||
|
||||
const assignment = await ctx.SplitTestHandler.promises.getAssignment(
|
||||
ctx.req,
|
||||
ctx.res,
|
||||
'labs-experiment-gr'
|
||||
)
|
||||
|
||||
expect(assignment.variant).to.equal('default')
|
||||
})
|
||||
|
||||
it('should assign to variant when under user limit', async function (ctx) {
|
||||
ctx.cachedSplitTests.set(
|
||||
'labs-experiment-gr',
|
||||
makeSplitTest('labs-experiment-gr', {
|
||||
phase: 'labs',
|
||||
analyticsEnabled: false,
|
||||
userLimit: 10,
|
||||
userCount: 5,
|
||||
})
|
||||
)
|
||||
ctx.SplitTestUserGetter.promises.getUser.resolves({
|
||||
_id: new ObjectId('abc123abc123abc123abc123'),
|
||||
splitTests: {},
|
||||
labsProgram: true,
|
||||
labsExperiments: ['labs-experiment-gr'],
|
||||
})
|
||||
|
||||
const assignment = await ctx.SplitTestHandler.promises.getAssignment(
|
||||
ctx.req,
|
||||
ctx.res,
|
||||
'labs-experiment-gr'
|
||||
)
|
||||
|
||||
expect(assignment.variant).to.equal('variant-1')
|
||||
})
|
||||
|
||||
it('should assign enrolled user to variant even when user limit is reached', async function (ctx) {
|
||||
ctx.cachedSplitTests.set(
|
||||
'labs-experiment-gr',
|
||||
makeSplitTest('labs-experiment-gr', {
|
||||
phase: 'labs',
|
||||
analyticsEnabled: false,
|
||||
userLimit: 10,
|
||||
userCount: 10,
|
||||
})
|
||||
)
|
||||
ctx.SplitTestUserGetter.promises.getUser.resolves({
|
||||
_id: new ObjectId('abc123abc123abc123abc123'),
|
||||
splitTests: {},
|
||||
labsProgram: true,
|
||||
labsExperiments: ['labs-experiment-gr'],
|
||||
})
|
||||
|
||||
const assignment = await ctx.SplitTestHandler.promises.getAssignment(
|
||||
ctx.req,
|
||||
ctx.res,
|
||||
'labs-experiment-gr'
|
||||
)
|
||||
|
||||
expect(assignment.variant).to.equal('variant-1')
|
||||
})
|
||||
|
||||
it('should allow already-assigned user even when limit is reached', async function (ctx) {
|
||||
ctx.cachedSplitTests.set(
|
||||
'labs-experiment-gr',
|
||||
makeSplitTest('labs-experiment-gr', {
|
||||
phase: 'labs',
|
||||
analyticsEnabled: false,
|
||||
userLimit: 10,
|
||||
userCount: 10,
|
||||
})
|
||||
)
|
||||
ctx.SplitTestUserGetter.promises.getUser.resolves({
|
||||
_id: new ObjectId('abc123abc123abc123abc123'),
|
||||
splitTests: {
|
||||
'labs-experiment-gr': [
|
||||
{
|
||||
variantName: 'variant-1',
|
||||
versionNumber: 1,
|
||||
assignedAt: new Date(),
|
||||
phase: 'labs',
|
||||
},
|
||||
],
|
||||
},
|
||||
labsProgram: true,
|
||||
labsExperiments: ['labs-experiment-gr'],
|
||||
})
|
||||
|
||||
const assignment = await ctx.SplitTestHandler.promises.getAssignment(
|
||||
ctx.req,
|
||||
ctx.res,
|
||||
'labs-experiment-gr'
|
||||
)
|
||||
|
||||
expect(assignment.variant).to.equal('variant-1')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -325,4 +325,52 @@ describe('SplitTestSessionHandler', function () {
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
describe('clearCachedVariant', function () {
|
||||
it('should remove all cached entries for a given split test name', function (ctx) {
|
||||
const session = {
|
||||
cachedSplitTestAssignments: {
|
||||
'my-test-1': 'variant-1',
|
||||
'my-test-2': 'variant-2',
|
||||
'other-test-1': 'variant-1',
|
||||
},
|
||||
}
|
||||
ctx.SplitTestSessionHandler.clearCachedVariant(session, 'my-test')
|
||||
expect(session.cachedSplitTestAssignments).to.deep.equal({
|
||||
'other-test-1': 'variant-1',
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle split test names with hyphens correctly', function (ctx) {
|
||||
const session = {
|
||||
cachedSplitTestAssignments: {
|
||||
'monthly-texlive-1': 'enabled',
|
||||
'monthly-texlive-2': 'enabled',
|
||||
'monthly-1': 'variant-1',
|
||||
},
|
||||
}
|
||||
ctx.SplitTestSessionHandler.clearCachedVariant(session, 'monthly-texlive')
|
||||
expect(session.cachedSplitTestAssignments).to.deep.equal({
|
||||
'monthly-1': 'variant-1',
|
||||
})
|
||||
})
|
||||
|
||||
it('should do nothing when session has no cached assignments', function (ctx) {
|
||||
const session = {}
|
||||
ctx.SplitTestSessionHandler.clearCachedVariant(session, 'my-test')
|
||||
expect(session.cachedSplitTestAssignments).to.be.undefined
|
||||
})
|
||||
|
||||
it('should do nothing when there are no matching entries', function (ctx) {
|
||||
const session = {
|
||||
cachedSplitTestAssignments: {
|
||||
'other-test-1': 'variant-1',
|
||||
},
|
||||
}
|
||||
ctx.SplitTestSessionHandler.clearCachedVariant(session, 'my-test')
|
||||
expect(session.cachedSplitTestAssignments).to.deep.equal({
|
||||
'other-test-1': 'variant-1',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user