'use strict'; /** * Spys on socket.io messages and saves them into several arrays * that are visible in tests */ helper.spyOnSocketIO = function () { helper.contentWindow().pad.socket.on('message', (msg) => { if (msg.type == 'COLLABROOM') { if (msg.data.type == 'ACCEPT_COMMIT') { helper.commits.push(msg); } else if (msg.data.type == 'USER_NEWINFO') { helper.userInfos.push(msg); } else if (msg.data.type == 'CHAT_MESSAGE') { helper.chatMessages.push(msg); } } }); }; /** * Makes an edit via `sendkeys` to the position of the caret and ensures ACCEPT_COMMIT * is returned by the server * It does not check if the ACCEPT_COMMIT is the edit sent, though * If `line` is not given, the edit goes to line no. 1 * * @param {string} message The edit to make - can be anything supported by `sendkeys` * @param {number} [line] the optional line to make the edit on starting from 1 * @returns {Promise} * @todo needs to support writing to a specified caret position * */ helper.edit = async function (message, line) { const editsNum = helper.commits.length; line = line ? line - 1 : 0; helper.linesDiv()[line].sendkeys(message); return helper.waitForPromise(() => editsNum + 1 === helper.commits.length); }; /** * The pad text as an array of divs * * @example * helper.linesDiv()[2].sendkeys('abc') // sends abc to the third line * * @returns {Array.} array of divs */ helper.linesDiv = function () { return helper.padInner$('.ace-line').map(function () { return $(this); }).get(); }; /** * The pad text as an array of lines * For lines in timeslider use `helper.timesliderTextLines()` * * @returns {Array.} lines of text */ helper.textLines = function () { return helper.linesDiv().map((div) => div.text()); }; /** * The default pad text transmitted via `clientVars` * * @returns {string} */ helper.defaultText = function () { return helper.padChrome$.window.clientVars.collab_client_vars.initialAttributedText.text; }; /** * Sends a chat `message` via `sendKeys` * You *must* include `{enter}` at the end of the string or it will * just fill the input field but not send the message. * * @todo Cannot send multiple messages at once * * @example * * `helper.sendChatMessage('hi{enter}')` * * @param {string} message the chat message to be sent * @returns {Promise} */ helper.sendChatMessage = function (message) { const noOfChatMessages = helper.chatMessages.length; helper.padChrome$('#chatinput').sendkeys(message); return helper.waitForPromise(() => noOfChatMessages + 1 === helper.chatMessages.length); }; /** * Opens the settings menu if its hidden via button * * @returns {Promise} */ helper.showSettings = function () { if (!helper.isSettingsShown()) { helper.settingsButton().click(); return helper.waitForPromise(() => helper.isSettingsShown(), 2000); } }; /** * Hide the settings menu if its open via button * * @returns {Promise} * @todo untested */ helper.hideSettings = function () { if (helper.isSettingsShown()) { helper.settingsButton().click(); return helper.waitForPromise(() => !helper.isSettingsShown(), 2000); } }; /** * Makes the chat window sticky via settings menu if the settings menu is * open and sticky button is not checked * * @returns {Promise} */ helper.enableStickyChatviaSettings = function () { const stickyChat = helper.padChrome$('#options-stickychat'); if (helper.isSettingsShown() && !stickyChat.is(':checked')) { stickyChat.click(); return helper.waitForPromise(() => helper.isChatboxSticky(), 2000); } }; /** * Unsticks the chat window via settings menu if the settings menu is open * and sticky button is checked * * @returns {Promise} */ helper.disableStickyChatviaSettings = function () { const stickyChat = helper.padChrome$('#options-stickychat'); if (helper.isSettingsShown() && stickyChat.is(':checked')) { stickyChat.click(); return helper.waitForPromise(() => !helper.isChatboxSticky(), 2000); } }; /** * Makes the chat window sticky via an icon on the top right of the chat * window * * @returns {Promise} */ helper.enableStickyChatviaIcon = function () { const stickyChat = helper.padChrome$('#titlesticky'); if (helper.isChatboxShown() && !helper.isChatboxSticky()) { stickyChat.click(); return helper.waitForPromise(() => helper.isChatboxSticky(), 2000); } }; /** * Disables the stickyness of the chat window via an icon on the * upper right * * @returns {Promise} */ helper.disableStickyChatviaIcon = function () { if (helper.isChatboxShown() && helper.isChatboxSticky()) { helper.titlecross().click(); return helper.waitForPromise(() => !helper.isChatboxSticky(), 2000); } }; /** * Sets the src-attribute of the main iframe to the timeslider * In case a revision is given, sets the timeslider to this specific revision. * Defaults to going to the last revision. * It waits until the timer is filled with date and time, because it's one of the * last things that happen during timeslider load * * @param {number} [revision] the optional revision * @returns {Promise} * @todo for some reason this does only work the first time, you cannot * goto rev 0 and then via the same method to rev 5. Use buttons instead */ helper.gotoTimeslider = function (revision) { revision = Number.isInteger(revision) ? `#${revision}` : ''; const iframe = $('#iframe-container iframe'); iframe.attr('src', `${iframe.attr('src')}/timeslider${revision}`); return helper.waitForPromise(() => helper.timesliderTimerTime() && !Number.isNaN(new Date(helper.timesliderTimerTime()).getTime()), 10000); }; /** * Clicks in the timeslider at a specific offset * It's used to navigate the timeslider * * @todo no mousemove test * @param {number} X coordinate */ helper.sliderClick = function (X) { const sliderBar = helper.sliderBar(); const edown = new jQuery.Event('mousedown'); const eup = new jQuery.Event('mouseup'); edown.clientX = eup.clientX = X; edown.clientY = eup.clientY = sliderBar.offset().top; sliderBar.trigger(edown); sliderBar.trigger(eup); }; /** * The timeslider text as an array of lines * * @returns {Array.} lines of text */ helper.timesliderTextLines = function () { return helper.contentWindow().$('.ace-line').map(function () { return $(this).text(); }).get(); }; helper.padIsEmpty = () => ( !helper.padInner$.document.getSelection().isCollapsed || (helper.padInner$('div').length === 1 && helper.padInner$('div').first().html() === '
')); helper.clearPad = async () => { if (helper.padIsEmpty()) return; const commitsBefore = helper.commits.length; const lines = helper.linesDiv(); helper.selectLines(lines[0], lines[lines.length - 1]); await helper.waitForPromise(() => !helper.padInner$.document.getSelection().isCollapsed); const e = new helper.padInner$.Event(helper.evtType); e.keyCode = 8; // delete key helper.padInner$('#innerdocbody').trigger(e); await helper.waitForPromise(helper.padIsEmpty); await helper.waitForPromise(() => helper.commits.length > commitsBefore); }; /** * Scrolls up in the editor * TODO: is `getSelection` always defined? */ helper.pageUp = async (opts) => { // the caret is in this node const caretNode = helper.padInner$.document.getSelection().anchorNode; const event = new helper.padInner$.Event(helper.evtType); event.which = 33; if (opts) { if (opts.shift) { event.shiftKey = true; } } if (opts && opts.pressAndHold) { let i = 0; while (i < 100) { // TODO: This triggers the same 100 times, not press and hold.. helper.padInner$('#innerdocbody').trigger(event); i++; } await helper.waitForPromise(() => ((helper.padInner$.document.getSelection().anchorNode !== caretNode) && (i === 100))); } else { helper.padInner$('#innerdocbody').trigger(event); // return as soon as the selection has changed await helper.waitForPromise(() => helper.padInner$.document.getSelection().anchorNode !== caretNode); } // return as soon as the selection has changed }; /** * Scrolls down in the editor * TODO: is `getSelection` always defined? */ helper.pageDown = async (opts) => { // the caret is in this node const caretNode = helper.padInner$.document.getSelection().anchorNode; const event = new helper.padInner$.Event(helper.evtType); event.which = 34; if (opts) { if (opts.shift) { event.shiftKey = true; } } if (opts && opts.pressAndHold) { let i = 0; while (i < 100) { // TODO: This triggers the same 100 times, not press and hold.. helper.padInner$('#innerdocbody').trigger(event); i++; } await helper.waitForPromise(() => ((helper.padInner$.document.getSelection().anchorNode !== caretNode) && (i === 100))); } else { helper.padInner$('#innerdocbody').trigger(event); // return as soon as the selection has changed await helper.waitForPromise(() => helper.padInner$.document.getSelection().anchorNode !== caretNode); } }; /** * Gets the line number where the caret is currently in * * @returns {number} line number */ helper.caretLineNumber = () => { if (helper.padInner$.document.getSelection()) { let caretNode = helper.padInner$.document.getSelection().anchorNode; const bodyElement = helper.padInner$('body')[0]; // a text node does not have a classList method if (caretNode.nodeType === 3) { caretNode = caretNode.parentNode; } // find the ace-line that contains the caret while (!caretNode.classList.contains('ace-line') && caretNode !== bodyElement) { caretNode = caretNode.parentNode; // a text node does not have a classList method if (caretNode.nodeType === 3) { caretNode = caretNode.parentNode; } } // find the index of that line in the array of all lines const lines = helper.linesDiv().map((line) => line[0]); return lines.indexOf(caretNode) + 1; } };