diff --git a/tests/frontend/helper.js b/tests/frontend/helper.js index 32b3c6bc6..d867cc1d6 100644 --- a/tests/frontend/helper.js +++ b/tests/frontend/helper.js @@ -145,6 +145,26 @@ var helper = {}; helper.padOuter$.fx.off = true; helper.padInner$.fx.off = true; + /* + * chat messages received + * @type {Array} + */ + helper.chatMessages = []; + + /* + * changeset commits from the server + * @type {Array} + */ + helper.commits = []; + + /* + * userInfo messages from the server + * @type {Array} + */ + helper.userInfos = []; + + // listen for server messages + helper.spyOnSocketIO(); opts.cb(); }).fail(function(){ if (helper.retry > 3) { @@ -200,6 +220,8 @@ var helper = {}; /** * Same as `waitFor` but using Promises * + * @returns {Promise} + * */ helper.waitForPromise = async function(...args) { // Note: waitFor() has a strange API: On timeout it rejects, but it also throws an uncatchable diff --git a/tests/frontend/helper/methods.js b/tests/frontend/helper/methods.js new file mode 100644 index 000000000..e524253e3 --- /dev/null +++ b/tests/frontend/helper/methods.js @@ -0,0 +1,229 @@ +/** + * 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', function(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){ + let editsNum = helper.commits.length; + line = line ? line - 1 : 0; + helper.linesDiv()[line].sendkeys(message); + return helper.waitForPromise(function(){ + return 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){ + let noOfChatMessages = helper.chatMessages.length; + helper.padChrome$("#chatinput").sendkeys(message) + return helper.waitForPromise(function(){ + return 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(function(){return 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(function(){return !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() { + var stickyChat = helper.padChrome$('#options-stickychat'); + if(helper.isSettingsShown() && !stickyChat.is(':checked')) { + stickyChat.click(); + return helper.waitForPromise(function(){ + return 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() { + var stickyChat = helper.padChrome$('#options-stickychat'); + if(helper.isSettingsShown() && stickyChat.is(':checked')) { + stickyChat.click(); + return helper.waitForPromise(function(){return !helper.isChatboxSticky()},2000); + } +} + +/** + * Makes the chat window sticky via an icon on the top right of the chat + * window + * + * @returns {Promise} + */ +helper.enableStickyChatviaIcon = function() { + var stickyChat = helper.padChrome$('#titlesticky'); + if(helper.isChatboxShown() && !helper.isChatboxSticky()) { + stickyChat.click(); + return helper.waitForPromise(function(){return 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(function(){return !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 : ''; + var iframe = $('#iframe-container iframe'); + iframe.attr('src', iframe.attr('src')+'/timeslider' + revision); + + return helper.waitForPromise(function(){ + return helper.timesliderTimerTime() + && !Number.isNaN(new Date(helper.timesliderTimerTime()).getTime()) },5000); +} + +/** + * 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){ + let sliderBar = helper.sliderBar() + let edown = new jQuery.Event('mousedown'); + let 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() +} diff --git a/tests/frontend/helper/ui.js b/tests/frontend/helper/ui.js new file mode 100644 index 000000000..211e29ecf --- /dev/null +++ b/tests/frontend/helper/ui.js @@ -0,0 +1,146 @@ +/** + * the contentWindow is either the normal pad or timeslider + * + * @returns {HTMLElement} contentWindow + */ +helper.contentWindow = function(){ + return $('#iframe-container iframe')[0].contentWindow; +} + +/** + * Opens the chat unless it is already open via an + * icon on the bottom right of the page + * + * @returns {Promise} + */ +helper.showChat = function(){ + var chaticon = helper.chatIcon(); + if(chaticon.hasClass('visible')) { + chaticon.click() + return helper.waitForPromise(function(){return !chaticon.hasClass('visible'); },2000) + } +} + +/** + * Closes the chat window if it is shown and not sticky + * + * @returns {Promise} + */ +helper.hideChat = function(){ + if(helper.isChatboxShown() && !helper.isChatboxSticky()) { + helper.titlecross().click() + return helper.waitForPromise(function(){return !helper.isChatboxShown(); },2000); + } +} + +/** + * Gets the chat icon from the bottom right of the page + * + * @returns {HTMLElement} the chat icon + */ +helper.chatIcon = function(){return helper.padChrome$('#chaticon')} + +/** + * The chat messages from the UI + * + * @returns {Array.} + */ +helper.chatTextParagraphs = function(){return helper.padChrome$('#chattext').children("p")} + +/** + * Returns true if the chat box is sticky + * + * @returns {boolean} stickyness of the chat box + */ +helper.isChatboxSticky = function() { + return helper.padChrome$('#chatbox').hasClass('stickyChat'); +} + +/** + * Returns true if the chat box is shown + * + * @returns {boolean} visibility of the chat box + */ +helper.isChatboxShown = function() { + return helper.padChrome$('#chatbox').hasClass('visible'); +} + +/** + * Gets the settings menu + * + * @returns {HTMLElement} the settings menu + */ +helper.settingsMenu = function(){return helper.padChrome$('#settings') }; + +/** + * Gets the settings button + * + * @returns {HTMLElement} the settings button + */ +helper.settingsButton = function(){return helper.padChrome$("button[data-l10n-id='pad.toolbar.settings.title']") } + +/** + * Gets the titlecross icon + * + * @returns {HTMLElement} the titlecross icon + */ +helper.titlecross = function(){return helper.padChrome$('#titlecross')} + +/** + * Returns true if the settings menu is visible + * + * @returns {boolean} is the settings menu shown? + */ +helper.isSettingsShown = function() { + return helper.padChrome$('#settings').hasClass('popup-show'); +} + +/** + * Gets the timer div of a timeslider that has the datetime of the revision + * + * @returns {HTMLElement} timer + */ +helper.timesliderTimer = function(){ + if(typeof helper.contentWindow().$ == 'function'){ + return helper.contentWindow().$('#timer') } + } + +/** + * Gets the time of the revision on a timeslider + * + * @returns {HTMLElement} timer + */ +helper.timesliderTimerTime = function(){ + if(helper.timesliderTimer()){ + return helper.timesliderTimer().text() + } +} + +/** + * The ui-slidar-bar element in the timeslider + * + * @returns {HTMLElement} + */ +helper.sliderBar = function(){ + return helper.contentWindow().$('#ui-slider-bar') +} + +/** + * revision_date element + * like "Saved October 10, 2020" + * + * @returns {HTMLElement} + */ +helper.revisionDateElem = function(){ + return helper.contentWindow().$('#revision_date').text(); +} + +/** + * revision_label element + * like "Version 1" + * + * @returns {HTMLElement} + */ +helper.revisionLabelElem = function(){ + return helper.contentWindow().$('#revision_label') +} diff --git a/tests/frontend/index.html b/tests/frontend/index.html index d828e851c..8d31a8e6a 100644 --- a/tests/frontend/index.html +++ b/tests/frontend/index.html @@ -14,11 +14,12 @@ - + - + + diff --git a/tests/frontend/specs/chat.js b/tests/frontend/specs/chat.js index 4a88379df..caf19bbf5 100644 --- a/tests/frontend/specs/chat.js +++ b/tests/frontend/specs/chat.js @@ -2,134 +2,78 @@ describe("Chat messages and UI", function(){ //create a new pad before each test run beforeEach(function(cb){ helper.newPad(cb); - this.timeout(60000); }); - it("opens chat, sends a message and makes sure it exists on the page", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it("opens chat, sends a message, makes sure it exists on the page and hides chat", async function() { var chatValue = "JohnMcLear"; - //click on the chat button to make chat visible - var $chatButton = chrome$("#chaticon"); - $chatButton.click(); - var $chatInput = chrome$("#chatinput"); - $chatInput.sendkeys('JohnMcLear'); // simulate a keypress of typing JohnMcLear - $chatInput.sendkeys('{enter}'); // simulate a keypress of enter actually does evt.which = 10 not 13 + await helper.showChat(); + await helper.sendChatMessage(`${chatValue}{enter}`); - //check if chat shows up - helper.waitFor(function(){ - return chrome$("#chattext").children("p").length !== 0; // wait until the chat message shows up - }).done(function(){ - var $firstChatMessage = chrome$("#chattext").children("p"); - var containsMessage = $firstChatMessage.text().indexOf("JohnMcLear") !== -1; // does the string contain JohnMcLear? - expect(containsMessage).to.be(true); // expect the first chat message to contain JohnMcLear + expect(helper.chatTextParagraphs().length).to.be(1); - // do a slightly more thorough check - var username = $firstChatMessage.children("b"); - var usernameValue = username.text(); - var time = $firstChatMessage.children(".time"); - var timeValue = time.text(); - var discoveredValue = $firstChatMessage.text(); - var chatMsgExists = (discoveredValue.indexOf("JohnMcLear") !== -1); - expect(chatMsgExists).to.be(true); - done(); - }); + //

+ // unnamed: + // 12:38 + // JohnMcLear + //

+ let username = helper.chatTextParagraphs().children("b").text(); + let time = helper.chatTextParagraphs().children(".time").text(); + expect(helper.chatTextParagraphs().text()).to.be(`${username}${time} ${chatValue}`); + + await helper.hideChat(); }); - it("makes sure that an empty message can't be sent", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it("makes sure that an empty message can't be sent", async function() { + var chatValue = "mluto"; - //click on the chat button to make chat visible - var $chatButton = chrome$("#chaticon"); - $chatButton.click(); - var $chatInput = chrome$("#chatinput"); - $chatInput.sendkeys('{enter}'); // simulate a keypress of enter (to send an empty message) - $chatInput.sendkeys('mluto'); // simulate a keypress of typing mluto - $chatInput.sendkeys('{enter}'); // simulate a keypress of enter (to send 'mluto') + await helper.showChat(); - //check if chat shows up - helper.waitFor(function(){ - return chrome$("#chattext").children("p").length !== 0; // wait until the chat message shows up - }).done(function(){ - // check that the empty message is not there - expect(chrome$("#chattext").children("p").length).to.be(1); - // check that the received message is not the empty one - var $firstChatMessage = chrome$("#chattext").children("p"); - var containsMessage = $firstChatMessage.text().indexOf("mluto") !== -1; - expect(containsMessage).to.be(true); - done(); - }); + await helper.sendChatMessage(`{enter}${chatValue}{enter}`); // simulate a keypress of typing enter, mluto and enter (to send 'mluto') + + let chat = helper.chatTextParagraphs(); + + expect(chat.length).to.be(1); + + // check that the received message is not the empty one + let username = chat.children("b").text(); + let time = chat.children(".time").text(); + + expect(chat.text()).to.be(`${username}${time} ${chatValue}`); }); - it("makes chat stick to right side of the screen", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it("makes chat stick to right side of the screen via settings, remove sticky via settings, close it", async function() { + await helper.showSettings(); - //click on the settings button to make settings visible - var $settingsButton = chrome$(".buttonicon-settings"); - $settingsButton.click(); - - //get the chat selector - var $stickychatCheckbox = chrome$("#options-stickychat"); - - //select chat always on screen - if (!$stickychatCheckbox.is(':checked')) { - $stickychatCheckbox.click(); - } - - // due to animation, we need to make some timeout... - setTimeout(function() { - //check if chat changed to get the stickychat Class - var $chatbox = chrome$("#chatbox"); - var hasStickyChatClass = $chatbox.hasClass("stickyChat"); - expect(hasStickyChatClass).to.be(true); - - // select chat always on screen and fire change event - $stickychatCheckbox.click(); - - setTimeout(function() { - //check if chat changed to remove the stickychat Class - var hasStickyChatClass = $chatbox.hasClass("stickyChat"); - expect(hasStickyChatClass).to.be(false); - - done(); - }, 10) - }, 10) + await helper.enableStickyChatviaSettings(); + expect(helper.isChatboxShown()).to.be(true); + expect(helper.isChatboxSticky()).to.be(true); + await helper.disableStickyChatviaSettings(); + expect(helper.isChatboxSticky()).to.be(false); + expect(helper.isChatboxShown()).to.be(true); + await helper.hideChat(); + expect(helper.isChatboxSticky()).to.be(false); + expect(helper.isChatboxShown()).to.be(false); }); - it("makes chat stick to right side of the screen then makes it one step smaller", function(done) { - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; + it("makes chat stick to right side of the screen via icon on the top right, remove sticky via icon, close it", async function() { + await helper.showChat(); - // open chat - chrome$('#chaticon').click(); + await helper.enableStickyChatviaIcon(); + expect(helper.isChatboxShown()).to.be(true); + expect(helper.isChatboxSticky()).to.be(true); - // select chat always on screen from chatbox - chrome$('.stick-to-screen-btn').click(); + await helper.disableStickyChatviaIcon(); + expect(helper.isChatboxShown()).to.be(true); + expect(helper.isChatboxSticky()).to.be(false); - // due to animation, we need to make some timeout... - setTimeout(function() { - //check if chat changed to get the stickychat Class - var $chatbox = chrome$("#chatbox"); - var hasStickyChatClass = $chatbox.hasClass("stickyChat"); - expect(hasStickyChatClass).to.be(true); - - // select chat always on screen and fire change event - chrome$('#titlecross').click(); - - setTimeout(function() { - //check if chat changed to remove the stickychat Class - var hasStickyChatClass = $chatbox.hasClass("stickyChat"); - expect(hasStickyChatClass).to.be(false); - - done(); - }, 10) - }, 10) + await helper.hideChat(); + expect(helper.isChatboxSticky()).to.be(false); + expect(helper.isChatboxShown()).to.be(false); }); xit("Checks showChat=false URL Parameter hides chat then when removed it shows chat", function(done) { diff --git a/tests/frontend/specs/delete.js b/tests/frontend/specs/delete.js index 616cd4ddc..639980f5f 100644 --- a/tests/frontend/specs/delete.js +++ b/tests/frontend/specs/delete.js @@ -17,7 +17,7 @@ describe("delete keystroke", function(){ // get the original string value minus the last char var originalTextValue = $firstTextElement.text(); - originalTextValueMinusFirstChar = originalTextValue.substring(1, originalTextValue.length ); + var originalTextValueMinusFirstChar = originalTextValue.substring(1, originalTextValue.length ); // simulate key presses to delete content $firstTextElement.sendkeys('{leftarrow}'); // simulate a keypress of the left arrow key diff --git a/tests/frontend/specs/helper.js b/tests/frontend/specs/helper.js index 3a83e7b99..ad8fce389 100644 --- a/tests/frontend/specs/helper.js +++ b/tests/frontend/specs/helper.js @@ -136,7 +136,7 @@ describe("the test helper", function(){ return false; }, 1500).fail(function(){ var duration = Date.now() - startTime; - expect(duration).to.be.greaterThan(1400); + expect(duration).to.be.greaterThan(1490); done(); }); }); @@ -149,8 +149,10 @@ describe("the test helper", function(){ checks++; return false; }, 2000, 100).fail(function(){ - expect(checks).to.be.greaterThan(10); - expect(checks).to.be.lessThan(30); + // One at the beginning, and 19-20 more depending on whether it's the timeout or the final + // poll that wins at 2000ms. + expect(checks).to.be.greaterThan(18); + expect(checks).to.be.lessThan(22); done(); }); }); @@ -399,4 +401,68 @@ describe("the test helper", function(){ done(); }); }); + + describe('helper',function(){ + before(function(cb){ + helper.newPad(function(){ + cb(); + }) + }) + + it(".textLines() returns the text of the pad as strings", async function(){ + let lines = helper.textLines(); + let defaultText = helper.defaultText(); + expect(Array.isArray(lines)).to.be(true); + expect(lines[0]).to.be.an('string'); + // @todo + // final "\n" is added automatically, but my understanding is this should happen + // only when the default text does not end with "\n" already + expect(lines.join("\n")+"\n").to.equal(defaultText); + }) + + it(".linesDiv() returns the text of the pad as div elements", async function(){ + let lines = helper.linesDiv(); + let defaultText = helper.defaultText(); + expect(Array.isArray(lines)).to.be(true); + expect(lines[0]).to.be.an('object'); + expect(lines[0].text()).to.be.an('string'); + _.each(defaultText.split("\n"), function(line, index){ + //last line of default text + if(index === lines.length){ + expect(line).to.equal(''); + } else { + expect(lines[index].text()).to.equal(line); + } + }) + }) + + it(".edit() defaults to send an edit to the first line", async function(){ + let firstLine = helper.textLines()[0]; + await helper.edit("line") + expect(helper.textLines()[0]).to.be(`line${firstLine}`); + }) + + it(".edit() to the line specified with parameter lineNo", async function(){ + let firstLine = helper.textLines()[0]; + await helper.edit("second line", 2); + + let text = helper.textLines(); + expect(text[0]).to.equal(firstLine); + expect(text[1]).to.equal("second line"); + }) + + it(".edit() supports sendkeys syntax ({selectall},{del},{enter})", async function(){ + expect(helper.textLines()[0]).to.not.equal(''); + + // select first line + helper.linesDiv()[0].sendkeys("{selectall}") + // delete first line + await helper.edit("{del}") + + expect(helper.textLines()[0]).to.be(''); + let noOfLines = helper.textLines().length; + await helper.edit("{enter}") + expect(helper.textLines().length).to.be(noOfLines+1); + }) + }) }); diff --git a/tests/frontend/specs/responsiveness.js b/tests/frontend/specs/responsiveness.js index 121851c25..802ab1c92 100644 --- a/tests/frontend/specs/responsiveness.js +++ b/tests/frontend/specs/responsiveness.js @@ -13,7 +13,7 @@ // Adapted from John McLear's original test case. -describe('Responsiveness of Editor', function() { +xdescribe('Responsiveness of Editor', function() { // create a new pad before each test run beforeEach(function(cb) { helper.newPad(cb); diff --git a/tests/frontend/specs/timeslider_follow.js b/tests/frontend/specs/timeslider_follow.js index 258abc3ec..cf03a4db6 100644 --- a/tests/frontend/specs/timeslider_follow.js +++ b/tests/frontend/specs/timeslider_follow.js @@ -1,55 +1,120 @@ -describe("timeslider", function(){ +describe("timeslider follow", function(){ //create a new pad before each test run beforeEach(function(cb){ helper.newPad(cb); - this.timeout(6000); }); - it("follow content as it's added to timeslider", function(done) { // passes - var inner$ = helper.padInner$; - var chrome$ = helper.padChrome$; - - // make some changes to produce 100 revisions - var timePerRev = 900 - , revs = 10; - this.timeout(revs*timePerRev+10000); - for(var i=0; i < revs; i++) { - setTimeout(function() { - // enter 'a' in the first text element - inner$("div").last().sendkeys('a\n'); - inner$("div").last().sendkeys('{enter}'); - inner$("div").last().sendkeys('{enter}'); - inner$("div").last().sendkeys('{enter}'); - inner$("div").last().sendkeys('{enter}'); - }, timePerRev*i); + it("content as it's added to timeslider", async function() { + // send 6 revisions + let revs = 6; + let message = 'a\n\n\n\n\n\n\n\n\n\n'; + let newLines = message.split('\n').length + for (let i=0;i