Setup jest unit tests and add basic test cases for AB.js and App.vue

Bug: T300561
Change-Id: Ib7c314b094bd823ae233374f63c9094724d6c06f
This commit is contained in:
bwang 2022-01-26 16:10:35 -06:00 committed by Jdlrobson
parent 44e6289f8d
commit 66359e8fa5
15 changed files with 13373 additions and 926 deletions

View File

@ -3,3 +3,4 @@
/i18n/
/node_modules/
/vendor/
/coverage/

1
.gitignore vendored
View File

@ -22,6 +22,7 @@ sftp-config.json
/docs
/node_modules
/vendor
/coverage
# Operating systems
## Mac OS X

View File

@ -3,3 +3,4 @@
/node_modules/
/skinStyles/jquery.ui/
/vendor/
/coverage/

63
jest.config.js Normal file
View File

@ -0,0 +1,63 @@
// For a detailed explanation regarding each configuration property, visit:
// https://jestjs.io/docs/en/configuration.html
module.exports = {
moduleNameMapper: {
},
// Automatically clear mock calls and instances between every test
clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test
collectCoverage: true,
// An array of glob patterns indicating a set of files fo
// which coverage information should be collected
collectCoverageFrom: [
'resources/**/*.(js|vue)'
],
// The directory where Jest should output its coverage files
coverageDirectory: 'coverage',
// An array of regexp pattern strings used to skip coverage collection
coveragePathIgnorePatterns: [
'/node_modules/'
],
// An object that configures minimum threshold enforcement for coverage results
coverageThreshold: {
global: {
branches: 0,
functions: 0,
lines: 0,
statements: 0
}
},
// A set of global variables that need to be available in all test environments
globals: {
'vue-jest': {
babelConfig: false,
hideStyleWarn: true,
experimentalCSSCompile: true
}
},
// An array of file extensions your modules use
moduleFileExtensions: [
'js',
'json',
'vue'
],
// The paths to modules that run some code to configure or
// set up the testing environment before each test
setupFiles: [
'./jest.setup.js'
],
transform: {
'.*\\.(vue)$': '<rootDir>/node_modules/vue-jest'
}
};

4
jest.setup.js Normal file
View File

@ -0,0 +1,4 @@
/* eslint-disable */
// @ts-nocheck
var mockMediaWiki = require( '@wikimedia/mw-node-qunit/src/mockMediaWiki.js' );
global.mw = mockMediaWiki();

14027
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,8 @@
"private": true,
"scripts": {
"start": "bash dev-scripts/setup-storybook.sh && start-storybook --quiet -p 6006 -s resources/skins.vector.styles",
"test": "npm -s run lint && tsc && npm -s run doc",
"test": "npm -s run lint && tsc && npm run test:unit && npm -s run doc",
"test:unit": "jest --silent",
"lint": "npm -s run lint:js && npm -s run lint:styles && npm -s run lint:i18n",
"lint:fix:js": "npm -s run lint:js -- --fix",
"lint:fix:styles": "npm -s run lint:styles -- --fix",
@ -18,14 +19,18 @@
"devDependencies": {
"@babel/core": "7.7.7",
"@storybook/html": "5.2.8",
"@types/jest": "26.0.24",
"@types/jquery": "3.3.33",
"@types/mustache": "4.0.1",
"@types/node-fetch": "2.5.7",
"@vue/test-utils": "1.1.0",
"@wikimedia/mw-node-qunit": "6.3.0",
"@wikimedia/types-wikimedia": "0.2.0",
"@wikimedia/wvui": "0.3.0",
"@wikimedia/wvui": "0.3.5",
"babel-loader": "8.0.6",
"eslint-config-wikimedia": "0.20.0",
"grunt-banana-checker": "0.9.0",
"jest": "26.4.2",
"jsdoc": "3.6.7",
"jsdoc-wmf-theme": "0.0.3",
"less": "3.8.1",
@ -36,6 +41,7 @@
"stylelint-config-wikimedia": "0.11.1",
"svgo": "2.3.1",
"typescript": "4.5.5",
"vue": "2.6.11"
"vue": "2.6.11",
"vue-jest": "3.0.7"
}
}

View File

@ -116,5 +116,9 @@ function initAB( bucket ) {
module.exports = {
isInTestGroup,
getEnabledExperiment,
initAB
initAB,
test: {
getBucketName,
getABTestGroupExperimentName
}
};

View File

@ -1,9 +1,17 @@
{
"root": true,
"extends": [
"../.eslintrc.json"
],
"parserOptions": {
"ecmaVersion": 2017
},
"rules": {
"es/no-object-assign": "off"
},
"env": {
"es6": true,
"node": true
"node": true,
"jest": true
}
}

74
tests/jest/AB.test.js Normal file
View File

@ -0,0 +1,74 @@
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' );
describe( 'AB.js', () => {
const bucket = 'sampled';
const userId = '1';
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
};
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( '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' );
expect( hookMock ).toHaveBeenCalled();
} );
it( 'doesnt send data to WikimediaEvents when the user is anon ', () => {
isAnonMock.mockReturnValueOnce( true );
AB.initAB( 'sampled' );
expect( hookMock ).not.toHaveBeenCalled();
} );
it( 'doesnt send data to WikimediaEvents when the bucket is unsampled ', () => {
isAnonMock.mockReturnValueOnce( false );
AB.initAB( 'unsampled' );
expect( hookMock ).not.toHaveBeenCalled();
} );
} );
} );

35
tests/jest/App.test.js Normal file
View File

@ -0,0 +1,35 @@
const Vue = require( 'vue' );
const VueTestUtils = require( '@vue/test-utils' );
const App = require( '../../resources/skins.vector.search/App.vue' );
// @ts-ignore
Vue.directive( 'i18n-html', () => {} );
const defaultProps = {
id: 'searchform',
searchAccessKey: 'f',
searchTitle: 'search',
searchPlaceholder: 'Search MediaWiki',
searchQuery: ''
};
const mount = ( /** @type {Object} */ customProps ) => {
// @ts-ignore
return VueTestUtils.shallowMount( App, {
propsData: Object.assign( {}, defaultProps, customProps ),
mocks: {
$i18n: ( /** @type {string} */ str ) => ( {
text: () => str
} )
}
} );
};
describe( 'App', () => {
it( 'renders a typeahead search component', () => {
const wrapper = mount();
expect(
wrapper.element
).toMatchSnapshot();
} );
} );

View File

@ -0,0 +1,20 @@
{
"wgVectorWebABTestEnrollment": {
"name": "vector.sticky_header",
"enabled": true,
"buckets": {
"unsampled": {
"samplingRate": 0.1
},
"control": {
"samplingRate": 0.3
},
"stickyHeaderDisabled": {
"samplingRate": 0.3
},
"stickyHeaderEnabled": {
"samplingRate": 0.3
}
}
}
}

View File

@ -0,0 +1,4 @@
// Instead of mocking wvui, we ensure it matches the wvui-search module
// i.e. https://github.com/wikimedia/mediawiki/blob/master/resources/src/wvui/wvui-search.js
// @ts-ignore
module.exports = require( '@wikimedia/wvui/dist/commonjs2/wvui-search.commonjs2.js' ).default;

View File

@ -0,0 +1,35 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`App renders a typeahead search component 1`] = `
<wvui-typeahead-search-stub
accesskey="f"
aria-label="Search MediaWiki"
buttonlabel="searchbutton"
client="[object Object]"
domain="localhost"
formaction=""
highlightquery="true"
id="searchform"
initialinputvalue=""
placeholder="Search MediaWiki"
searchpagetitle="Special:Search"
showdescription="true"
showthumbnail="true"
suggestionslabel="searchresults"
title="search"
urlgenerator="[object Object]"
>
<input
name="title"
type="hidden"
value="Special:Search"
/>
<input
name="wprov"
type="hidden"
value="acrw1"
/>
<span />
</wvui-typeahead-search-stub>
`;

View File

@ -1,5 +1,9 @@
{
"exclude": [ "docs", "vendor" ],
"exclude": [
"docs",
"vendor",
"coverage"
],
"compilerOptions": {
"resolveJsonModule": true,
"esModuleInterop": true,