const CodeMirrorWikiEditor = require( '../../resources/codemirror.wikieditor.js' ); const { mediawiki } = require( '../../resources/modes/mediawiki/codemirror.mediawiki.js' ); const { javascript } = require( '../../resources/modes/codemirror.mode.exporter.js' ); function getCodeMirrorWikiEditor( readOnly = false, langSupport = [] ) { const form = document.createElement( 'form' ); const textarea = document.createElement( 'textarea' ); textarea.readOnly = readOnly; form.appendChild( textarea ); textarea.value = 'The Smashing Pumpkins'; textarea.selectionStart = textarea.selectionEnd = 0; const cmWe = new CodeMirrorWikiEditor( textarea, langSupport ); // Simulate the button that enables/disables CodeMirror as WikiEditor doesn't exist here. cmWe.$textarea.wikiEditor = jest.fn(); const toolbar = document.createElement( 'div' ); toolbar.className = 'wikiEditor-ui-toolbar'; const div = document.createElement( 'div' ); div.setAttribute( 'rel', 'CodeMirror' ); div.className = 'tool tool-element'; const btn = document.createElement( 'span' ); btn.className = 'tool cm-mw-toggle-wikieditor'; div.appendChild( btn ); toolbar.appendChild( div ); document.body.appendChild( toolbar ); // Add WikiEditor context to the textarea. cmWe.$textarea.data = jest.fn().mockReturnValue( { modules: { toolbar: { $toolbar: $( toolbar ) } }, $ui: $( '
' ) } ); return cmWe; } afterEach( () => { mw.hook.mockHooks = {}; jest.restoreAllMocks(); document.body.innerHTML = ''; } ); describe( 'initialize', () => { it( 'should add the button to the toolbar', () => { const cmWe = getCodeMirrorWikiEditor(); cmWe.initialize(); expect( cmWe.$textarea.wikiEditor ).toHaveBeenCalledWith( 'addToToolbar', expect.objectContaining( { groups: { codemirror: expect.any( Object ) } } ) ); } ); it( 'should add .ext-codemirror-readonly for readonly pages', () => { const cmWe = getCodeMirrorWikiEditor( true ); cmWe.initialize(); expect( cmWe.context.$ui[ 0 ].classList ).toContain( 'ext-codemirror-readonly' ); } ); it( 'should add a CSS class for the CodeMirror mode', () => { const cmWe = getCodeMirrorWikiEditor( false, javascript() ); cmWe.initialize(); expect( cmWe.context.$ui[ 0 ].classList ).toContain( 'ext-codemirror-javascript' ); // Reset mock. mockMwConfigGet(); } ); } ); describe( 'deactivate', () => { it( 'should remove the button from the toolbar', () => { const cmWe = getCodeMirrorWikiEditor(); cmWe.initialize(); cmWe.deactivate(); expect( cmWe.$textarea.wikiEditor ).toHaveBeenCalledWith( 'removeFromToolbar', expect.objectContaining( { section: 'advanced', group: 'codemirror' } ) ); } ); it( 'should remove buttons from the toolbar for non-wikitext', () => { const cmWe = getCodeMirrorWikiEditor( false, javascript() ); cmWe.initialize(); cmWe.deactivate(); expect( cmWe.$textarea.wikiEditor ).toHaveBeenCalledWith( 'removeFromToolbar', expect.objectContaining( { section: 'secondary', group: 'codemirror' } ) ); expect( cmWe.$textarea.wikiEditor ).toHaveBeenCalledWith( 'removeFromToolbar', expect.objectContaining( { section: 'main', group: 'codemirror-format' } ) ); expect( cmWe.$textarea.wikiEditor ).toHaveBeenCalledWith( 'removeFromToolbar', expect.objectContaining( { section: 'main', group: 'codemirror-preferences' } ) ); expect( cmWe.$textarea.wikiEditor ).toHaveBeenCalledWith( 'removeFromToolbar', expect.objectContaining( { section: 'main', group: 'codemirror-search' } ) ); // Reset mock. mockMwConfigGet(); } ); } ); describe( 'destroy', () => { it( 'should remove the button from the toolbar', () => { const cmWe = getCodeMirrorWikiEditor(); cmWe.initialize(); cmWe.destroy(); expect( cmWe.$textarea.wikiEditor ).toHaveBeenCalledWith( 'removeFromToolbar', expect.objectContaining( { section: 'main', group: 'codemirror' } ) ); } ); it( 'should remove .ext-codemirror-readonly on read-only pages', () => { const cmWe = getCodeMirrorWikiEditor( true ); cmWe.initialize(); expect( cmWe.context.$ui[ 0 ].classList ).toContain( 'ext-codemirror-readonly' ); cmWe.destroy(); expect( cmWe.context.$ui[ 0 ].classList ).not.toContain( 'ext-codemirror-readonly' ); } ); } ); describe( 'Hook handlers and event listeners', () => { it( 'should remove submit event listener when CodeMirror is off', () => { const cmWe = getCodeMirrorWikiEditor(); cmWe.initialize(); expect( cmWe.view ).not.toBeNull(); expect( cmWe.state ).not.toBeNull(); expect( mw.hook.mockHooks[ 'ext.WikiEditor.realtimepreview.enable' ].length ).toBe( 1 ); expect( mw.hook.mockHooks[ 'ext.WikiEditor.realtimepreview.disable' ].length ).toBe( 1 ); cmWe.toggle(); expect( mw.hook.mockHooks[ 'ext.WikiEditor.realtimepreview.enable' ].length ).toBe( 0 ); expect( mw.hook.mockHooks[ 'ext.WikiEditor.realtimepreview.disable' ].length ).toBe( 0 ); } ); it( 'should remove realtime preview hook handler when CodeMirror is off', () => { const cmWe = getCodeMirrorWikiEditor(); cmWe.initialize(); expect( cmWe.view ).not.toBeNull(); expect( cmWe.state ).not.toBeNull(); expect( mw.hook.mockHooks[ 'ext.WikiEditor.realtimepreview.enable' ].length ).toBe( 1 ); expect( mw.hook.mockHooks[ 'ext.WikiEditor.realtimepreview.disable' ].length ).toBe( 1 ); cmWe.toggle(); expect( mw.hook.mockHooks[ 'ext.WikiEditor.realtimepreview.enable' ].length ).toBe( 0 ); expect( mw.hook.mockHooks[ 'ext.WikiEditor.realtimepreview.disable' ].length ).toBe( 0 ); } ); it( 'should fire ext.CodeMirror.switch only once per toggle', () => { let count = 0; mw.hook( 'ext.CodeMirror.switch' ).add( () => { count++; } ); const cmWe = getCodeMirrorWikiEditor(); cmWe.initialize(); expect( mw.hook.mockHooks[ 'ext.CodeMirror.switch' ].length ).toBe( 1 ); expect( count ).toBe( 1 ); cmWe.toggle(); expect( mw.hook.mockHooks[ 'ext.CodeMirror.switch' ].length ).toBe( 1 ); expect( count ).toBe( 2 ); } ); it( 'only 1 ext.CodeMirror.ready hook handler', () => { [ ...Array( 3 ) ].forEach( () => mediawiki() ); expect( mw.hook.mockHooks[ 'ext.CodeMirror.ready' ].length ).toBe( 1 ); } ); } ); describe( 'logEditFeature', () => { it( 'should log when activating and deactivating', () => { const cmWe = getCodeMirrorWikiEditor(); const spy = jest.spyOn( cmWe, 'logEditFeature' ); cmWe.initialize(); expect( spy ).toHaveBeenCalledTimes( 1 ); expect( spy ).toHaveBeenCalledWith( 'activated' ); expect( mw.track ).toBeCalledWith( 'visualEditorFeatureUse', { action: 'activated', feature: 'codemirror', // eslint-disable-next-line camelcase editor_interface: 'wikitext', platform: 'desktop', integration: 'page' } ); cmWe.deactivate(); expect( spy ).toHaveBeenCalledTimes( 2 ); expect( spy ).toHaveBeenCalledWith( 'deactivated' ); expect( mw.track ).toBeCalledWith( 'visualEditorFeatureUse', { action: 'deactivated', feature: 'codemirror', // eslint-disable-next-line camelcase editor_interface: 'wikitext', platform: 'desktop', integration: 'page' } ); } ); it( 'should show a Preferences button in the advanced section of the toolbar', () => { const cmWe = getCodeMirrorWikiEditor(); cmWe.initialize(); expect( cmWe.$textarea.wikiEditor ).toHaveBeenCalledWith( 'addToToolbar', expect.objectContaining( { section: 'advanced', groups: { codemirror: { tools: { CodeMirrorPreferences: { label: 'codemirror-keymap-preferences', oouiIcon: 'settings', type: 'button', action: { execute: expect.any( Function ), type: 'callback' } } } } } } ) ); } ); it( 'should only log when preferences are not the same as the default', () => { const cmWe = getCodeMirrorWikiEditor(); cmWe.initialize(); const defaultPrefs = mw.config.get( 'extCodeMirrorConfig' ).defaultPreferences; // FIXME: remove after linting is stable delete defaultPrefs.lint; mw.user.options.get = jest.fn().mockReturnValue( JSON.stringify( defaultPrefs ) ); // Force re-fetch of user preferences. cmWe.preferences.preferences = cmWe.preferences.fetchPreferences(); const spy = jest.spyOn( cmWe, 'logEditFeature' ); cmWe.preferences.registerExtension( 'bracketMatching', [], cmWe.view ); // There should be no logging since we're using the default preferences. expect( spy ).not.toHaveBeenCalledWith( 'prefs-bracketMatching' ); // Enable activeLine and assert that it was logged. cmWe.preferences.setPreference( 'activeLine', true ); expect( spy ).toHaveBeenCalledWith( 'prefs-activeLine' ); expect( mw.track ).toBeCalledWith( 'visualEditorFeatureUse', { action: 'prefs-activeLine', feature: 'codemirror', // eslint-disable-next-line camelcase editor_interface: 'wikitext', platform: 'desktop', integration: 'page' } ); // Re-call the extension getter to verify *all* enabled extensions are logged. // eslint-disable-next-line no-unused-expressions cmWe.preferences.extension; expect( spy ).toHaveBeenNthCalledWith( 2, 'prefs-autofocus' ); expect( spy ).toHaveBeenNthCalledWith( 3, 'prefs-bracketMatching' ); expect( spy ).toHaveBeenNthCalledWith( 4, 'prefs-closeBrackets' ); expect( spy ).toHaveBeenNthCalledWith( 5, 'prefs-lineNumbering' ); expect( spy ).toHaveBeenNthCalledWith( 6, 'prefs-lineWrapping' ); expect( spy ).toHaveBeenNthCalledWith( 7, 'prefs-activeLine' ); expect( spy ).toHaveBeenNthCalledWith( 8, 'prefs-specialChars' ); // Other extensions are not used here because we aren't using the mediawiki language. } ); it( 'should log when opening the preferences panel', () => { const cmWe = getCodeMirrorWikiEditor(); cmWe.initialize(); const spy = jest.spyOn( cmWe, 'logEditFeature' ); cmWe.preferences.toggle( cmWe.view, true ); expect( spy ).toHaveBeenCalledWith( 'prefs-display' ); } ); it( 'should log when opening the search panel', () => { const cmWe = getCodeMirrorWikiEditor(); cmWe.initialize(); const spy = jest.spyOn( cmWe, 'logEditFeature' ); cmWe.view.contentDOM.dispatchEvent( new KeyboardEvent( 'keydown', { key: 'f', ctrlKey: true } ) ); expect( spy ).toHaveBeenCalledWith( 'search' ); expect( mw.track ).toBeCalledWith( 'visualEditorFeatureUse', { action: 'search', feature: 'codemirror', // eslint-disable-next-line camelcase editor_interface: 'wikitext', platform: 'desktop', integration: 'page' } ); } ); } ); describe( 'addCodeFormattingButtonsToToolbar', () => { it( 'should add the expected tool groups and buttons for non-wikitext', () => { const cmWe = getCodeMirrorWikiEditor( false, javascript() ); cmWe.initialize(); expect( cmWe.$textarea.wikiEditor ).toHaveBeenCalledWith( 'addToToolbar', expect.objectContaining( { groups: { 'codemirror-format': { tools: { indentMore: expect.any( Object ), indentLess: expect.any( Object ) } }, 'codemirror-preferences': { tools: { whitespace: expect.any( Object ), lineWrapping: expect.any( Object ), autocomplete: expect.any( Object ) } }, 'codemirror-search': { tools: { gotoLine: expect.any( Object ), search: expect.any( Object ) } } } } ) ); mockMwConfigGet(); } ); } ); describe( 'getTool / getToggleTool', () => { it( 'getTool', () => { const command = jest.fn(); const cmWe = getCodeMirrorWikiEditor(); expect( cmWe.getTool( 'fooBar', command ) ).toStrictEqual( { label: 'codemirror-keymap-foobar', type: 'button', oouiIcon: 'fooBar', action: { type: 'callback', execute: command } } ); expect( cmWe.getTool( 'fooBar', command, 'FOOBAR', 'customIcon' ) ).toStrictEqual( { label: 'FOOBAR', type: 'button', oouiIcon: 'customIcon', action: { type: 'callback', execute: command } } ); } ); it( 'getToggleTool', () => { const cmWe = getCodeMirrorWikiEditor(); expect( cmWe.getToggleTool( 'fooBar', 'fooBarIcon' ) ).toMatchObject( { label: 'codemirror-prefs-foobar', type: 'element', element: expect.any( Function ) } ); } ); } );