mediawiki-skins-Vector/tests/jest/AB.test.js
Nicholas Ray d01dead5a7 Revise AB.js to handle other features + server sampling/bucketing
* Eliminates AB.js dependency on sticky header
* Code coverage has been raised to 100%
* Instead of importing ABTestConfig, these props are now passed into the
  function along with a token.
* WikimediaEvents hook is now fired when experiment is initialized. The
  experiment should not be initialized if it is not enabled.
* Removes several methods (e.g. initAB, getEnabledExperiment) due to the
  preceeding changes.
* Adds `isInSample` and `isInTreatmentBucket` methods so that the client
  has less work.

Treatment buckets now follow a naming convention so that the client can
do less work querying if the subject is part of the treatment:

* Treatment buckets should have the case-insensitive `treatment`
  substring somewhere in their name (e.g. 'treatment',
  'stickyHeaderTreatment', 'sticky-header-treatment' )

Bug: T302046
Change-Id: I4febec42b4c471b2f2ef02be2e334bd6d2c31eec
2022-03-22 11:58:48 -06:00

232 lines
6.9 KiB
JavaScript

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 = 'treatment';
const getBucketMock = jest.fn().mockReturnValue( bucket );
mw.experiments.getBucket = getBucketMock;
afterEach( () => {
document.body.removeAttribute( 'class' );
} );
describe( 'initialization when body tag does not contain 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)', () => {
getBucketMock.mockReturnValueOnce( 'control' );
createInstance();
expect( hookMock ).toHaveBeenCalled();
} );
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();
} );
} );
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 );
} );
} );
} );