const hCaptchaUtils = require( 'ext.confirmEdit.hCaptcha/ext.confirmEdit.hCaptcha/utils.js' ); const hCaptchaOnLoadHandler = require( 'ext.confirmEdit.hCaptcha/ext.confirmEdit.hCaptcha/ve/ve.init.mw.HCaptchaOnLoadHandler.js' ); const hCaptchaConfig = require( 'ext.confirmEdit.hCaptcha/ext.confirmEdit.hCaptcha/config.json' ); QUnit.module.if( 'VisualEditor', mw.loader.getModuleNames().includes( 'ext.visualEditor.targetLoader' ), () => { QUnit.module( 'ext.confirmEdit.hCaptcha.ve.HCaptchaOnLoadHandler', QUnit.newMwEnvironment( { beforeEach() { this.loadHCaptcha = this.sandbox.stub( hCaptchaUtils, 'loadHCaptcha' ); mw.config.set( 'wgConfirmEditHCaptchaSiteKey', 'test-site-key' ); this.origVisualEditorSurface = ve.init.target.surface; ve.init.target.surface = {}; this.window = { hcaptcha: { render: this.sandbox.stub() } }; this.origSiteKey = hCaptchaConfig.HCaptchaSiteKey; this.origInvisibleMode = hCaptchaConfig.HCaptchaInvisibleMode; hCaptchaConfig.HCaptchaSiteKey = 'test-default-site-key'; hCaptchaConfig.HCaptchaInvisibleMode = false; // In a real environment, initPlugins.js does this for us. However, to avoid // side effects, we don't use that method of loading the code we are testing. // Therefore, run this ourselves. require( 'ext.confirmEdit.hCaptcha/ext.confirmEdit.hCaptcha/ve/ve.init.mw.HCaptcha.js' )(); }, afterEach() { this.loadHCaptcha.restore(); mw.config.set( 'wgConfirmEditHCaptchaSiteKey', '' ); ve.init.mw.HCaptchaOnLoadHandler.static.readyPromise = null; ve.init.target.surface = this.origVisualEditorSurface; hCaptchaConfig.HCaptchaSiteKey = this.origSiteKey; hCaptchaConfig.HCaptchaInvisibleMode = this.origInvisibleMode; } } ) ); QUnit.test( 'transact event in VisualEditor surface causes hCaptcha load once', function ( assert ) { mw.config.set( 'wgConfirmEditCaptchaNeededForGenericEdit', 'hcaptcha' ); this.loadHCaptcha.returns( Promise.resolve() ); const fakeDocument = new OO.EventEmitter(); // Mock the surface to allow the code we are testing to interact with // the fake VisualEditor editor document created above ve.init.target.surface = { getModel: () => ( { getDocument: () => fakeDocument } ) }; hCaptchaOnLoadHandler(); ve.init.mw.HCaptchaOnLoadHandler.static.onActivationComplete(); assert.true( this.loadHCaptcha.notCalled, 'loadHCaptcha is not called before transact event is fired' ); // Trigger the transact event multiple times so we can test loading hCaptcha only happens // once for all of these events fakeDocument.emit( 'transact' ); fakeDocument.emit( 'transact' ); fakeDocument.emit( 'transact' ); assert.true( this.loadHCaptcha.calledOnce, 'loadHCaptcha is called once after transact event is fired' ); assert.deepEqual( this.loadHCaptcha.firstCall.args, [ window, 'visualeditor', { render: 'explicit' } ], 'loadHCaptcha arguments are as expected' ); } ); QUnit.test.each( 'shouldRun correctly matches', { 'wgConfirmEditCaptchaNeededForGenericEdit is undefined': { configVariableValue: undefined, expected: false }, 'wgConfirmEditCaptchaNeededForGenericEdit is false': { configVariableValue: false, expected: false }, 'wgConfirmEditCaptchaNeededForGenericEdit is fancycaptcha': { configVariableValue: 'fancycaptcha', expected: false }, 'wgConfirmEditCaptchaNeededForGenericEdit is hcaptcha': { configVariableValue: 'hcaptcha', expected: true } }, ( assert, options ) => { mw.config.set( 'wgConfirmEditCaptchaNeededForGenericEdit', options.configVariableValue ); hCaptchaOnLoadHandler(); assert.deepEqual( ve.init.mw.HCaptchaOnLoadHandler.static.shouldRun(), options.expected, '::shouldRun returns expected value' ); } ); QUnit.test( 'renderHCaptcha is called when hCaptcha is not required for an edit', function ( assert ) { mw.config.set( 'wgConfirmEditCaptchaNeededForGenericEdit', 'fancycaptcha' ); hCaptchaOnLoadHandler(); return ve.init.mw.HCaptchaOnLoadHandler.static.renderHCaptcha( this.window ).then( () => { assert.deepEqual( this.loadHCaptcha.callCount, 0, 'loadHCaptcha is not called when hCaptcha is not required for an edit' ); }, () => assert.false( true, 'renderHCaptcha should not return a rejected promise' ) ); } ); /** * Common helper method used to set up the ve.init.target.saveDialog method. * * @param {this} self The `this` of the calling method * @return {void} */ function setupSaveDialog( self ) { const $qunitFixture = $( '#qunit-fixture' ); // Append a fake hCaptcha container to the DOM to test that is gets cleared out // These can exist if the hCaptcha widget has already been rendered through some // other method, like the API error handler or if the save dialog is closed and opened. const $fakeHCaptchaContainer = $( '
' ); $fakeHCaptchaContainer.addClass( 'ext-confirmEdit-visualEditor-hCaptchaContainer' ); $qunitFixture.append( $fakeHCaptchaContainer ); const $saveDialogFooter = $( '
' ); $saveDialogFooter.addClass( 've-ui-mwSaveDialog-foot' ); $qunitFixture.append( $saveDialogFooter ); // Mock the saveDialog to allow us to make it be the qunit test fixture element ve.init.target.saveDialog = { $element: $qunitFixture, updateSize: self.sandbox.stub() }; } /** * Performs assertions that are the same for any call to renderHCaptcha, * regardless if the `loadHCaptcha` call returns rejected or fulfilled * promise. * * @param {*} assert QUnit assert object * @param {this} self The `this` of the calling method * @param {boolean} invisibleMode Is hCaptcha in invisible mode * @return {void} */ function commonPostRenderHCaptchaAssertions( assert, self, invisibleMode ) { // Check loadHCaptcha was called assert.deepEqual( self.loadHCaptcha.callCount, 1, 'loadHCaptcha is called once' ); assert.deepEqual( self.loadHCaptcha.firstCall.args, [ window, 'visualeditor', { render: 'explicit' } ], 'loadHCaptcha is called with the correct arguments' ); // Check saveDialog.updateSize() was called to make the dialog not have a vertical scroll assert.true( ve.init.target.saveDialog.updateSize.callCount > 0, 've.init.target.saveDialog.updateSize should be called at least once' ); // Check that the DOM is as expected const $actualHCaptchaContainer = $( '.ext-confirmEdit-visualEditor-hCaptchaContainer' ); assert.deepEqual( $actualHCaptchaContainer.length, 1, 'Only one hCaptcha container should exist in the DOM' ); assert.deepEqual( $( '.ext-confirmEdit-visualEditor-hCaptchaWidgetContainer', $actualHCaptchaContainer ).length, 1, 'Only one hCaptcha widget container should exist in the DOM' ); assert.deepEqual( $( '.ext-confirmEdit-hcaptcha-privacy-policy', $actualHCaptchaContainer ).length, invisibleMode ? 1 : 0, 'hCaptcha privacy policy text should only be added in invisible mode' ); } QUnit.test.each( 'renderHCaptcha is called for successful render', { 'hCaptcha is in invisible mode': { invisibleMode: true }, 'hCaptcha is not in invisible mode': { invisibleMode: false } }, function ( assert, options ) { mw.config.set( 'wgConfirmEditCaptchaNeededForGenericEdit', 'hcaptcha' ); this.loadHCaptcha.returns( Promise.resolve() ); this.window.hcaptcha.render.returns( 'widget-id' ); hCaptchaConfig.HCaptchaInvisibleMode = options.invisibleMode; setupSaveDialog( this ); hCaptchaOnLoadHandler(); assert.deepEqual( ve.init.mw.HCaptchaOnLoadHandler.static.widgetId, null, 'widgetId property should be null before renderHCaptcha call' ); return ve.init.mw.HCaptchaOnLoadHandler.static.renderHCaptcha( this.window ).then( () => { commonPostRenderHCaptchaAssertions( assert, this, options.invisibleMode ); // Check that hcaptcha.render is called assert.deepEqual( this.window.hcaptcha.render.callCount, 1, 'window.hcaptcha.render is called once' ); const actualRenderCallArgs = this.window.hcaptcha.render.firstCall.args; // eslint-disable-next-line no-jquery/no-class-state const isFirstRenderCallArgTheContainer = $( actualRenderCallArgs[ 0 ] ).hasClass( 'ext-confirmEdit-visualEditor-hCaptchaWidgetContainer' ); assert.true( isFirstRenderCallArgTheContainer, 'window.hcaptcha.render was provided with the expected container' ); assert.deepEqual( actualRenderCallArgs[ 1 ], { sitekey: 'test-site-key' }, 'window.hcaptcha.render was provided with the expected configuration values' ); assert.deepEqual( ve.init.mw.HCaptchaOnLoadHandler.static.widgetId, 'widget-id', 'widgetId property should be set with the return value of hcaptcha.render' ); // Check there is no error message displayed const $actualHCaptchaContainer = $( '.ext-confirmEdit-visualEditor-hCaptchaContainer' ); const $hcaptchaErrorWidget = $( '.cdx-message--error', $actualHCaptchaContainer ); assert.deepEqual( $hcaptchaErrorWidget.length, 1, 'hCaptcha error message widget exists' ); assert.deepEqual( $hcaptchaErrorWidget.css( 'display' ), 'none', 'hCaptcha error message widget should be hidden' ); }, () => assert.true( false, 'renderHCaptcha should not return a rejected promise' ) ); } ); QUnit.test.each( 'renderHCaptcha is called and hCaptcha SDK fails to load', { 'hCaptcha is in invisible mode': { invisibleMode: true }, 'hCaptcha is not in invisible mode': { invisibleMode: false } }, function ( assert, options ) { mw.config.set( 'wgConfirmEditCaptchaNeededForGenericEdit', 'hcaptcha' ); this.loadHCaptcha.returns( Promise.reject( 'generic-error' ) ); hCaptchaConfig.HCaptchaInvisibleMode = options.invisibleMode; setupSaveDialog( this ); hCaptchaOnLoadHandler(); assert.deepEqual( ve.init.mw.HCaptchaOnLoadHandler.static.widgetId, null, 'widgetId property should be null before renderHCaptcha call' ); return ve.init.mw.HCaptchaOnLoadHandler.static.renderHCaptcha( this.window ).then( () => assert.true( false, 'renderHCaptcha should not return a fulfilled promise' ), () => { commonPostRenderHCaptchaAssertions( assert, this, options.invisibleMode ); // Check that hcaptcha.render is not called, as the SDK loading failed assert.true( this.window.hcaptcha.render.notCalled, 'window.hcaptcha.render is never called' ); assert.deepEqual( ve.init.mw.HCaptchaOnLoadHandler.static.widgetId, null, 'widgetId property should be null as window.hcaptcha.render was not called' ); // Check there is an error message displayed const $actualHCaptchaContainer = $( '.ext-confirmEdit-visualEditor-hCaptchaContainer' ); const $hcaptchaErrorWidget = $( '.cdx-message--error', $actualHCaptchaContainer ); assert.deepEqual( 1, $hcaptchaErrorWidget.length, 'hCaptcha error message widget exists' ); assert.notDeepEqual( $hcaptchaErrorWidget.css( 'display' ), 'none', 'hCaptcha error message widget should be visible' ); assert.deepEqual( '(hcaptcha-generic-error)', $hcaptchaErrorWidget.text(), 'hCaptcha error message widget has the error message' ); } ); } ); } );