diff --git a/jest.config.js b/jest.config.js index 0c2b47908..34d8d776d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -47,6 +47,10 @@ module.exports = { 'vue' ], + modulePathIgnorePatterns: [ + '/tests/integration-qunit/' + ], + // The paths to modules that run some code to configure or // set up the testing environment before each test setupFiles: [ diff --git a/package-lock.json b/package-lock.json index 078c94b5b..aafeabbdd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@wikimedia/codex": "0.20.0", "@wikimedia/codex-icons": "0.20.0", "@wikimedia/mw-node-qunit": "7.2.0", - "@wikimedia/types-wikimedia": "0.4.1", + "@wikimedia/types-wikimedia": "0.4.2", "eslint-config-wikimedia": "0.25.1", "eslint-plugin-no-jquery": "2.7.0", "grunt-banana-checker": "0.11.0", @@ -2562,9 +2562,9 @@ "dev": true }, "node_modules/@wikimedia/types-wikimedia": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@wikimedia/types-wikimedia/-/types-wikimedia-0.4.1.tgz", - "integrity": "sha512-RJUZXQNo+z7zBjcRSsVRR5GH2/R8/aSJc9Vo/ntnTzr8v1TjUlM3Dh4sP6XQODYKm/0HQn+hln3bWyeU6YTyRw==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@wikimedia/types-wikimedia/-/types-wikimedia-0.4.2.tgz", + "integrity": "sha512-MWZJE6JRUYRSuwajjiO4l7xz6530MUqdTOP0t0AteVm8Gqs+hUEcs5tTmAVJDg/ByvzyZ/M/KsW1UcuTRs0N8g==", "dev": true }, "node_modules/abab": { @@ -14469,9 +14469,9 @@ } }, "@wikimedia/types-wikimedia": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@wikimedia/types-wikimedia/-/types-wikimedia-0.4.1.tgz", - "integrity": "sha512-RJUZXQNo+z7zBjcRSsVRR5GH2/R8/aSJc9Vo/ntnTzr8v1TjUlM3Dh4sP6XQODYKm/0HQn+hln3bWyeU6YTyRw==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@wikimedia/types-wikimedia/-/types-wikimedia-0.4.2.tgz", + "integrity": "sha512-MWZJE6JRUYRSuwajjiO4l7xz6530MUqdTOP0t0AteVm8Gqs+hUEcs5tTmAVJDg/ByvzyZ/M/KsW1UcuTRs0N8g==", "dev": true }, "abab": { diff --git a/package.json b/package.json index c32abf2d6..1631746f3 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@wikimedia/codex": "0.20.0", "@wikimedia/codex-icons": "0.20.0", "@wikimedia/mw-node-qunit": "7.2.0", - "@wikimedia/types-wikimedia": "0.4.1", + "@wikimedia/types-wikimedia": "0.4.2", "eslint-config-wikimedia": "0.25.1", "eslint-plugin-no-jquery": "2.7.0", "grunt-banana-checker": "0.11.0", diff --git a/resources/skins.vector.clientPreferences/clientPreferences.js b/resources/skins.vector.clientPreferences/clientPreferences.js index 7b4a77415..85a9cb009 100644 --- a/resources/skins.vector.clientPreferences/clientPreferences.js +++ b/resources/skins.vector.clientPreferences/clientPreferences.js @@ -21,14 +21,40 @@ function getClientPreferences() { ).map( ( className ) => className.split( '-clientpref-' )[ 0 ] ); } +/** + * @param {string} featureName + * @param {string} value + */ +function toggleDocClassAndSave( featureName, value ) { + const pref = config[ featureName ]; + if ( mw.user.isNamed() ) { + // FIXME: Ideally this would be done in mw.user.clientprefs API. + // mw.user.clientPrefs.get is marked as being only stable for anonymous and temporary users. + // So instead we have to keep track of all the different possible values and remove them + // before adding the new class. + config[ featureName ].options.forEach( ( possibleValue ) => { + document.documentElement.classList.remove( `${featureName}-clientpref-${possibleValue}` ); + } ); + document.documentElement.classList.add( `${featureName}-clientpref-${value}` ); + // Ideally this should be taken care of via a single core helper function. + mw.util.debounce( function () { + api = api || new mw.Api(); + api.saveOption( pref.preferenceKey, value ); + }, 100 )(); + // END FIXME. + } else { + // This case is much simpler - the API transparently takes care of classes as well as storage. + mw.user.clientPrefs.set( featureName, value ); + } +} + /** * @param {Element} parent * @param {string} featureName - * @param {ClientPreference} pref * @param {string} value * @param {string} currentValue */ -function appendRadioToggle( parent, featureName, pref, value, currentValue ) { +function appendRadioToggle( parent, featureName, value, currentValue ) { const input = document.createElement( 'input' ); const name = `vector-client-pref-${featureName}-group`; const id = `vector-client-pref-${featureName}-value-${value}`; @@ -55,14 +81,7 @@ function appendRadioToggle( parent, featureName, pref, value, currentValue ) { container.appendChild( label ); parent.appendChild( container ); input.addEventListener( 'change', () => { - // @ts-ignore https://github.com/wikimedia/typescript-types/pull/44 - mw.user.clientPrefs.set( featureName, value ); - if ( mw.user.isNamed() ) { - mw.util.debounce( function () { - api = api || new mw.Api(); - api.saveOption( pref.preferenceKey, value ); - }, 100 )(); - } + toggleDocClassAndSave( featureName, value ); } ); } @@ -87,17 +106,16 @@ function makeClientPreferenceBinaryToggle( featureName ) { if ( !pref ) { return null; } - // @ts-ignore https://github.com/wikimedia/typescript-types/pull/44 const currentValue = mw.user.clientPrefs.get( featureName ); // The client preference was invalid. This shouldn't happen unless a gadget // or script has modified the documentElement. - if ( !currentValue ) { + if ( typeof currentValue === 'boolean' ) { return null; } const row = createRow( '' ); const form = document.createElement( 'form' ); pref.options.forEach( ( value ) => { - appendRadioToggle( form, featureName, pref, value, currentValue ); + appendRadioToggle( form, featureName, value, currentValue ); } ); row.appendChild( form ); return row; @@ -194,5 +212,6 @@ function bind( clickSelector, renderSelector ) { } module.exports = { bind, + toggleDocClassAndSave, render }; diff --git a/skin.json b/skin.json index ea787d309..469c35046 100644 --- a/skin.json +++ b/skin.json @@ -549,6 +549,14 @@ "+ext.uls.interface": "skinStyles/ext.uls.interface.less" } }, + "QUnitTestModule": { + "localBasePath": "", + "remoteExtPath": "Vector", + "dependencies": [ "skins.vector.clientPreferences" ], + "scripts": [ + "tests/integration-qunit/integration.test.js" + ] + }, "config": { "VectorClientPreferences": { "value": { diff --git a/tests/integration-qunit/integration.test.js b/tests/integration-qunit/integration.test.js new file mode 100644 index 000000000..d07fb37af --- /dev/null +++ b/tests/integration-qunit/integration.test.js @@ -0,0 +1,27 @@ +/* global QUnit */ +const clientPreferences = require( 'skins.vector.clientPreferences' ); + +/*! + * Vector integration tests. + * + * This should only be used to test APIs that Vector depends on to work. + * For unit tests please see tests/jest. + */ +QUnit.module( 'Vector (integration)', function () { + QUnit.test( 'Client preferences: Behaves same for all users', function ( assert ) { + const sandbox = this.sandbox; + const helper = ( feature, isNamedReturnValue ) => { + document.documentElement.setAttribute( 'class', `${feature}-clientpref-0` ); + const stub = sandbox.stub( mw.user, 'isNamed', () => isNamedReturnValue ); + clientPreferences.toggleDocClassAndSave( feature, '1' ); + stub.restore(); + return document.documentElement.getAttribute( 'class' ); + }; + + assert.strictEqual( + helper( 'vector-feature-limited-width', false ), + helper( 'vector-feature-limited-width', true ), + 'The same classes are modified regardless of the user status.' + ); + } ); +} ); diff --git a/tsconfig.json b/tsconfig.json index ffff977d4..d0e2c01e7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,8 @@ "jest.setup.js", "vendor", "coverage", - "tests/jest" + "tests/jest", + "tests/integration-qunit" ], "compilerOptions": { "resolveJsonModule": true,