Merge "Revise AB.js to handle other features + server sampling/bucketing"
This commit is contained in:
commit
b3f19854b6
|
@ -1,124 +1,186 @@
|
|||
/** @module WebABTest */
|
||||
|
||||
const EXCLUDED_BUCKET = 'unsampled';
|
||||
const TREATMENT_BUCKET_SUBSTRING = 'treatment';
|
||||
const WEB_AB_TEST_ENROLLMENT_HOOK = 'mediawiki.web_AB_test_enrollment';
|
||||
|
||||
/**
|
||||
* Example A/B test configuration for sticky header:
|
||||
*
|
||||
* $wgVectorABTestEnrollment = [
|
||||
* 'name' => 'vector.sticky_header',
|
||||
* 'enabled' => true,
|
||||
* 'buckets' => [
|
||||
* 'unsampled' => [
|
||||
* 'samplingRate' => 0.1,
|
||||
* ],
|
||||
* 'control' => [
|
||||
* 'samplingRate' => 0.3,
|
||||
* ],
|
||||
* 'stickyHeaderDisabled' => [
|
||||
* 'samplingRate' => 0.3,
|
||||
* ],
|
||||
* 'stickyHeaderEnabled' => [
|
||||
* 'samplingRate' => 0.3,
|
||||
* ],
|
||||
* ],
|
||||
* ];
|
||||
* @typedef {Object} SamplingRate
|
||||
* @property {number} samplingRate The desired sampling rate for the group in the range [0, 1].
|
||||
*/
|
||||
|
||||
/**
|
||||
* Functions and variables to implement A/B testing.
|
||||
* @typedef {Object} WebABTestProps
|
||||
* @property {string} name The name of the experiment.
|
||||
* @property {Object} buckets Dict with bucket name as key and SamplingRate
|
||||
* object as value. There must be an `unsampled` bucket that represents a
|
||||
* population excluded from the experiment. Additionally, the treatment
|
||||
* bucket(s) must include a case-insensitive `treatment` substring in their name
|
||||
* (e.g. `treatment`, `stickyHeaderTreatment`, `sticky-header-treatment`).
|
||||
* @property {string} [token] Token that uniquely identifies the subject for the
|
||||
* duration of the experiment. If bucketing/server occurs on the server and the
|
||||
* bucket is a class on the body tag, this can be ignored. Otherwise, it is
|
||||
* required.
|
||||
*/
|
||||
const ABTestConfig = require( /** @type {string} */ ( './config.json' ) ).wgVectorWebABTestEnrollment || {};
|
||||
|
||||
/**
|
||||
* Get the name of the bucket the user is assigned to for A/B testing.
|
||||
* Initializes an AB test experiment.
|
||||
*
|
||||
* @return {string} the name of the bucket the user is assigned.
|
||||
* Example props:
|
||||
*
|
||||
* webABTest({
|
||||
* name: 'nameOfExperiment',
|
||||
* buckets: {
|
||||
* unsampled: {
|
||||
* samplingRate: 0.5
|
||||
* },
|
||||
* control: {
|
||||
* samplingRate: 0.25
|
||||
* },
|
||||
* treatment: {
|
||||
* samplingRate: 0.25
|
||||
* }
|
||||
* },
|
||||
* token: 'token'
|
||||
* });
|
||||
*
|
||||
* @param {WebABTestProps} props
|
||||
* @return {WebABTest}
|
||||
*/
|
||||
function getBucketName() {
|
||||
module.exports = function webABTest( props ) {
|
||||
let /** @type {string} */ cachedBucket;
|
||||
|
||||
/**
|
||||
* Provided config should contain the keys:
|
||||
* name: the name of the experiment prefixed with the skin name.
|
||||
* enabled: must be true or all users are assigned to control.
|
||||
* buckets: dict with bucket name as key and test config as value.
|
||||
* Get the names of all the buckets from props.buckets.
|
||||
*
|
||||
* Bucket test config can contain the keys:
|
||||
* samplingRate: sampling rates will be summed up and each bucket will receive a proportion
|
||||
* equal to its value.
|
||||
* @return {string[]}
|
||||
*/
|
||||
return mw.experiments.getBucket( {
|
||||
name: ABTestConfig.name,
|
||||
enabled: ABTestConfig.enabled,
|
||||
buckets: {
|
||||
function getBucketNames() {
|
||||
return Object.keys( props.buckets );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the bucket from the class added to the body tag.
|
||||
*
|
||||
* @return {?string}
|
||||
*/
|
||||
function getBucketFromHTML() {
|
||||
for ( const bucketName of getBucketNames() ) {
|
||||
if ( document.body.classList.contains( `${props.name}-${bucketName}` ) ) {
|
||||
return bucketName;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the bucket the subject is assigned to for A/B testing.
|
||||
*
|
||||
* @throws {Error} Will throw an error if token is undefined and body tag has
|
||||
* not been assigned a bucket.
|
||||
* @return {string} the name of the bucket the subject is assigned.
|
||||
*/
|
||||
function getBucket() {
|
||||
if ( cachedBucket ) {
|
||||
// If we've already cached the bucket, return early.
|
||||
return cachedBucket;
|
||||
}
|
||||
|
||||
const bucketFromHTML = getBucketFromHTML();
|
||||
// If bucketing/sampling already occurred on the server, return that bucket
|
||||
// instead of trying to do it on the client.
|
||||
if ( bucketFromHTML ) {
|
||||
cachedBucket = bucketFromHTML;
|
||||
|
||||
return bucketFromHTML;
|
||||
}
|
||||
|
||||
if ( props.token === undefined ) {
|
||||
throw new Error( 'Tried to call `getBucket` with an undefined token' );
|
||||
}
|
||||
|
||||
cachedBucket = mw.experiments.getBucket( {
|
||||
name: props.name,
|
||||
enabled: true,
|
||||
// @ts-ignore
|
||||
unsampled: ABTestConfig.buckets.unsampled.samplingRate,
|
||||
control: ABTestConfig.buckets.control.samplingRate,
|
||||
stickyHeaderDisabled: ABTestConfig.buckets.stickyHeaderDisabled.samplingRate,
|
||||
stickyHeaderEnabled: ABTestConfig.buckets.stickyHeaderEnabled.samplingRate
|
||||
}
|
||||
}, mw.user.getId().toString() );
|
||||
}
|
||||
buckets:
|
||||
getBucketNames().reduce( ( buckets, key ) => {
|
||||
// @ts-ignore
|
||||
buckets[ key ] = props.buckets[ key ].samplingRate;
|
||||
|
||||
/**
|
||||
* Get the group and experiment name for an A/B test.
|
||||
*
|
||||
* @return {Object} data to pass to event logging
|
||||
*/
|
||||
function getABTestGroupExperimentName() {
|
||||
return buckets;
|
||||
}, {} )
|
||||
}, props.token );
|
||||
|
||||
return cachedBucket;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the subject is included in the experiment.
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
function isInSample() {
|
||||
return getBucket() !== EXCLUDED_BUCKET;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if subject is in a particular bucket.
|
||||
*
|
||||
* @param {string} targetBucket The target test bucket.
|
||||
* @return {boolean} Whether the subject is in the test bucket.
|
||||
*/
|
||||
function isInBucket( targetBucket ) {
|
||||
return getBucket() === targetBucket;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the subject has been bucketed in a treatment bucket as
|
||||
* defined by the bucket name containing the case-insensitive `treatment`
|
||||
* substring (e.g. 'treatment', 'sticky-header-treatment' and
|
||||
* 'stickyHeaderTreatment' are all assumed to be treatment buckets).
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
function isInTreatmentBucket() {
|
||||
const bucket = getBucket();
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return bucket.toLowerCase().includes( TREATMENT_BUCKET_SUBSTRING );
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize and fire hook to register A/B test enrollment if necessary.
|
||||
*/
|
||||
function init() {
|
||||
// Send data to WikimediaEvents to log A/B test initialization if the subject
|
||||
// has been sampled into the experiment.
|
||||
if ( isInSample() ) {
|
||||
// @ts-ignore
|
||||
mw.hook( WEB_AB_TEST_ENROLLMENT_HOOK ).fire( {
|
||||
group: getBucket(),
|
||||
experimentName: props.name
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
/**
|
||||
* @typedef {Object} WebABTest
|
||||
* @property {string} name
|
||||
* @property {getBucket} getBucket
|
||||
* @property {isInBucket} isInBucket
|
||||
* @property {isInSample} isInSample
|
||||
* @property {isInTreatmentBucket} isInTreatmentBucket
|
||||
*/
|
||||
return {
|
||||
group: getBucketName(),
|
||||
experimentName: ABTestConfig.name
|
||||
name: props.name,
|
||||
getBucket,
|
||||
isInBucket,
|
||||
isInSample,
|
||||
isInTreatmentBucket
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides A/B test config for the current user.
|
||||
*
|
||||
* @return {Object} A/B test config data
|
||||
*/
|
||||
function getEnabledExperiment() {
|
||||
const mergedConfig = {};
|
||||
|
||||
if ( ABTestConfig.enabled ) {
|
||||
// Merge all the A/B config to return.
|
||||
Object.assign( mergedConfig, getABTestGroupExperimentName(), ABTestConfig );
|
||||
}
|
||||
return mergedConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if user is in test group to experience feature.
|
||||
*
|
||||
* @param {string} bucket the bucket name the user is assigned
|
||||
* @param {string} targetGroup the target test group to experience feature
|
||||
* @return {boolean} true if the user should experience feature
|
||||
*/
|
||||
function isInTestGroup( bucket, targetGroup ) {
|
||||
return bucket === targetGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire hook to register A/B test enrollment.
|
||||
*
|
||||
* @param {string} bucket the bucket user is assigned to
|
||||
*/
|
||||
function initAB( bucket ) {
|
||||
// Send data to WikimediaEvents to log A/B test initialization if experiment is enabled
|
||||
// and if the user is logged in.
|
||||
if ( ABTestConfig.enabled && !mw.user.isAnon() && bucket !== 'unsampled' ) {
|
||||
// @ts-ignore
|
||||
mw.hook( 'mediawiki.web_AB_test_enrollment' ).fire( getABTestGroupExperimentName() );
|
||||
|
||||
// Remove class if present on the html element so that scroll padding isn't undesirably
|
||||
// applied to users who don't experience the new treatment.
|
||||
if ( bucket !== 'stickyHeaderEnabled' ) {
|
||||
document.documentElement.classList.remove( 'vector-sticky-header-enabled' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isInTestGroup,
|
||||
getEnabledExperiment,
|
||||
initAB,
|
||||
test: {
|
||||
getBucketName,
|
||||
getABTestGroupExperimentName
|
||||
}
|
||||
};
|
||||
|
|
|
@ -3,14 +3,15 @@ const
|
|||
searchToggle = require( './searchToggle.js' ),
|
||||
stickyHeader = require( './stickyHeader.js' ),
|
||||
scrollObserver = require( './scrollObserver.js' ),
|
||||
AB = require( './AB.js' ),
|
||||
initExperiment = require( './AB.js' ),
|
||||
initSectionObserver = require( './sectionObserver.js' ),
|
||||
initTableOfContents = require( './tableOfContents.js' ),
|
||||
deferUntilFrame = require( './deferUntilFrame.js' ),
|
||||
TOC_ID = 'mw-panel-toc',
|
||||
BODY_CONTENT_ID = 'bodyContent',
|
||||
HEADLINE_SELECTOR = '.mw-headline',
|
||||
TOC_SECTION_ID_PREFIX = 'toc-';
|
||||
TOC_SECTION_ID_PREFIX = 'toc-',
|
||||
ABTestConfig = require( /** @type {string} */ ( './config.json' ) ).wgVectorWebABTestEnrollment || {};
|
||||
|
||||
/**
|
||||
* @return {void}
|
||||
|
@ -23,24 +24,25 @@ const main = () => {
|
|||
searchToggle( searchToggleElement );
|
||||
}
|
||||
|
||||
// Get the A/B test config for sticky header if enabled.
|
||||
// If necessary, initialize experiment and fire the A/B test enrollment hook.
|
||||
const stickyHeaderExperiment =
|
||||
!!ABTestConfig.enabled &&
|
||||
ABTestConfig.name === stickyHeader.STICKY_HEADER_EXPERIMENT_NAME &&
|
||||
!mw.user.isAnon() &&
|
||||
stickyHeader.isStickyHeaderAllowed() &&
|
||||
initExperiment( Object.assign( {}, ABTestConfig, { token: mw.user.getId() } ) );
|
||||
|
||||
// Remove class if present on the html element so that scroll padding isn't undesirably
|
||||
// applied to users who don't experience the new treatment.
|
||||
if ( stickyHeaderExperiment && !stickyHeaderExperiment.isInTreatmentBucket() ) {
|
||||
document.documentElement.classList.remove( 'vector-sticky-header-enabled' );
|
||||
}
|
||||
|
||||
const
|
||||
FEATURE_TEST_GROUP = 'stickyHeaderEnabled',
|
||||
testConfig = AB.getEnabledExperiment(),
|
||||
stickyConfig = testConfig &&
|
||||
// @ts-ignore
|
||||
testConfig.experimentName === stickyHeader.STICKY_HEADER_EXPERIMENT_NAME ?
|
||||
testConfig : null,
|
||||
// Note that the default test group is set to experience the feature by default.
|
||||
// @ts-ignore
|
||||
testGroup = stickyConfig ? stickyConfig.group : FEATURE_TEST_GROUP,
|
||||
targetElement = stickyHeader.header,
|
||||
targetIntersection = stickyHeader.stickyIntersection,
|
||||
isStickyHeaderAllowed = stickyHeader.isStickyHeaderAllowed() &&
|
||||
testGroup !== 'unsampled' && AB.isInTestGroup( testGroup, FEATURE_TEST_GROUP );
|
||||
|
||||
// Fire the A/B test enrollment hook.
|
||||
AB.initAB( testGroup );
|
||||
isStickyHeaderAllowed = stickyHeaderExperiment ?
|
||||
stickyHeaderExperiment.isInTreatmentBucket() : stickyHeader.isStickyHeaderAllowed();
|
||||
|
||||
// Set up intersection observer for sticky header functionality and firing scroll event hooks
|
||||
// for event logging if AB test is enabled.
|
||||
|
|
|
@ -476,12 +476,12 @@
|
|||
"stickyHeaderDisabled": {
|
||||
"samplingRate": 0.3
|
||||
},
|
||||
"stickyHeaderEnabled": {
|
||||
"stickyHeaderEnabledTreatment": {
|
||||
"samplingRate": 0.3
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "An associative array of A/B test configs keyed by parameters noted in mediawiki.experiments.js."
|
||||
"description": "An associative array of A/B test configs keyed by parameters noted in mediawiki.experiments.js. There must be an `unsampled` bucket that represents a population excluded from the experiment. Additionally, the treatment bucket(s) must include a case-insensitive `treatment` substring in their name (e.g. `treatment`, `stickyHeaderTreatment`, `sticky-header-treatment`)"
|
||||
},
|
||||
"VectorDisableSidebarPersistence": {
|
||||
"value": false,
|
||||
|
|
|
@ -1,74 +1,231 @@
|
|||
const mockConfig = require( './__mocks__/config.json' );
|
||||
const ABTestConfig = mockConfig.wgVectorWebABTestEnrollment;
|
||||
|
||||
// Mock out virtual config.json file used in AB.js, before importing AB.js
|
||||
jest.mock( '../../resources/skins.vector.es6/config.json', () => {
|
||||
return mockConfig;
|
||||
}, { virtual: true } );
|
||||
const AB = require( '../../resources/skins.vector.es6/AB.js' );
|
||||
const NAME_OF_EXPERIMENT = 'name-of-experiment';
|
||||
const TOKEN = 'token';
|
||||
const MW_EXPERIMENT_PARAM = {
|
||||
name: NAME_OF_EXPERIMENT,
|
||||
enabled: true,
|
||||
buckets: {
|
||||
unsampled: 0.5,
|
||||
control: 0.25,
|
||||
treatment: 0.25
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line jsdoc/require-returns
|
||||
/**
|
||||
* @param {Object} props
|
||||
*/
|
||||
function createInstance( props = {} ) {
|
||||
const mergedProps = /** @type {AB.WebABTestProps} */ ( Object.assign( {
|
||||
name: NAME_OF_EXPERIMENT,
|
||||
buckets: {
|
||||
unsampled: {
|
||||
samplingRate: 0.5
|
||||
},
|
||||
control: {
|
||||
samplingRate: 0.25
|
||||
},
|
||||
treatment: {
|
||||
samplingRate: 0.25
|
||||
}
|
||||
},
|
||||
token: TOKEN
|
||||
}, props ) );
|
||||
|
||||
return AB( mergedProps );
|
||||
}
|
||||
|
||||
describe( 'AB.js', () => {
|
||||
const bucket = 'sampled';
|
||||
const userId = '1';
|
||||
const bucket = 'treatment';
|
||||
const getBucketMock = jest.fn().mockReturnValue( bucket );
|
||||
const toStringMock = jest.fn().mockReturnValue( userId );
|
||||
mw.experiments.getBucket = getBucketMock;
|
||||
// @ts-ignore
|
||||
mw.user.getId = () => ( { toString: toStringMock } );
|
||||
|
||||
const expectedABTestGroupExperimentName = {
|
||||
group: bucket,
|
||||
experimentName: ABTestConfig.name
|
||||
};
|
||||
afterEach( () => {
|
||||
document.body.removeAttribute( 'class' );
|
||||
} );
|
||||
|
||||
describe( 'getBucketName', () => {
|
||||
it( 'calls mw.experiments.getBucket with config data', () => {
|
||||
expect( AB.test.getBucketName() ).toBe( bucket );
|
||||
expect( getBucketMock ).toBeCalledWith( {
|
||||
name: ABTestConfig.name,
|
||||
enabled: ABTestConfig.enabled,
|
||||
buckets: {
|
||||
unsampled: ABTestConfig.buckets.unsampled.samplingRate,
|
||||
control: ABTestConfig.buckets.control.samplingRate,
|
||||
stickyHeaderDisabled: ABTestConfig.buckets.stickyHeaderDisabled.samplingRate,
|
||||
stickyHeaderEnabled: ABTestConfig.buckets.stickyHeaderEnabled.samplingRate
|
||||
}
|
||||
}, userId );
|
||||
expect( toStringMock ).toHaveBeenCalled();
|
||||
describe( 'initialization when body tag does not contain bucket', () => {
|
||||
let /** @type {jest.Mock} */ hookMock;
|
||||
|
||||
beforeEach( () => {
|
||||
hookMock = jest.fn().mockReturnValue( { fire: () => {} } );
|
||||
mw.hook = hookMock;
|
||||
} );
|
||||
} );
|
||||
describe( 'getABTestGroupExperimentName', () => {
|
||||
it( 'returns group and experiment name object', () => {
|
||||
expect( AB.test.getABTestGroupExperimentName() )
|
||||
.toEqual( expectedABTestGroupExperimentName );
|
||||
} );
|
||||
} );
|
||||
describe( 'getEnabledExperiment', () => {
|
||||
it( 'returns AB config data when enabled', () => {
|
||||
expect( AB.getEnabledExperiment() ).toEqual(
|
||||
Object.assign( {}, expectedABTestGroupExperimentName, ABTestConfig )
|
||||
);
|
||||
} );
|
||||
} );
|
||||
describe( 'initAB(', () => {
|
||||
const hookMock = jest.fn().mockReturnValue( { fire: () => {} } );
|
||||
const isAnonMock = jest.fn();
|
||||
mw.user.isAnon = isAnonMock;
|
||||
mw.hook = hookMock;
|
||||
it( 'sends data to WikimediaEvents when the AB test is enabled ', () => {
|
||||
isAnonMock.mockReturnValueOnce( false );
|
||||
AB.initAB( 'sampled' );
|
||||
|
||||
it( 'sends data to WikimediaEvents when the bucket is part of sample (e.g. control)', () => {
|
||||
getBucketMock.mockReturnValueOnce( 'control' );
|
||||
createInstance();
|
||||
expect( hookMock ).toHaveBeenCalled();
|
||||
} );
|
||||
it( 'doesnt send data to WikimediaEvents when the user is anon ', () => {
|
||||
isAnonMock.mockReturnValueOnce( true );
|
||||
AB.initAB( 'sampled' );
|
||||
it( 'sends data to WikimediaEvents when the bucket is part of sample (e.g. treatment)', () => {
|
||||
getBucketMock.mockReturnValueOnce( 'treatment' );
|
||||
createInstance();
|
||||
expect( hookMock ).toHaveBeenCalled();
|
||||
} );
|
||||
it( 'does not send data to WikimediaEvents when the bucket is unsampled ', () => {
|
||||
getBucketMock.mockReturnValueOnce( 'unsampled' );
|
||||
createInstance();
|
||||
expect( hookMock ).not.toHaveBeenCalled();
|
||||
} );
|
||||
it( 'doesnt send data to WikimediaEvents when the bucket is unsampled ', () => {
|
||||
isAnonMock.mockReturnValueOnce( false );
|
||||
AB.initAB( 'unsampled' );
|
||||
} );
|
||||
|
||||
describe( 'initialization when body tag contains bucket', () => {
|
||||
let /** @type {jest.Mock} */ hookMock;
|
||||
|
||||
beforeEach( () => {
|
||||
hookMock = jest.fn().mockReturnValue( { fire: () => {} } );
|
||||
mw.hook = hookMock;
|
||||
} );
|
||||
|
||||
it( 'sends data to WikimediaEvents when the bucket is part of sample (e.g. control)', () => {
|
||||
document.body.classList.add( 'name-of-experiment-control' );
|
||||
createInstance();
|
||||
expect( hookMock ).toHaveBeenCalled();
|
||||
} );
|
||||
it( 'sends data to WikimediaEvents when the bucket is part of sample (e.g. treatment)', () => {
|
||||
document.body.classList.add( 'name-of-experiment-treatment' );
|
||||
createInstance();
|
||||
expect( hookMock ).toHaveBeenCalled();
|
||||
} );
|
||||
it( 'does not send data to WikimediaEvents when the bucket is unsampled ', () => {
|
||||
document.body.classList.add( 'name-of-experiment-unsampled' );
|
||||
createInstance();
|
||||
expect( hookMock ).not.toHaveBeenCalled();
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'initialization when token is undefined', () => {
|
||||
it( 'throws an error', () => {
|
||||
expect( () => {
|
||||
createInstance( { token: undefined } );
|
||||
} ).toThrow( 'Tried to call `getBucket`' );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'getBucket when body tag does not contain AB class', () => {
|
||||
it( 'calls mw.experiments.getBucket with config data', () => {
|
||||
const experiment = createInstance();
|
||||
|
||||
expect( getBucketMock ).toBeCalledWith( MW_EXPERIMENT_PARAM, TOKEN );
|
||||
expect( experiment.getBucket() ).toBe( bucket );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'getBucket when body tag contains AB class that is in the sample', () => {
|
||||
it( 'returns the bucket on the body tag', () => {
|
||||
document.body.classList.add( 'name-of-experiment-control' );
|
||||
const experiment = createInstance();
|
||||
|
||||
expect( getBucketMock ).not.toHaveBeenCalled();
|
||||
expect( experiment.getBucket() ).toBe( 'control' );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'getBucket when body tag contains AB class that is not in the sample', () => {
|
||||
it( 'returns the bucket on the body tag', () => {
|
||||
document.body.classList.add( 'name-of-experiment-unsampled' );
|
||||
const experiment = createInstance();
|
||||
|
||||
expect( getBucketMock ).not.toHaveBeenCalled();
|
||||
expect( experiment.getBucket() ).toBe( 'unsampled' );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'isInBucket', () => {
|
||||
it( 'compares assigned bucket with passed in bucket', () => {
|
||||
const experiment = createInstance();
|
||||
|
||||
expect( experiment.isInBucket( 'treatment' ) ).toBe( true );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'isInTreatmentBucket when assigned to unsampled bucket (from server)', () => {
|
||||
it( 'returns false', () => {
|
||||
document.body.classList.add( 'name-of-experiment-unsampled' );
|
||||
const experiment = createInstance();
|
||||
|
||||
expect( experiment.isInTreatmentBucket() ).toBe( false );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'isInTreatmentBucket when assigned to control bucket (from server)', () => {
|
||||
it( 'returns false', () => {
|
||||
document.body.classList.add( 'name-of-experiment-control' );
|
||||
const experiment = createInstance();
|
||||
|
||||
expect( experiment.isInTreatmentBucket() ).toBe( false );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'isInTreatmentBucket when assigned to treatment bucket (from server)', () => {
|
||||
it( 'returns true', () => {
|
||||
document.body.classList.add( 'name-of-experiment-treatment' );
|
||||
const experiment = createInstance();
|
||||
|
||||
expect( experiment.isInTreatmentBucket() ).toBe( true );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'isInTreatmentBucket when assigned to unsampled bucket (from client)', () => {
|
||||
it( 'returns false', () => {
|
||||
getBucketMock.mockReturnValueOnce( 'unsampled' );
|
||||
const experiment = createInstance();
|
||||
|
||||
expect( experiment.isInTreatmentBucket() ).toBe( false );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'isInTreatmentBucket when assigned to control bucket (from client)', () => {
|
||||
it( 'returns false', () => {
|
||||
getBucketMock.mockReturnValueOnce( 'control' );
|
||||
const experiment = createInstance();
|
||||
|
||||
expect( experiment.isInTreatmentBucket() ).toBe( false );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'isInTreatmentBucket when assigned to treatment bucket (from client)', () => {
|
||||
it( 'returns true', () => {
|
||||
getBucketMock.mockReturnValueOnce( 'treatment' );
|
||||
const experiment = createInstance();
|
||||
|
||||
expect( experiment.isInTreatmentBucket() ).toBe( true );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'isInTreatmentBucket when assigned to treatment bucket (is case insensitive)', () => {
|
||||
it( 'returns true', () => {
|
||||
getBucketMock.mockReturnValueOnce( 'StickyHeaderVisibleTreatment' );
|
||||
const experiment = createInstance();
|
||||
|
||||
expect( experiment.isInTreatmentBucket() ).toBe( true );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'isInSample when in unsampled bucket', () => {
|
||||
it( 'returns false', () => {
|
||||
document.body.classList.add( 'name-of-experiment-unsampled' );
|
||||
const experiment = createInstance();
|
||||
|
||||
expect( experiment.isInSample() ).toBe( false );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'isInSample when in control bucket', () => {
|
||||
it( 'returns true', () => {
|
||||
document.body.classList.add( 'name-of-experiment-control' );
|
||||
const experiment = createInstance();
|
||||
|
||||
expect( experiment.isInSample() ).toBe( true );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'isInSample when in treatment bucket', () => {
|
||||
it( 'returns true', () => {
|
||||
document.body.classList.add( 'name-of-experiment-treatment' );
|
||||
const experiment = createInstance();
|
||||
|
||||
expect( experiment.isInSample() ).toBe( true );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
{
|
||||
"wgVectorWebABTestEnrollment": {
|
||||
"name": "vector.sticky_header",
|
||||
"enabled": true,
|
||||
"buckets": {
|
||||
"unsampled": {
|
||||
"samplingRate": 0.1
|
||||
},
|
||||
"control": {
|
||||
"samplingRate": 0.3
|
||||
},
|
||||
"stickyHeaderDisabled": {
|
||||
"samplingRate": 0.3
|
||||
},
|
||||
"stickyHeaderEnabled": {
|
||||
"samplingRate": 0.3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue