restructure: move bin/ and tests/ to src/

Also add symlinks from the old `bin/` and `tests/` locations to avoid
breaking scripts and other tools.

Motivations:

  * Scripts and tests no longer have to do dubious things like:

        require('ep_etherpad-lite/node_modules/foo')

    to access packages installed as dependencies in
    `src/package.json`.

  * Plugins can access the backend test helper library in a non-hacky
    way:

        require('ep_etherpad-lite/tests/backend/common')

  * We can delete the top-level `package.json` without breaking our
    ability to lint the files in `bin/` and `tests/`.

    Deleting the top-level `package.json` has downsides: It will cause
    `npm` to print warnings whenever plugins are installed, npm will
    no longer be able to enforce a plugin's peer dependency on
    ep_etherpad-lite, and npm will keep deleting the
    `node_modules/ep_etherpad-lite` symlink that points to `../src`.

    But there are significant upsides to deleting the top-level
    `package.json`: It will drastically speed up plugin installation
    because `npm` doesn't have to recursively walk the dependencies in
    `src/package.json`. Also, deleting the top-level `package.json`
    avoids npm's horrible dependency hoisting behavior (where it moves
    stuff from `src/node_modules/` to the top-level `node_modules/`
    directory). Dependency hoisting causes numerous mysterious
    problems such as silent failures in `npm outdated` and `npm
    update`. Dependency hoisting also breaks plugins that do:

        require('ep_etherpad-lite/node_modules/foo')
This commit is contained in:
John McLear 2021-02-03 12:08:43 +00:00 committed by Richard Hansen
parent efde0b787a
commit 2ea8ea1275
146 changed files with 191 additions and 1161 deletions

View file

@ -0,0 +1,25 @@
'use strict';
describe('All the alphabet works n stuff', function () {
const expectedString = 'abcdefghijklmnopqrstuvwxyz';
// create a new pad before each test run
beforeEach(function (cb) {
helper.newPad(cb);
this.timeout(60000);
});
it('when you enter any char it appears right', function (done) {
const inner$ = helper.padInner$;
// get the first text element out of the inner iframe
const firstTextElement = inner$('div').first();
// simulate key presses to delete content
firstTextElement.sendkeys('{selectall}'); // select all
firstTextElement.sendkeys('{del}'); // clear the first line
firstTextElement.sendkeys(expectedString); // insert the string
helper.waitFor(() => inner$('div').first().text() === expectedString, 2000).done(done);
});
});

View file

@ -0,0 +1,107 @@
'use strict';
describe('author of pad edition', function () {
const REGULAR_LINE = 0;
const LINE_WITH_ORDERED_LIST = 1;
const LINE_WITH_UNORDERED_LIST = 2;
// author 1 creates a new pad with some content (regular lines and lists)
before(function (done) {
const padId = helper.newPad(() => {
// make sure pad has at least 3 lines
const $firstLine = helper.padInner$('div').first();
const threeLines = ['regular line', 'line with ordered list', 'line with unordered list']
.join('<br>');
$firstLine.html(threeLines);
// wait for lines to be processed by Etherpad
helper.waitFor(() => {
const $lineWithUnorderedList = getLine(LINE_WITH_UNORDERED_LIST);
return $lineWithUnorderedList.text() === 'line with unordered list';
}).done(() => {
// create the unordered list
const $lineWithUnorderedList = getLine(LINE_WITH_UNORDERED_LIST);
$lineWithUnorderedList.sendkeys('{selectall}');
const $insertUnorderedListButton = helper.padChrome$('.buttonicon-insertunorderedlist');
$insertUnorderedListButton.click();
helper.waitFor(() => {
const $lineWithUnorderedList = getLine(LINE_WITH_UNORDERED_LIST);
return $lineWithUnorderedList.find('ul li').length === 1;
}).done(() => {
// create the ordered list
const $lineWithOrderedList = getLine(LINE_WITH_ORDERED_LIST);
$lineWithOrderedList.sendkeys('{selectall}');
const $insertOrderedListButton = helper.padChrome$('.buttonicon-insertorderedlist');
$insertOrderedListButton.click();
helper.waitFor(() => {
const $lineWithOrderedList = getLine(LINE_WITH_ORDERED_LIST);
return $lineWithOrderedList.find('ol li').length === 1;
}).done(() => {
// Reload pad, to make changes as a second user. Need a timeout here to make sure
// all changes were saved before reloading
setTimeout(() => {
// Expire cookie, so author is changed after reloading the pad.
// See https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie#Example_4_Reset_the_previous_cookie
helper.padChrome$.document.cookie =
'token=foo;expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
helper.newPad(done, padId);
}, 1000);
});
});
});
});
this.timeout(60000);
});
// author 2 makes some changes on the pad
it('marks only the new content as changes of the second user on a regular line', function (done) {
changeLineAndCheckOnlyThatChangeIsFromThisAuthor(REGULAR_LINE, 'x', done);
});
it('marks only the new content as changes of the second user on a ' +
'line with ordered list', function (done) {
changeLineAndCheckOnlyThatChangeIsFromThisAuthor(LINE_WITH_ORDERED_LIST, 'y', done);
});
it('marks only the new content as changes of the second user on ' +
'a line with unordered list', function (done) {
changeLineAndCheckOnlyThatChangeIsFromThisAuthor(LINE_WITH_UNORDERED_LIST, 'z', done);
});
/* ********************** Helper functions ************************ */
const getLine = (lineNumber) => helper.padInner$('div').eq(lineNumber);
const getAuthorFromClassList = (classes) => classes.find((cls) => cls.startsWith('author'));
const changeLineAndCheckOnlyThatChangeIsFromThisAuthor = (lineNumber, textChange, done) => {
// get original author class
const classes = getLine(lineNumber).find('span').first().attr('class').split(' ');
const originalAuthor = getAuthorFromClassList(classes);
// make change on target line
const $regularLine = getLine(lineNumber);
helper.selectLines($regularLine, $regularLine, 2, 2); // place caret after 2nd char of line
$regularLine.sendkeys(textChange);
// wait for change to be processed by Etherpad
let otherAuthorsOfLine;
helper.waitFor(() => {
const authorsOfLine = getLine(lineNumber).find('span').map(function () {
return getAuthorFromClassList($(this).attr('class').split(' '));
}).get();
otherAuthorsOfLine = authorsOfLine.filter((author) => author !== originalAuthor);
const lineHasChangeOfThisAuthor = otherAuthorsOfLine.length > 0;
return lineHasChangeOfThisAuthor;
}).done(() => {
const thisAuthor = otherAuthorsOfLine[0];
const $changeOfThisAuthor = getLine(lineNumber).find(`span.${thisAuthor}`);
expect($changeOfThisAuthor.text()).to.be(textChange);
done();
});
};
});

View file

@ -0,0 +1,65 @@
'use strict';
describe('bold button', function () {
// create a new pad before each test run
beforeEach(function (cb) {
helper.newPad(cb);
this.timeout(60000);
});
it('makes text bold on click', function (done) {
const inner$ = helper.padInner$;
const chrome$ = helper.padChrome$;
// get the first text element out of the inner iframe
const $firstTextElement = inner$('div').first();
// select this text element
$firstTextElement.sendkeys('{selectall}');
// get the bold button and click it
const $boldButton = chrome$('.buttonicon-bold');
$boldButton.click();
const $newFirstTextElement = inner$('div').first();
// is there a <b> element now?
const isBold = $newFirstTextElement.find('b').length === 1;
// expect it to be bold
expect(isBold).to.be(true);
// make sure the text hasn't changed
expect($newFirstTextElement.text()).to.eql($firstTextElement.text());
done();
});
it('makes text bold on keypress', function (done) {
const inner$ = helper.padInner$;
// get the first text element out of the inner iframe
const $firstTextElement = inner$('div').first();
// select this text element
$firstTextElement.sendkeys('{selectall}');
const e = new inner$.Event(helper.evtType);
e.ctrlKey = true; // Control key
e.which = 66; // b
inner$('#innerdocbody').trigger(e);
const $newFirstTextElement = inner$('div').first();
// is there a <b> element now?
const isBold = $newFirstTextElement.find('b').length === 1;
// expect it to be bold
expect(isBold).to.be(true);
// make sure the text hasn't changed
expect($newFirstTextElement.text()).to.eql($firstTextElement.text());
done();
});
});

View file

@ -0,0 +1,352 @@
'use strict';
describe('As the caret is moved is the UI properly updated?', function () {
/*
let padName;
const numberOfRows = 50;
//create a new pad before each test run
beforeEach(function(cb){
helper.newPad(cb);
this.timeout(60000);
});
xit("creates a pad", function(done) {
padName = helper.newPad(done);
this.timeout(60000);
});
*/
/* Tests to do
* Keystroke up (38), down (40), left (37), right (39)
* with and without special keys IE control / shift
* Page up (33) / down (34) with and without special keys
* Page up on the first line shouldn't move the viewport
* Down down on the last line shouldn't move the viewport
* Down arrow on any other line except the last lines shouldn't move the viewport
* Do all of the above tests after a copy/paste event
*/
/* Challenges
* How do we keep the authors focus on a line if the lines above the author are modified?
* We should only redraw the user to a location if they are typing and make sure shift
* and arrow keys aren't redrawing the UI else highlight - copy/paste would get broken
* How can we simulate an edit event in the test framework?
*/
/*
// THIS DOESNT WORK IN CHROME AS IT DOESNT MOVE THE CURSOR!
it("down arrow", function(done){
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
var $newFirstTextElement = inner$("div").first();
$newFirstTextElement.focus();
keyEvent(inner$, 37, false, false); // arrow down
keyEvent(inner$, 37, false, false); // arrow down
done();
});
it("Creates N lines", function(done){
var inner$ = helper.padInner$;
console.log(inner$);
var chrome$ = helper.padChrome$;
var $newFirstTextElement = inner$("div").first();
prepareDocument(numberOfRows, $newFirstTextElement); // N lines into the first div as a target
helper.waitFor(function(){ // Wait for the DOM to register the new items
return inner$("div").first().text().length == 6;
}).done(function(){ // Once the DOM has registered the items
done();
});
});
it("Moves caret up a line", function(done){
var inner$ = helper.padInner$;
var $newFirstTextElement = inner$("div").first();
var originalCaretPosition = caretPosition(inner$);
var originalPos = originalCaretPosition.y;
var newCaretPos;
keyEvent(inner$, 38, false, false); // arrow up
helper.waitFor(function(){ // Wait for the DOM to register the new items
var newCaretPosition = caretPosition(inner$);
newCaretPos = newCaretPosition.y;
return (newCaretPos < originalPos);
}).done(function(){
expect(newCaretPos).to.be.lessThan(originalPos);
done();
});
});
it("Moves caret down a line", function(done){
var inner$ = helper.padInner$;
var $newFirstTextElement = inner$("div").first();
var originalCaretPosition = caretPosition(inner$);
var originalPos = originalCaretPosition.y;
var newCaretPos;
keyEvent(inner$, 40, false, false); // arrow down
helper.waitFor(function(){ // Wait for the DOM to register the new items
var newCaretPosition = caretPosition(inner$);
newCaretPos = newCaretPosition.y;
return (newCaretPos > originalPos);
}).done(function(){
expect(newCaretPos).to.be.moreThan(originalPos);
done();
});
});
it("Moves caret to top of doc", function(done){
var inner$ = helper.padInner$;
var $newFirstTextElement = inner$("div").first();
var originalCaretPosition = caretPosition(inner$);
var originalPos = originalCaretPosition.y;
var newCaretPos;
var i = 0;
while(i < numberOfRows){ // press pageup key N times
keyEvent(inner$, 33, false, false);
i++;
}
helper.waitFor(function(){ // Wait for the DOM to register the new items
var newCaretPosition = caretPosition(inner$);
newCaretPos = newCaretPosition.y;
return (newCaretPos < originalPos);
}).done(function(){
expect(newCaretPos).to.be.lessThan(originalPos);
done();
});
});
it("Moves caret right a position", function(done){
var inner$ = helper.padInner$;
var $newFirstTextElement = inner$("div").first();
var originalCaretPosition = caretPosition(inner$);
var originalPos = originalCaretPosition.x;
var newCaretPos;
keyEvent(inner$, 39, false, false); // arrow right
helper.waitFor(function(){ // Wait for the DOM to register the new items
var newCaretPosition = caretPosition(inner$);
newCaretPos = newCaretPosition.x;
return (newCaretPos > originalPos);
}).done(function(){
expect(newCaretPos).to.be.moreThan(originalPos);
done();
});
});
it("Moves caret left a position", function(done){
var inner$ = helper.padInner$;
var $newFirstTextElement = inner$("div").first();
var originalCaretPosition = caretPosition(inner$);
var originalPos = originalCaretPosition.x;
var newCaretPos;
keyEvent(inner$, 33, false, false); // arrow left
helper.waitFor(function(){ // Wait for the DOM to register the new items
var newCaretPosition = caretPosition(inner$);
newCaretPos = newCaretPosition.x;
return (newCaretPos < originalPos);
}).done(function(){
expect(newCaretPos).to.be.lessThan(originalPos);
done();
});
});
it("Moves caret to the next line using right arrow", function(done){
var inner$ = helper.padInner$;
var $newFirstTextElement = inner$("div").first();
var originalCaretPosition = caretPosition(inner$);
var originalPos = originalCaretPosition.y;
var newCaretPos;
keyEvent(inner$, 39, false, false); // arrow right
keyEvent(inner$, 39, false, false); // arrow right
keyEvent(inner$, 39, false, false); // arrow right
keyEvent(inner$, 39, false, false); // arrow right
keyEvent(inner$, 39, false, false); // arrow right
keyEvent(inner$, 39, false, false); // arrow right
keyEvent(inner$, 39, false, false); // arrow right
helper.waitFor(function(){ // Wait for the DOM to register the new items
var newCaretPosition = caretPosition(inner$);
newCaretPos = newCaretPosition.y;
return (newCaretPos > originalPos);
}).done(function(){
expect(newCaretPos).to.be.moreThan(originalPos);
done();
});
});
it("Moves caret to the previous line using left arrow", function(done){
var inner$ = helper.padInner$;
var $newFirstTextElement = inner$("div").first();
var originalCaretPosition = caretPosition(inner$);
var originalPos = originalCaretPosition.y;
var newCaretPos;
keyEvent(inner$, 33, false, false); // arrow left
helper.waitFor(function(){ // Wait for the DOM to register the new items
var newCaretPosition = caretPosition(inner$);
newCaretPos = newCaretPosition.y;
return (newCaretPos < originalPos);
}).done(function(){
expect(newCaretPos).to.be.lessThan(originalPos);
done();
});
});
/*
it("Creates N rows, changes height of rows, updates UI by caret key events", function(done){
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
var numberOfRows = 50;
// ace creates a new dom element when you press a keystroke,
// so just get the first text element again
var $newFirstTextElement = inner$("div").first();
var originalDivHeight = inner$("div").first().css("height");
prepareDocument(numberOfRows, $newFirstTextElement); // N lines into the first div as a target
helper.waitFor(function(){ // Wait for the DOM to register the new items
return inner$("div").first().text().length == 6;
}).done(function(){ // Once the DOM has registered the items
// Randomize the item heights (replicates images / headings etc)
inner$("div").each(function(index){
var random = Math.floor(Math.random() * (50)) + 20;
$(this).css("height", random+"px");
});
console.log(caretPosition(inner$));
var newDivHeight = inner$("div").first().css("height");
// has the new div height changed from the original div height
var heightHasChanged = originalDivHeight != newDivHeight;
expect(heightHasChanged).to.be(true); // expect the first line to be blank
});
// Is this Element now visible to the pad user?
helper.waitFor(function(){ // Wait for the DOM to register the new items
// Wait for the DOM to scroll into place
return isScrolledIntoView(inner$("div:nth-child("+numberOfRows+")"), inner$);
}).done(function(){ // Once the DOM has registered the items
// Randomize the item heights (replicates images / headings etc)
inner$("div").each(function(index){
var random = Math.floor(Math.random() * (80 - 20 + 1)) + 20;
$(this).css("height", random+"px");
});
var newDivHeight = inner$("div").first().css("height");
// has the new div height changed from the original div height
var heightHasChanged = originalDivHeight != newDivHeight;
expect(heightHasChanged).to.be(true); // expect the first line to be blank
});
var i = 0;
while(i < numberOfRows){ // press down arrow
keyEvent(inner$, 40, false, false);
i++;
}
// Does scrolling back up the pad with the up arrow show the correct contents?
helper.waitFor(function(){ // Wait for the new position to be in place
try{
// Wait for the DOM to scroll into place
return isScrolledIntoView(inner$("div:nth-child("+numberOfRows+")"), inner$);
}catch(e){
return false;
}
}).done(function(){ // Once the DOM has registered the items
var i = 0;
while(i < numberOfRows){ // press down arrow
keyEvent(inner$, 33, false, false); // doesn't work
i++;
}
// Does scrolling back up the pad with the up arrow show the correct contents?
helper.waitFor(function(){ // Wait for the new position to be in place
try{
// Wait for the DOM to scroll into place
return isScrolledIntoView(inner$("div:nth-child(0)"), inner$);
}catch(e){
return false;
}
}).done(function(){ // Once the DOM has registered the items
});
});
var i = 0;
while(i < numberOfRows){ // press down arrow
keyEvent(inner$, 33, false, false); // doesn't work
i++;
}
// Does scrolling back up the pad with the up arrow show the correct contents?
helper.waitFor(function(){ // Wait for the new position to be in place
// Wait for the DOM to scroll into place
return isScrolledIntoView(inner$("div:nth-child(1)"), inner$);
}).done(function(){ // Once the DOM has registered the items
expect(true).to.be(true);
done();
});
*/
});
// generates a random document with random content on n lines
const prepareDocument = (n, target) => {
let i = 0;
while (i < n) { // for each line
target.sendkeys(makeStr()); // generate a random string and send that to the editor
target.sendkeys('{enter}'); // generator an enter keypress
i++; // rinse n times
}
};
// sends a charCode to the window
const keyEvent = (target, charCode, ctrl, shift) => {
const e = new target.Event(helper.evtType);
if (ctrl) {
e.ctrlKey = true; // Control key
}
if (shift) {
e.shiftKey = true; // Shift Key
}
e.which = charCode;
e.keyCode = charCode;
target('#innerdocbody').trigger(e);
};
// from http://stackoverflow.com/questions/1349404/generate-a-string-of-5-random-characters-in-javascript
const makeStr = () => {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 5; i++) text += possible.charAt(Math.floor(Math.random() * possible.length));
return text;
};
// from http://stackoverflow.com/questions/487073/check-if-element-is-visible-after-scrolling
const isScrolledIntoView = (elem, $) => {
const docViewTop = $(window).scrollTop();
const docViewBottom = docViewTop + $(window).height();
const elemTop = $(elem).offset().top; // how far the element is from the top of it's container
// how far plus the height of the elem.. IE is it all in?
let elemBottom = elemTop + $(elem).height();
elemBottom -= 16; // don't ask, sorry but this is needed..
return ((elemBottom <= docViewBottom) && (elemTop >= docViewTop));
};
const caretPosition = ($) => {
const doc = $.window.document;
const pos = doc.getSelection();
pos.y = pos.anchorNode.parentElement.offsetTop;
pos.x = pos.anchorNode.parentElement.offsetLeft;
return pos;
};

View file

@ -0,0 +1,107 @@
'use strict';
describe('change user color', function () {
// create a new pad before each test run
beforeEach(function (cb) {
helper.newPad(cb);
this.timeout(60000);
});
it('Color picker matches original color and remembers the user color' +
' after a refresh', function (done) {
this.timeout(60000);
const chrome$ = helper.padChrome$;
// click on the settings button to make settings visible
const $userButton = chrome$('.buttonicon-showusers');
$userButton.click();
const $userSwatch = chrome$('#myswatch');
$userSwatch.click();
const fb = chrome$.farbtastic('#colorpicker');
const $colorPickerSave = chrome$('#mycolorpickersave');
const $colorPickerPreview = chrome$('#mycolorpickerpreview');
// Same color represented in two different ways
const testColorHash = '#abcdef';
const testColorRGB = 'rgb(171, 205, 239)';
// Check that the color picker matches the automatically assigned random color on the swatch.
// NOTE: This has a tiny chance of creating a false positive for passing in the
// off-chance the randomly assigned color is the same as the test color.
expect($colorPickerPreview.css('background-color')).to.be($userSwatch.css('background-color'));
// The swatch updates as the test color is picked.
fb.setColor(testColorHash);
expect($colorPickerPreview.css('background-color')).to.be(testColorRGB);
$colorPickerSave.click();
expect($userSwatch.css('background-color')).to.be(testColorRGB);
setTimeout(() => { // give it a second to save the color on the server side
helper.newPad({ // get a new pad, but don't clear the cookies
clearCookies: false,
cb() {
const chrome$ = helper.padChrome$;
// click on the settings button to make settings visible
const $userButton = chrome$('.buttonicon-showusers');
$userButton.click();
const $userSwatch = chrome$('#myswatch');
$userSwatch.click();
const $colorPickerPreview = chrome$('#mycolorpickerpreview');
expect($colorPickerPreview.css('background-color')).to.be(testColorRGB);
expect($userSwatch.css('background-color')).to.be(testColorRGB);
done();
},
});
}, 1000);
});
it('Own user color is shown when you enter a chat', function (done) {
const chrome$ = helper.padChrome$;
const $colorOption = helper.padChrome$('#options-colorscheck');
if (!$colorOption.is(':checked')) {
$colorOption.click();
}
// click on the settings button to make settings visible
const $userButton = chrome$('.buttonicon-showusers');
$userButton.click();
const $userSwatch = chrome$('#myswatch');
$userSwatch.click();
const fb = chrome$.farbtastic('#colorpicker');
const $colorPickerSave = chrome$('#mycolorpickersave');
// Same color represented in two different ways
const testColorHash = '#abcdef';
const testColorRGB = 'rgb(171, 205, 239)';
fb.setColor(testColorHash);
$colorPickerSave.click();
// click on the chat button to make chat visible
const $chatButton = chrome$('#chaticon');
$chatButton.click();
const $chatInput = chrome$('#chatinput');
$chatInput.sendkeys('O hi'); // simulate a keypress of typing user
// simulate a keypress of enter actually does evt.which = 10 not 13
$chatInput.sendkeys('{enter}');
// wait until the chat message shows up
helper.waitFor(() => chrome$('#chattext').children('p').length !== 0
).done(() => {
const $firstChatMessage = chrome$('#chattext').children('p');
// expect the first chat message to be of the user's color
expect($firstChatMessage.css('background-color')).to.be(testColorRGB);
done();
});
});
});

View file

@ -0,0 +1,36 @@
'use strict';
describe('change username value', function () {
// create a new pad before each test run
beforeEach(function (cb) {
helper.newPad(cb);
});
it('Remembers the user name after a refresh', async function () {
helper.toggleUserList();
helper.setUserName('😃');
helper.newPad({ // get a new pad, but don't clear the cookies
clearCookies: false,
cb() {
helper.toggleUserList();
expect(helper.usernameField().val()).to.be('😃');
},
});
});
it('Own user name is shown when you enter a chat', async function () {
helper.toggleUserList();
helper.setUserName('😃');
helper.showChat();
helper.sendChatMessage('O hi{enter}');
await helper.waitForPromise(() => {
// username:hours:minutes text
const chatText = helper.chatTextParagraphs().text();
return chatText.indexOf('😃') === 0;
});
});
});

View file

@ -0,0 +1,116 @@
'use strict';
describe('Chat messages and UI', function () {
// create a new pad before each test run
beforeEach(function (cb) {
helper.newPad(cb);
});
it('opens chat, sends a message, makes sure it exists ' +
'on the page and hides chat', async function () {
const chatValue = 'JohnMcLear';
await helper.showChat();
await helper.sendChatMessage(`${chatValue}{enter}`);
expect(helper.chatTextParagraphs().length).to.be(1);
// <p data-authorid="a.qjkwNs4z0pPROphS"
// class="author-a-qjkwz78zs4z122z0pz80zz82zz79zphz83z">
// <b>unnamed:</b>
// <span class="time author-a-qjkwz78zs4z122z0pz80zz82zz79zphz83z">12:38
// </span> JohnMcLear
// </p>
const username = helper.chatTextParagraphs().children('b').text();
const 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", async function () {
const chatValue = 'mluto';
await helper.showChat();
// simulate a keypress of typing enter, mluto and enter (to send 'mluto')
await helper.sendChatMessage(`{enter}${chatValue}{enter}`);
const chat = helper.chatTextParagraphs();
expect(chat.length).to.be(1);
// check that the received message is not the empty one
const username = chat.children('b').text();
const time = chat.children('.time').text();
expect(chat.text()).to.be(`${username}${time} ${chatValue}`);
});
it('makes chat stick to right side of the screen via settings, ' +
'remove sticky via settings, close it', async function () {
await helper.showSettings();
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 via icon on the top' +
' right, remove sticky via icon, close it', async function () {
await helper.showChat();
await helper.enableStickyChatviaIcon();
expect(helper.isChatboxShown()).to.be(true);
expect(helper.isChatboxSticky()).to.be(true);
await helper.disableStickyChatviaIcon();
expect(helper.isChatboxShown()).to.be(true);
expect(helper.isChatboxSticky()).to.be(false);
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) {
this.timeout(60000);
setTimeout(() => { // give it a second to save the username on the server side
helper.newPad({ // get a new pad, but don't clear the cookies
clearCookies: false,
params: {
showChat: 'false',
}, cb() {
const chrome$ = helper.padChrome$;
const chaticon = chrome$('#chaticon');
// chat should be hidden.
expect(chaticon.is(':visible')).to.be(false);
setTimeout(() => { // give it a second to save the username on the server side
helper.newPad({ // get a new pad, but don't clear the cookies
clearCookies: false,
cb() {
const chrome$ = helper.padChrome$;
const chaticon = chrome$('#chaticon');
// chat should be visible.
expect(chaticon.is(':visible')).to.be(true);
done();
},
});
}, 1000);
},
});
}, 1000);
});
});

View file

@ -0,0 +1,80 @@
'use strict';
describe('chat-load-messages', function () {
let padName;
it('creates a pad', function (done) {
padName = helper.newPad(done);
this.timeout(60000);
});
it('adds a lot of messages', function (done) {
const chrome$ = helper.padChrome$;
const chatButton = chrome$('#chaticon');
chatButton.click();
const chatInput = chrome$('#chatinput');
const chatText = chrome$('#chattext');
this.timeout(60000);
const messages = 140;
for (let i = 1; i <= messages; i++) {
let num = `${i}`;
if (num.length === 1) num = `00${num}`;
if (num.length === 2) num = `0${num}`;
chatInput.sendkeys(`msg${num}`);
chatInput.sendkeys('{enter}');
}
helper.waitFor(() => chatText.children('p').length === messages, 60000).always(() => {
expect(chatText.children('p').length).to.be(messages);
helper.newPad(done, padName);
});
});
it('checks initial message count', function (done) {
let chatText;
const expectedCount = 101;
const chrome$ = helper.padChrome$;
helper.waitFor(() => {
const chatButton = chrome$('#chaticon');
chatButton.click();
chatText = chrome$('#chattext');
return chatText.children('p').length === expectedCount;
}).always(() => {
expect(chatText.children('p').length).to.be(expectedCount);
done();
});
});
it('loads more messages', function (done) {
const expectedCount = 122;
const chrome$ = helper.padChrome$;
const chatButton = chrome$('#chaticon');
chatButton.click();
const chatText = chrome$('#chattext');
const loadMsgBtn = chrome$('#chatloadmessagesbutton');
loadMsgBtn.click();
helper.waitFor(() => chatText.children('p').length === expectedCount).always(() => {
expect(chatText.children('p').length).to.be(expectedCount);
done();
});
});
it('checks for button vanishing', function (done) {
const expectedDisplay = 'none';
const chrome$ = helper.padChrome$;
const chatButton = chrome$('#chaticon');
chatButton.click();
const loadMsgBtn = chrome$('#chatloadmessagesbutton');
const loadMsgBall = chrome$('#chatloadmessagesball');
loadMsgBtn.click();
helper.waitFor(() => loadMsgBtn.css('display') === expectedDisplay &&
loadMsgBall.css('display') === expectedDisplay).always(() => {
expect(loadMsgBtn.css('display')).to.be(expectedDisplay);
expect(loadMsgBall.css('display')).to.be(expectedDisplay);
done();
});
});
});

View file

@ -0,0 +1,121 @@
'use strict';
describe('clear authorship colors button', function () {
// create a new pad before each test run
beforeEach(function (cb) {
helper.newPad(cb);
this.timeout(60000);
});
it('makes text clear authorship colors', function (done) {
const inner$ = helper.padInner$;
const chrome$ = helper.padChrome$;
// override the confirm dialogue functioon
helper.padChrome$.window.confirm = function () {
return true;
};
// get the first text element out of the inner iframe
const $firstTextElement = inner$('div').first();
// Set some new text
const sentText = 'Hello';
// select this text element
$firstTextElement.sendkeys('{selectall}');
$firstTextElement.sendkeys(sentText);
$firstTextElement.sendkeys('{rightarrow}');
// wait until we have the full value available
helper.waitFor(() => inner$('div span').first().attr('class').indexOf('author') !== -1
).done(() => {
// IE hates you if you don't give focus to the inner frame bevore you do a clearAuthorship
inner$('div').first().focus();
// get the clear authorship colors button and click it
const $clearauthorshipcolorsButton = chrome$('.buttonicon-clearauthorship');
$clearauthorshipcolorsButton.click();
// does the first div include an author class?
const hasAuthorClass = inner$('div').first().attr('class').indexOf('author') !== -1;
expect(hasAuthorClass).to.be(false);
helper.waitFor(() => {
const disconnectVisible =
chrome$('div.disconnected').attr('class').indexOf('visible') === -1;
return (disconnectVisible === true);
});
const disconnectVisible = chrome$('div.disconnected').attr('class').indexOf('visible') === -1;
expect(disconnectVisible).to.be(true);
done();
});
});
it("makes text clear authorship colors and checks it can't be undone", function (done) {
const inner$ = helper.padInner$;
const chrome$ = helper.padChrome$;
// override the confirm dialogue functioon
helper.padChrome$.window.confirm = function () {
return true;
};
// get the first text element out of the inner iframe
const $firstTextElement = inner$('div').first();
// Set some new text
const sentText = 'Hello';
// select this text element
$firstTextElement.sendkeys('{selectall}');
$firstTextElement.sendkeys(sentText);
$firstTextElement.sendkeys('{rightarrow}');
// wait until we have the full value available
helper.waitFor(
() => inner$('div span').first().attr('class').indexOf('author') !== -1
).done(() => {
// IE hates you if you don't give focus to the inner frame bevore you do a clearAuthorship
inner$('div').first().focus();
// get the clear authorship colors button and click it
const $clearauthorshipcolorsButton = chrome$('.buttonicon-clearauthorship');
$clearauthorshipcolorsButton.click();
// does the first div include an author class?
let hasAuthorClass = inner$('div').first().attr('class').indexOf('author') !== -1;
expect(hasAuthorClass).to.be(false);
const e = new inner$.Event(helper.evtType);
e.ctrlKey = true; // Control key
e.which = 90; // z
inner$('#innerdocbody').trigger(e); // shouldn't od anything
// does the first div include an author class?
hasAuthorClass = inner$('div').first().attr('class').indexOf('author') !== -1;
expect(hasAuthorClass).to.be(false);
// get undo and redo buttons
const $undoButton = chrome$('.buttonicon-undo');
// click the button
$undoButton.click(); // shouldn't do anything
hasAuthorClass = inner$('div').first().attr('class').indexOf('author') !== -1;
expect(hasAuthorClass).to.be(false);
helper.waitFor(() => {
const disconnectVisible =
chrome$('div.disconnected').attr('class').indexOf('visible') === -1;
return (disconnectVisible === true);
});
const disconnectVisible = chrome$('div.disconnected').attr('class').indexOf('visible') === -1;
expect(disconnectVisible).to.be(true);
done();
});
});
});

View file

@ -0,0 +1,33 @@
'use strict';
describe('delete keystroke', function () {
// create a new pad before each test run
beforeEach(function (cb) {
helper.newPad(cb);
this.timeout(60000);
});
it('makes text delete', function (done) {
const inner$ = helper.padInner$;
// get the first text element out of the inner iframe
const $firstTextElement = inner$('div').first();
// get the original length of this element
const elementLength = $firstTextElement.text().length;
// simulate key presses to delete content
$firstTextElement.sendkeys('{leftarrow}'); // simulate a keypress of the left arrow key
$firstTextElement.sendkeys('{del}'); // simulate a keypress of delete
const $newFirstTextElement = inner$('div').first();
// get the new length of this element
const newElementLength = $newFirstTextElement.text().length;
// expect it to be one char less in length
expect(newElementLength).to.be((elementLength - 1));
done();
});
});

View file

@ -0,0 +1,171 @@
'use strict';
// WARNING: drag and drop is only simulated on these tests, manual testing might also be necessary
describe('drag and drop', function () {
before(function (done) {
helper.newPad(() => {
createScriptWithSeveralLines(done);
});
this.timeout(60000);
});
context('when user drags part of one line and drops it far form its original place', function () {
before(function (done) {
selectPartOfSourceLine();
dragSelectedTextAndDropItIntoMiddleOfLine(TARGET_LINE);
// make sure DnD was correctly simulated
helper.waitFor(() => {
const $targetLine = getLine(TARGET_LINE);
const sourceWasMovedToTarget = $targetLine.text() === 'Target line [line 1]';
return sourceWasMovedToTarget;
}).done(done);
});
context('and user triggers UNDO', function () {
before(function () {
const $undoButton = helper.padChrome$('.buttonicon-undo');
$undoButton.click();
});
it('moves text back to its original place', function (done) {
// test text was removed from drop target
const $targetLine = getLine(TARGET_LINE);
expect($targetLine.text()).to.be('Target line []');
// test text was added back to original place
const $firstSourceLine = getLine(FIRST_SOURCE_LINE);
const $lastSourceLine = getLine(FIRST_SOURCE_LINE + 1);
expect($firstSourceLine.text()).to.be('Source line 1.');
expect($lastSourceLine.text()).to.be('Source line 2.');
done();
});
});
});
context('when user drags some lines far form its original place', function () {
before(function (done) {
selectMultipleSourceLines();
dragSelectedTextAndDropItIntoMiddleOfLine(TARGET_LINE);
// make sure DnD was correctly simulated
helper.waitFor(() => {
const $lineAfterTarget = getLine(TARGET_LINE + 1);
const sourceWasMovedToTarget = $lineAfterTarget.text() !== '...';
return sourceWasMovedToTarget;
}).done(done);
});
context('and user triggers UNDO', function () {
before(function () {
const $undoButton = helper.padChrome$('.buttonicon-undo');
$undoButton.click();
});
it('moves text back to its original place', function (done) {
// test text was removed from drop target
const $targetLine = getLine(TARGET_LINE);
expect($targetLine.text()).to.be('Target line []');
// test text was added back to original place
const $firstSourceLine = getLine(FIRST_SOURCE_LINE);
const $lastSourceLine = getLine(FIRST_SOURCE_LINE + 1);
expect($firstSourceLine.text()).to.be('Source line 1.');
expect($lastSourceLine.text()).to.be('Source line 2.');
done();
});
});
});
/* ********************* Helper functions/constants ********************* */
const TARGET_LINE = 2;
const FIRST_SOURCE_LINE = 5;
const getLine = (lineNumber) => {
const $lines = helper.padInner$('div');
return $lines.slice(lineNumber, lineNumber + 1);
};
const createScriptWithSeveralLines = (done) => {
// create some lines to be used on the tests
const $firstLine = helper.padInner$('div').first();
$firstLine.html('...<br>...<br>Target line []<br>...<br>...<br>' +
'Source line 1.<br>Source line 2.<br>');
// wait for lines to be split
helper.waitFor(() => {
const $lastSourceLine = getLine(FIRST_SOURCE_LINE + 1);
return $lastSourceLine.text() === 'Source line 2.';
}).done(done);
};
const selectPartOfSourceLine = () => {
const $sourceLine = getLine(FIRST_SOURCE_LINE);
// select 'line 1' from 'Source line 1.'
const start = 'Source '.length;
const end = start + 'line 1'.length;
helper.selectLines($sourceLine, $sourceLine, start, end);
};
const selectMultipleSourceLines = () => {
const $firstSourceLine = getLine(FIRST_SOURCE_LINE);
const $lastSourceLine = getLine(FIRST_SOURCE_LINE + 1);
helper.selectLines($firstSourceLine, $lastSourceLine);
};
const dragSelectedTextAndDropItIntoMiddleOfLine = (targetLineNumber) => {
// dragstart: start dragging content
triggerEvent('dragstart');
// drop: get HTML data from selected text
const draggedHtml = getHtmlFromSelectedText();
triggerEvent('drop');
// dragend: remove original content + insert HTML data into target
moveSelectionIntoTarget(draggedHtml, targetLineNumber);
triggerEvent('dragend');
};
const getHtmlFromSelectedText = () => {
const innerDocument = helper.padInner$.document;
const range = innerDocument.getSelection().getRangeAt(0);
const clonedSelection = range.cloneContents();
const span = innerDocument.createElement('span');
span.id = 'buffer';
span.appendChild(clonedSelection);
const draggedHtml = span.outerHTML;
return draggedHtml;
};
const triggerEvent = (eventName) => {
const event = new helper.padInner$.Event(eventName);
helper.padInner$('#innerdocbody').trigger(event);
};
const moveSelectionIntoTarget = (draggedHtml, targetLineNumber) => {
const innerDocument = helper.padInner$.document;
// delete original content
innerDocument.execCommand('delete');
// set position to insert content on target line
const $target = getLine(targetLineNumber);
$target.sendkeys('{selectall}{rightarrow}{leftarrow}');
// Insert content.
// Based on http://stackoverflow.com/a/6691294, to be IE-compatible
const range = innerDocument.getSelection().getRangeAt(0);
const frag = innerDocument.createDocumentFragment();
const el = innerDocument.createElement('div');
el.innerHTML = draggedHtml;
while (el.firstChild) {
frag.appendChild(el.firstChild);
}
range.insertNode(frag);
};
});

View file

@ -0,0 +1,135 @@
'use strict';
describe('embed links', function () {
const objectify = function (str) {
const hash = {};
const parts = str.split('&');
for (let i = 0; i < parts.length; i++) {
const keyValue = parts[i].split('=');
hash[keyValue[0]] = keyValue[1];
}
return hash;
};
const checkiFrameCode = function (embedCode, readonly) {
// turn the code into an html element
const $embediFrame = $(embedCode);
// read and check the frame attributes
const width = $embediFrame.attr('width');
const height = $embediFrame.attr('height');
const name = $embediFrame.attr('name');
expect(width).to.be('100%');
expect(height).to.be('600');
expect(name).to.be(readonly ? 'embed_readonly' : 'embed_readwrite');
// parse the url
const src = $embediFrame.attr('src');
const questionMark = src.indexOf('?');
const url = src.substr(0, questionMark);
const paramsStr = src.substr(questionMark + 1);
const params = objectify(paramsStr);
const expectedParams = {
showControls: 'true',
showChat: 'true',
showLineNumbers: 'true',
useMonospaceFont: 'false',
};
// check the url
if (readonly) {
expect(url.indexOf('r.') > 0).to.be(true);
} else {
expect(url).to.be(helper.padChrome$.window.location.href);
}
// check if all parts of the url are like expected
expect(params).to.eql(expectedParams);
};
describe('read and write', function () {
// create a new pad before each test run
beforeEach(function (cb) {
helper.newPad(cb);
this.timeout(60000);
});
describe('the share link', function () {
it('is the actual pad url', function (done) {
const chrome$ = helper.padChrome$;
// open share dropdown
chrome$('.buttonicon-embed').click();
// get the link of the share field + the actual pad url and compare them
const shareLink = chrome$('#linkinput').val();
const padURL = chrome$.window.location.href;
expect(shareLink).to.be(padURL);
done();
});
});
describe('the embed as iframe code', function () {
it('is an iframe with the the correct url parameters and correct size', function (done) {
const chrome$ = helper.padChrome$;
// open share dropdown
chrome$('.buttonicon-embed').click();
// get the link of the share field + the actual pad url and compare them
const embedCode = chrome$('#embedinput').val();
checkiFrameCode(embedCode, false);
done();
});
});
});
describe('when read only option is set', function () {
beforeEach(function (cb) {
helper.newPad(cb);
this.timeout(60000);
});
describe('the share link', function () {
it('shows a read only url', function (done) {
const chrome$ = helper.padChrome$;
// open share dropdown
chrome$('.buttonicon-embed').click();
chrome$('#readonlyinput').click();
chrome$('#readonlyinput:checkbox:not(:checked)').attr('checked', 'checked');
// get the link of the share field + the actual pad url and compare them
const shareLink = chrome$('#linkinput').val();
const containsReadOnlyLink = shareLink.indexOf('r.') > 0;
expect(containsReadOnlyLink).to.be(true);
done();
});
});
describe('the embed as iframe code', function () {
it('is an iframe with the the correct url parameters and correct size', function (done) {
const chrome$ = helper.padChrome$;
// open share dropdown
chrome$('.buttonicon-embed').click();
// check read only checkbox, a bit hacky
chrome$('#readonlyinput').click();
chrome$('#readonlyinput:checkbox:not(:checked)').attr('checked', 'checked');
// get the link of the share field + the actual pad url and compare them
const embedCode = chrome$('#embedinput').val();
checkiFrameCode(embedCode, true);
done();
});
});
});
});

View file

@ -0,0 +1,31 @@
'use strict';
describe('enter keystroke', function () {
// create a new pad before each test run
beforeEach(function (cb) {
helper.newPad(cb);
this.timeout(60000);
});
it('creates a new line & puts cursor onto a new line', function (done) {
const inner$ = helper.padInner$;
// get the first text element out of the inner iframe
const $firstTextElement = inner$('div').first();
// get the original string value minus the last char
const originalTextValue = $firstTextElement.text();
// simulate key presses to enter content
$firstTextElement.sendkeys('{enter}');
helper.waitFor(() => inner$('div').first().text() === '').done(() => {
const $newSecondLine = inner$('div').first().next();
const newFirstTextElementValue = inner$('div').first().text();
expect(newFirstTextElementValue).to.be(''); // expect the first line to be blank
// expect the second line to be the same as the original first line.
expect($newSecondLine.text()).to.be(originalTextValue);
done();
});
});
});

View file

@ -0,0 +1,34 @@
'use strict';
describe('font select', function () {
// create a new pad before each test run
beforeEach(function (cb) {
helper.newPad(cb);
this.timeout(60000);
});
it('makes text RobotoMono', function (done) {
const inner$ = helper.padInner$;
const chrome$ = helper.padChrome$;
// click on the settings button to make settings visible
const $settingsButton = chrome$('.buttonicon-settings');
$settingsButton.click();
// get the font menu and RobotoMono option
const $viewfontmenu = chrome$('#viewfontmenu');
// select RobotoMono and fire change event
// $RobotoMonooption.attr('selected','selected');
// commenting out above will break safari test
$viewfontmenu.val('RobotoMono');
$viewfontmenu.change();
// check if font changed to RobotoMono
const fontFamily = inner$('body').css('font-family').toLowerCase();
const containsStr = fontFamily.indexOf('robotomono');
expect(containsStr).to.not.be(-1);
done();
});
});

View file

@ -0,0 +1,468 @@
'use strict';
describe('the test helper', function () {
describe('the newPad method', function () {
xit("doesn't leak memory if you creates iframes over and over again", function (done) {
this.timeout(100000);
let times = 10;
const loadPad = () => {
helper.newPad(() => {
times--;
if (times > 0) {
loadPad();
} else {
done();
}
});
};
loadPad();
});
it('gives me 3 jquery instances of chrome, outer and inner', function (done) {
this.timeout(10000);
helper.newPad(() => {
// check if the jquery selectors have the desired elements
expect(helper.padChrome$('#editbar').length).to.be(1);
expect(helper.padOuter$('#outerdocbody').length).to.be(1);
expect(helper.padInner$('#innerdocbody').length).to.be(1);
// check if the document object was set correctly
expect(helper.padChrome$.window.document).to.be(helper.padChrome$.document);
expect(helper.padOuter$.window.document).to.be(helper.padOuter$.document);
expect(helper.padInner$.window.document).to.be(helper.padInner$.document);
done();
});
});
// Make sure the cookies are cleared, and make sure that the cookie
// clearing has taken effect at this point in the code. It has been
// observed that the former can happen without the latter if there
// isn't a timeout (within `newPad`) after clearing the cookies.
// However this doesn't seem to always be easily replicated, so this
// timeout may or may end up in the code. None the less, we test here
// to catch it if the bug comes up again.
it('clears cookies', function (done) {
this.timeout(60000);
// set cookies far into the future to make sure they're not expired yet
window.document.cookie = 'token=foo;expires=Thu, 01 Jan 3030 00:00:00 GMT; path=/';
window.document.cookie = 'language=bar;expires=Thu, 01 Jan 3030 00:00:00 GMT; path=/';
expect(window.document.cookie).to.contain('token=foo');
expect(window.document.cookie).to.contain('language=bar');
helper.newPad(() => {
// helper function seems to have cleared cookies
// NOTE: this doesn't yet mean it's proven to have taken effect by this point in execution
const firstCookie = window.document.cookie;
expect(firstCookie).to.not.contain('token=foo');
expect(firstCookie).to.not.contain('language=bar');
const chrome$ = helper.padChrome$;
// click on the settings button to make settings visible
const $userButton = chrome$('.buttonicon-showusers');
$userButton.click();
const $usernameInput = chrome$('#myusernameedit');
$usernameInput.click();
$usernameInput.val('John McLear');
$usernameInput.blur();
// Before refreshing, make sure the name is there
expect($usernameInput.val()).to.be('John McLear');
// Now that we have a chrome, we can set a pad cookie
// so we can confirm it gets wiped as well
chrome$.document.cookie = 'prefsHtml=baz;expires=Thu, 01 Jan 3030 00:00:00 GMT';
expect(chrome$.document.cookie).to.contain('prefsHtml=baz');
// Cookies are weird. Because it's attached to chrome$ (as helper.setPadCookies does)
// AND we didn't put path=/, we shouldn't expect it to be visible on
// window.document.cookie. Let's just be sure.
expect(window.document.cookie).to.not.contain('prefsHtml=baz');
setTimeout(() => { // give it a second to save the username on the server side
helper.newPad(() => { // get a new pad, let it clear the cookies
const chrome$ = helper.padChrome$;
// helper function seems to have cleared cookies
// NOTE: this doesn't yet mean cookies were cleared effectively.
// We still need to test below that we're in a new session
expect(window.document.cookie).to.not.contain('token=foo');
expect(window.document.cookie).to.not.contain('language=bar');
expect(chrome$.document.cookie).to.contain('prefsHtml=baz');
expect(window.document.cookie).to.not.contain('prefsHtml=baz');
expect(window.document.cookie).to.not.be(firstCookie);
// click on the settings button to make settings visible
const $userButton = chrome$('.buttonicon-showusers');
$userButton.click();
// confirm that the session was actually cleared
const $usernameInput = chrome$('#myusernameedit');
expect($usernameInput.val()).to.be('');
done();
});
}, 1000);
});
});
it('sets pad prefs cookie', function (done) {
this.timeout(60000);
helper.newPad({
padPrefs: {foo: 'bar'},
cb() {
const chrome$ = helper.padChrome$;
expect(chrome$.document.cookie).to.contain('prefsHttp=%7B%22');
expect(chrome$.document.cookie).to.contain('foo%22%3A%22bar');
done();
},
});
});
});
describe('the waitFor method', function () {
it('takes a timeout and waits long enough', function (done) {
this.timeout(2000);
const startTime = Date.now();
helper.waitFor(() => false, 1500).fail(() => {
const duration = Date.now() - startTime;
expect(duration).to.be.greaterThan(1490);
done();
});
});
it('takes an interval and checks on every interval', function (done) {
this.timeout(4000);
let checks = 0;
helper.waitFor(() => {
checks++;
return false;
}, 2000, 100).fail(() => {
// 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(15);
expect(checks).to.be.lessThan(24);
done();
});
});
it('rejects if the predicate throws', async function () {
let err;
await helper.waitFor(() => { throw new Error('test exception'); })
.fail(() => {}) // Suppress the redundant uncatchable exception.
.catch((e) => { err = e; });
expect(err).to.be.an(Error);
expect(err.message).to.be('test exception');
});
describe('returns a deferred object', function () {
it('it calls done after success', function (done) {
helper.waitFor(() => true).done(() => {
done();
});
});
it('calls fail after failure', function (done) {
helper.waitFor(() => false, 0).fail(() => {
done();
});
});
xit("throws if you don't listen for fails", function (done) {
const onerror = window.onerror;
window.onerror = function () {
window.onerror = onerror;
done();
};
helper.waitFor(() => false, 100);
});
});
describe('checks first then sleeps', function () {
it('resolves quickly if the predicate is immediately true', async function () {
const before = Date.now();
await helper.waitFor(() => true, 1000, 900);
expect(Date.now() - before).to.be.lessThan(800);
});
it('polls exactly once if timeout < interval', async function () {
let calls = 0;
await helper.waitFor(() => { calls++; }, 1, 1000)
.fail(() => {}) // Suppress the redundant uncatchable exception.
.catch(() => {}); // Don't throw an exception -- we know it rejects.
expect(calls).to.be(1);
});
it('resolves if condition is immediately true even if timeout is 0', async function () {
await helper.waitFor(() => true, 0);
});
});
});
describe('the waitForPromise method', function () {
it('returns a Promise', async function () {
expect(helper.waitForPromise(() => true)).to.be.a(Promise);
});
it('takes a timeout and waits long enough', async function () {
this.timeout(2000);
const startTime = Date.now();
let rejected;
await helper.waitForPromise(() => false, 1500)
.catch(() => { rejected = true; });
expect(rejected).to.be(true);
const duration = Date.now() - startTime;
expect(duration).to.be.greaterThan(1490);
});
it('takes an interval and checks on every interval', async function () {
this.timeout(4000);
let checks = 0;
let rejected;
await helper.waitForPromise(() => { checks++; return false; }, 2000, 100)
.catch(() => { rejected = true; });
expect(rejected).to.be(true);
// `checks` is expected to be 20 or 21: one at the beginning, plus 19 or 20 more depending on
// whether it's the timeout or the final poll that wins at 2000ms. Margin is added to reduce
// flakiness on slow test machines.
expect(checks).to.be.greaterThan(15);
expect(checks).to.be.lessThan(24);
});
});
describe('the selectLines method', function () {
// function to support tests, use a single way to represent whitespaces
const cleanText = function (text) {
return text
// IE replaces line breaks with a whitespace, so we need to unify its behavior
// for other browsers, to have all tests running for all browsers
.replace(/\n/gi, '')
.replace(/\s/gi, ' ');
};
before(function (done) {
helper.newPad(() => {
// create some lines to be used on the tests
const $firstLine = helper.padInner$('div').first();
$firstLine.sendkeys('{selectall}some{enter}short{enter}lines{enter}to test{enter}{enter}');
// wait for lines to be split
helper.waitFor(() => {
const $fourthLine = helper.padInner$('div').eq(3);
return $fourthLine.text() === 'to test';
}).done(done);
});
this.timeout(60000);
});
it('changes editor selection to be between startOffset of $startLine ' +
'and endOffset of $endLine', function (done) {
const inner$ = helper.padInner$;
const startOffset = 2;
const endOffset = 4;
const $lines = inner$('div');
const $startLine = $lines.eq(1);
const $endLine = $lines.eq(3);
helper.selectLines($startLine, $endLine, startOffset, endOffset);
const selection = inner$.document.getSelection();
/*
* replace() is required here because Firefox keeps the line breaks.
*
* I'm not sure this is ideal behavior of getSelection() where the text
* is not consistent between browsers but that's the situation so that's
* how I'm covering it in this test.
*/
expect(cleanText(selection.toString().replace(/(\r\n|\n|\r)/gm, ''))).to.be('ort lines to t');
done();
});
it('ends selection at beginning of $endLine when it is an empty line', function (done) {
const inner$ = helper.padInner$;
const startOffset = 2;
const endOffset = 1;
const $lines = inner$('div');
const $startLine = $lines.eq(1);
const $endLine = $lines.eq(4);
helper.selectLines($startLine, $endLine, startOffset, endOffset);
const selection = inner$.document.getSelection();
/*
* replace() is required here because Firefox keeps the line breaks.
*
* I'm not sure this is ideal behavior of getSelection() where the text
* is not consistent between browsers but that's the situation so that's
* how I'm covering it in this test.
*/
expect(cleanText(
selection.toString().replace(/(\r\n|\n|\r)/gm, ''))).to.be('ort lines to test');
done();
});
it('ends selection at beginning of $endLine when its offset is zero', function (done) {
const inner$ = helper.padInner$;
const startOffset = 2;
const endOffset = 0;
const $lines = inner$('div');
const $startLine = $lines.eq(1);
const $endLine = $lines.eq(3);
helper.selectLines($startLine, $endLine, startOffset, endOffset);
const selection = inner$.document.getSelection();
/*
* replace() is required here because Firefox keeps the line breaks.
*
* I'm not sure this is ideal behavior of getSelection() where the text
* is not consistent between browsers but that's the situation so that's
* how I'm covering it in this test.
*/
expect(cleanText(selection.toString().replace(/(\r\n|\n|\r)/gm, ''))).to.be('ort lines ');
done();
});
it('selects full line when offset is longer than line content', function (done) {
const inner$ = helper.padInner$;
const startOffset = 2;
const endOffset = 50;
const $lines = inner$('div');
const $startLine = $lines.eq(1);
const $endLine = $lines.eq(3);
helper.selectLines($startLine, $endLine, startOffset, endOffset);
const selection = inner$.document.getSelection();
/*
* replace() is required here because Firefox keeps the line breaks.
*
* I'm not sure this is ideal behavior of getSelection() where the text
* is not consistent between browsers but that's the situation so that's
* how I'm covering it in this test.
*/
expect(cleanText(
selection.toString().replace(/(\r\n|\n|\r)/gm, ''))).to.be('ort lines to test');
done();
});
it('selects all text between beginning of $startLine and end of $endLine ' +
'when no offset is provided', function (done) {
const inner$ = helper.padInner$;
const $lines = inner$('div');
const $startLine = $lines.eq(1);
const $endLine = $lines.eq(3);
helper.selectLines($startLine, $endLine);
const selection = inner$.document.getSelection();
/*
* replace() is required here because Firefox keeps the line breaks.
*
* I'm not sure this is ideal behavior of getSelection() where the text
* is not consistent between browsers but that's the situation so that's
* how I'm covering it in this test.
*/
expect(cleanText(
selection.toString().replace(/(\r\n|\n|\r)/gm, ''))).to.be('short lines to test');
done();
});
});
describe('helper', function () {
before(function (cb) {
helper.newPad(() => {
cb();
});
});
it('.textLines() returns the text of the pad as strings', async function () {
const lines = helper.textLines();
const 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 () {
const lines = helper.linesDiv();
const 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'), (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 () {
const 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 () {
const firstLine = helper.textLines()[0];
await helper.edit('second line', 2);
const 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('');
const noOfLines = helper.textLines().length;
await helper.edit('{enter}');
expect(helper.textLines().length).to.be(noOfLines + 1);
});
});
});

View file

@ -0,0 +1,329 @@
'use strict';
describe('import functionality', function () {
beforeEach(function (cb) {
helper.newPad(cb); // creates a new pad
this.timeout(60000);
});
function getinnertext() {
const inner = helper.padInner$;
if (!inner) {
return '';
}
let newtext = '';
inner('div').each((line, el) => {
newtext += `${el.innerHTML}\n`;
});
return newtext;
}
function importrequest(data, importurl, type) {
let error;
const result = $.ajax({
url: importurl,
type: 'post',
processData: false,
async: false,
contentType: 'multipart/form-data; boundary=boundary',
accepts: {
text: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
},
data: [
'Content-Type: multipart/form-data; boundary=--boundary',
'',
'--boundary',
`Content-Disposition: form-data; name="file"; filename="import.${type}"`,
'Content-Type: text/plain',
'',
data,
'',
'--boundary',
].join('\r\n'),
error(res) {
error = res;
},
});
expect(error).to.be(undefined);
return result;
}
function exportfunc(link) {
const exportresults = [];
$.ajaxSetup({
async: false,
});
$.get(`${link}/export/html`, (data) => {
const start = data.indexOf('<body>');
const end = data.indexOf('</body>');
const html = data.substr(start + 6, end - start - 6);
exportresults.push(['html', html]);
});
$.get(`${link}/export/txt`, (data) => {
exportresults.push(['txt', data]);
});
return exportresults;
}
xit('import a pad with newlines from txt', function (done) {
const importurl = `${helper.padChrome$.window.location.href}/import`;
const textWithNewLines = 'imported text\nnewline';
importrequest(textWithNewLines, importurl, 'txt');
helper.waitFor(() => expect(getinnertext())
.to.be('<span class="">imported text</span>\n<span class="">newline</span>\n<br>\n'));
const results = exportfunc(helper.padChrome$.window.location.href);
expect(results[0][1]).to.be('imported text<br>newline<br><br>');
expect(results[1][1]).to.be('imported text\nnewline\n\n');
done();
});
xit('import a pad with newlines from html', function (done) {
const importurl = `${helper.padChrome$.window.location.href}/import`;
const htmlWithNewLines = '<html><body>htmltext<br/>newline</body></html>';
importrequest(htmlWithNewLines, importurl, 'html');
helper.waitFor(() => expect(getinnertext())
.to.be('<span class="">htmltext</span>\n<span class="">newline</span>\n<br>\n'));
const results = exportfunc(helper.padChrome$.window.location.href);
expect(results[0][1]).to.be('htmltext<br>newline<br><br>');
expect(results[1][1]).to.be('htmltext\nnewline\n\n');
done();
});
xit('import a pad with attributes from html', function (done) {
const importurl = `${helper.padChrome$.window.location.href}/import`;
const htmlWithNewLines = '<html><body>htmltext<br/><span class="b s i u">' +
'<b><i><s><u>newline</u></s></i></b></body></html>';
importrequest(htmlWithNewLines, importurl, 'html');
helper.waitFor(() => expect(getinnertext())
.to.be('<span class="">htmltext</span>\n<span class="b i s u">' +
'<b><i><s><u>newline</u></s></i></b></span>\n<br>\n'));
const results = exportfunc(helper.padChrome$.window.location.href);
expect(results[0][1])
.to.be('htmltext<br><strong><em><s><u>newline</u></s></em></strong><br><br>');
expect(results[1][1]).to.be('htmltext\nnewline\n\n');
done();
});
xit('import a pad with bullets from html', function (done) {
const importurl = `${helper.padChrome$.window.location.href}/import`;
const htmlWithBullets = '<html><body><ul class="list-bullet1"><li>bullet line 1</li>' +
'<li>bullet line 2</li><ul class="list-bullet2"><li>bullet2 line 1</li>' +
'<li>bullet2 line 2</li></ul></ul></body></html>';
importrequest(htmlWithBullets, importurl, 'html');
helper.waitFor(() => expect(getinnertext()).to.be(
'<ul class="list-bullet1"><li><span class="">bullet line 1</span></li></ul>\n' +
'<ul class="list-bullet1"><li><span class="">bullet line 2</span></li></ul>\n' +
'<ul class="list-bullet2"><li><span class="">bullet2 line 1</span></li></ul>\n' +
'<ul class="list-bullet2"><li><span class="">bullet2 line 2</span></li></ul>\n' +
'<br>\n'));
const results = exportfunc(helper.padChrome$.window.location.href);
expect(results[0][1]).to.be(
'<ul class="bullet"><li>bullet line 1</li><li>bullet line 2</li>' +
'<ul class="bullet"><li>bullet2 line 1</li><li>bullet2 line 2</li></ul></ul><br>');
expect(results[1][1])
.to.be('\t* bullet line 1\n\t* bullet line 2\n' +
'\t\t* bullet2 line 1\n\t\t* bullet2 line 2\n\n');
done();
});
xit('import a pad with bullets and newlines from html', function (done) {
const importurl = `${helper.padChrome$.window.location.href}/import`;
const htmlWithBullets = '<html><body><ul class="list-bullet1"><li>bullet line 1</li>' +
'</ul><br/><ul class="list-bullet1"><li>bullet line 2</li><ul class="list-bullet2">' +
'<li>bullet2 line 1</li></ul></ul><br/><ul class="list-bullet1">' +
'<ul class="list-bullet2"><li>bullet2 line 2</li></ul></ul></body></html>';
importrequest(htmlWithBullets, importurl, 'html');
helper.waitFor(() => expect(getinnertext()).to.be(
'<ul class="list-bullet1"><li><span class="">bullet line 1</span></li></ul>\n' +
'<br>\n' +
'<ul class="list-bullet1"><li><span class="">bullet line 2</span></li></ul>\n' +
'<ul class="list-bullet2"><li><span class="">bullet2 line 1</span></li></ul>\n' +
'<br>\n' +
'<ul class="list-bullet2"><li><span class="">bullet2 line 2</span></li></ul>\n' +
'<br>\n'));
const results = exportfunc(helper.padChrome$.window.location.href);
expect(results[0][1]).to.be(
'<ul class="bullet"><li>bullet line 1</li></ul><br><ul class="bullet">' +
'<li>bullet line 2</li><ul class="bullet"><li>bullet2 line 1</li></ul>' +
'</ul><br><ul><ul class="bullet"><li>bullet2 line 2</li></ul></ul><br>');
expect(results[1][1]).to.be(
'\t* bullet line 1\n\n\t* bullet line 2\n\t\t* bullet2 line 1\n\n\t\t* bullet2 line 2\n\n');
done();
});
xit('import a pad with bullets and newlines and attributes from html', function (done) {
const importurl = `${helper.padChrome$.window.location.href}/import`;
const htmlWithBullets = '<html><body><ul class="list-bullet1"><li>bullet line 1</li>' +
'</ul><br/><ul class="list-bullet1"><li>bullet line 2</li>' +
'<ul class="list-bullet2"><li>bullet2 line 1</li></ul></ul>' +
'<br/><ul class="list-bullet1"><ul class="list-bullet2"><ul class="list-bullet3">' +
'<ul class="list-bullet4"><li><span class="b s i u"><b><i>' +
'<s><u>bullet4 line 2 bisu</u></s></i></b></span></li><li>' +
'<span class="b s "><b><s>bullet4 line 2 bs</s></b></span></li>' +
'<li><span class="u"><u>bullet4 line 2 u</u></span><span class="u i s">' +
'<i><s><u>uis</u></s></i></span></li></ul></ul></ul></ul></body></html>';
importrequest(htmlWithBullets, importurl, 'html');
helper.waitFor(() => expect(getinnertext()).to.be(
'<ul class="list-bullet1"><li><span class="">bullet line 1</span></li></ul>\n<br>\n' +
'<ul class="list-bullet1"><li><span class="">bullet line 2</span></li></ul>\n' +
'<ul class="list-bullet2"><li><span class="">bullet2 line 1</span></li></ul>\n<br>\n' +
'<ul class="list-bullet4"><li><span class="b i s u">' +
'<b><i><s><u>bullet4 line 2 bisu</u></s></i></b></span></li></ul>\n' +
'<ul class="list-bullet4"><li><span class="b s">' +
'<b><s>bullet4 line 2 bs</s></b></span></li></ul>\n' +
'<ul class="list-bullet4"><li><span class="u"><u>bullet4 line 2 u</u>' +
'</span><span class="i s u"><i><s><u>uis</u></s></i></span></li></ul>\n' +
'<br>\n'));
const results = exportfunc(helper.padChrome$.window.location.href);
expect(results[0][1]).to.be(
'<ul class="bullet"><li>bullet line 1</li></ul>' +
'<br><ul class="bullet"><li>bullet line 2</li><ul class="bullet"><li>bullet2 line 1</li>' +
'</ul></ul><br><ul><ul><ul><ul class="bullet"><li><strong><em><s><u>bullet4 line 2 bisu' +
'</u></s></em></strong></li><li><strong><s>bullet4 line 2 bs</s></strong>' +
'</li><li><u>bullet4 line 2 u<em><s>uis</s></em></u></li></ul></ul></ul></ul><br>');
expect(results[1][1]).to.be(
'\t* bullet line 1\n\n\t* bullet line 2\n\t\t* bullet2 line 1\n\n\t\t\t\t* bullet4 line 2' +
' bisu\n\t\t\t\t* bullet4 line 2 bs\n\t\t\t\t* bullet4 line 2 uuis\n\n');
done();
});
xit('import a pad with nested bullets from html', function (done) {
const importurl = `${helper.padChrome$.window.location.href}/import`;
const htmlWithBullets = '<html><body><ul class="list-bullet1"><li>bullet line 1</li>' +
'</ul><ul class="list-bullet1"><li>bullet line 2</li><ul class="list-bullet2">' +
'<li>bullet2 line 1</li></ul></ul><ul class="list-bullet1"><ul class="list-bullet2">' +
'<ul class="list-bullet3"><ul class="list-bullet4"><li>bullet4 line 2</li>' +
'<li>bullet4 line 2</li><li>bullet4 line 2</li></ul><li>bullet3 line 1</li></ul>' +
'</ul><li>bullet2 line 1</li></ul></body></html>';
importrequest(htmlWithBullets, importurl, 'html');
const oldtext = getinnertext();
helper.waitFor(() => oldtext !== getinnertext()
// return expect(getinnertext()).to.be('\
// <ul class="list-bullet1"><li><span class="">bullet line 1</span></li></ul>\n\
// <ul class="list-bullet1"><li><span class="">bullet line 2</span></li></ul>\n\
// <ul class="list-bullet2"><li><span class="">bullet2 line 1</span></li></ul>\n\
// <ul class="list-bullet4"><li><span class="">bullet4 line 2</span></li></ul>\n\
// <ul class="list-bullet4"><li><span class="">bullet4 line 2</span></li></ul>\n\
// <ul class="list-bullet4"><li><span class="">bullet4 line 2</span></li></ul>\n\
// <br>\n')
);
const results = exportfunc(helper.padChrome$.window.location.href);
expect(results[0][1]).to.be(
'<ul class="bullet"><li>bullet line 1</li><li>bullet line 2</li>' +
'<ul class="bullet"><li>bullet2 line 1</li><ul><ul class="bullet"><li>bullet4 line 2</li>' +
'<li>bullet4 line 2</li><li>bullet4 line 2</li></ul><li>bullet3 line 1</li></ul></ul>' +
'<li>bullet2 line 1</li></ul><br>');
expect(results[1][1]).to.be(
'\t* bullet line 1\n\t* bullet line 2\n\t\t* bullet2 line 1\n\t\t\t\t* bullet4 line 2' +
'\n\t\t\t\t* bullet4 line 2\n\t\t\t\t* bullet4 line 2\n\t\t\t* bullet3 line 1' +
'\n\t* bullet2 line 1\n\n');
done();
});
xit('import with 8 levels of bullets and newlines and attributes from html', function (done) {
const importurl = `${helper.padChrome$.window.location.href}/import`;
const htmlWithBullets =
'<html><body><ul class="list-bullet1"><li>bullet line 1</li>' +
'</ul><br/><ul class="list-bullet1"><li>bullet line 2</li><ul class="list-bullet2"><li>' +
'bullet2 line 1</li></ul></ul><br/><ul class="list-bullet1"><ul class="list-bullet2">' +
'<ul class="list-bullet3"><ul class="list-bullet4"><li><span class="b s i u"><b><i>' +
'<s><u>bullet4 line 2 bisu</u></s></i></b></span></li><li><span class="b s "><b><s>' +
'bullet4 line 2 bs</s></b></span></li><li><span class="u"><u>bullet4 line 2 u' +
'</u></span><span class="u i s"><i><s><u>uis</u></s></i></span></li>' +
'<ul class="list-bullet5"><ul class="list-bullet6"><ul class="list-bullet7">' +
'<ul class="list-bullet8"><li><span class="">foo</span></li><li><span class="b s">' +
'<b><s>foobar bs</b></s></span></li></ul></ul></ul></ul><ul class="list-bullet5">' +
'<li>foobar</li></ul></ul></ul></ul></body></html>';
importrequest(htmlWithBullets, importurl, 'html');
helper.waitFor(() => expect(getinnertext()).to.be(
'<ul class="list-bullet1"><li><span class="">bullet line 1</span></li></ul>\n<br>\n' +
'<ul class="list-bullet1"><li><span class="">bullet line 2</span></li></ul>\n' +
'<ul class="list-bullet2"><li><span class="">bullet2 line 1</span></li></ul>\n<br>\n' +
'<ul class="list-bullet4"><li><span class="b i s u"><b><i><s><u>bullet4 line 2 bisu</u>' +
'</s></i></b></span></li></ul>\n' +
'<ul class="list-bullet4"><li><span class="b s"><b><s>bullet4 line 2 bs</s></b>' +
'</span></li></ul>\n' +
'<ul class="list-bullet4"><li><span class="u"><u>bullet4 line 2 u</u></span>' +
'<span class="i s u"><i><s><u>uis</u></s>' +
'</i></span></li></ul>\n' +
'<ul class="list-bullet8"><li><span class="">foo</span></li></ul>\n' +
'<ul class="list-bullet8"><li><span class="b s"><b><s>foobar bs</s></b>' +
'</span></li></ul>\n' +
'<ul class="list-bullet5"><li><span class="">foobar</span></li></ul>\n' +
'<br>\n'));
const results = exportfunc(helper.padChrome$.window.location.href);
expect(results[0][1]).to.be(
'<ul class="bullet"><li>bullet line 1</li></ul><br><ul class="bullet">' +
'<li>bullet line 2</li><ul class="bullet"><li>bullet2 line 1</li></ul></ul>' +
'<br><ul><ul><ul><ul class="bullet"><li><strong><em><s><u>' +
'bullet4 line 2 bisu</u></s></em></strong></li><li><strong><s>' +
'bullet4 line 2 bs</s></strong></li><li><u>bullet4 line 2 u<em>' +
'<s>uis</s></em></u></li><ul><ul><ul><ul class="bullet"><li>foo</li>' +
'<li><strong><s>foobar bs</s></strong></li></ul></ul></ul><li>foobar</li>' +
'</ul></ul></ul></ul></ul><br>');
expect(results[1][1]).to.be(
'\t* bullet line 1\n\n\t* bullet line 2\n\t\t* ' +
'bullet2 line 1\n\n\t\t\t\t* bullet4 line 2 bisu\n\t\t\t\t* bullet4 line 2 ' +
'bs\n\t\t\t\t* bullet4 line 2 uuis\n\t\t\t\t\t\t\t\t* foo\n\t\t\t\t\t\t\t\t* ' +
'foobar bs\n\t\t\t\t\t* foobar\n\n');
done();
});
xit('import a pad with ordered lists from html', function (done) {
const importurl = `${helper.padChrome$.window.location.href}/import`;
const htmlWithBullets = '<html><body><ol class="list-number1" start="1">' +
'<li>number 1 line 1</li></ol><ol class="list-number1" start="2">' +
'<li>number 2 line 2</li></ol></body></html>';
importrequest(htmlWithBullets, importurl, 'html');
console.error(getinnertext());
expect(getinnertext()).to.be(
'<ol class="list-number1" start="1"><li><span class="">number 1 line 1</span></li></ol>\n' +
'<ol class="list-number1" start="2"><li><span class="">number 2 line 2</span></li></ol>\n' +
'<br>\n');
const results = exportfunc(helper.padChrome$.window.location.href);
expect(results[0][1]).to.be(
'<ol class="list-number1" start="1"><li>number 1 line 1</li>' +
'</ol><ol class="list-number1" start="2"><li>number 2 line 2</li></ol>');
expect(results[1][1]).to.be('');
done();
});
xit('import a pad with ordered lists and newlines from html', function (done) {
const importurl = `${helper.padChrome$.window.location.href}/import`;
const htmlWithBullets = '<html><body><ol class="list-number1" start="1">' +
'<li>number 9 line 1</li></ol><br/><ol class="list-number1" start="2">' +
'<li>number 10 line 2</li><ol class="list-number2">' +
'<li>number 2 times line 1</li></ol></ol><br/><ol class="list-bullet1">' +
'<ol class="list-number2"><li>number 2 times line 2</li></ol></ol></body></html>';
importrequest(htmlWithBullets, importurl, 'html');
expect(getinnertext()).to.be(
'<ol class="list-number1" start="1"><li><span class="">number 9 line 1</span></li></ol>\n' +
'<br>\n' +
'<ol class="list-number1" start="2"><li><span class="">number 10 line 2</span></li>' +
'</ol>\n' +
'<ol class="list-number2"><li><span class="">number 2 times line 1</span></li></ol>\n' +
'<br>\n' +
'<ol class="list-number2"><li><span class="">number 2 times line 2</span></li></ol>\n' +
'<br>\n');
const results = exportfunc(helper.padChrome$.window.location.href);
console.error(results);
done();
});
xit('import with nested ordered lists and attributes and newlines from html', function (done) {
const importurl = `${helper.padChrome$.window.location.href}/import`;
const htmlWithBullets = '<html><body><ol class="list-number1" start="1"><li>' +
'<span class="b s i u"><b><i><s><u>bold strikethrough italics underline</u>' +
'</s><i/></b></span> line <span class="b"><b>1bold</b></span></li>' +
'</ol><br/><span class="i"><i><ol class="list-number1" start="2">' +
'<li>number 10 line 2</li><ol class="list-number2">' +
'<li>number 2 times line 1</li></ol></ol></i></span><br/>' +
'<ol class="list-bullet1"><ol class="list-number2">' +
'<li>number 2 times line 2</li></ol></ol></body></html>';
importrequest(htmlWithBullets, importurl, 'html');
expect(getinnertext()).to.be(
'<ol class="list-number1"><li><span class="b i s u"><b><i><s><u>' +
'bold strikethrough italics underline</u></s></i></b></span><span class="">' +
' line </span><span class="b"><b>1bold</b></span></li></ol>\n' +
'<br>\n' +
'<ol class="list-number1"><li><span class="i"><i>number 10 line 2</i></span></li></ol>\n' +
'<ol class="list-number2"><li><span class="i">' +
'<i>number 2 times line 1</i></span></li></ol>\n' +
'<br>\n' +
'<ol class="list-number2"><li><span class="">number 2 times line 2</span></li></ol>\n' +
'<br>\n');
const results = exportfunc(helper.padChrome$.window.location.href);
console.error(results);
done();
});
});

View file

@ -0,0 +1,129 @@
'use strict';
describe('import indents functionality', function () {
beforeEach(function (cb) {
helper.newPad(cb); // creates a new pad
this.timeout(60000);
});
function getinnertext() {
const inner = helper.padInner$;
let newtext = '';
inner('div').each((line, el) => {
newtext += `${el.innerHTML}\n`;
});
return newtext;
}
function importrequest(data, importurl, type) {
let error;
const result = $.ajax({
url: importurl,
type: 'post',
processData: false,
async: false,
contentType: 'multipart/form-data; boundary=boundary',
accepts: {
text: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
},
data: [
'Content-Type: multipart/form-data; boundary=--boundary',
'',
'--boundary',
`Content-Disposition: form-data; name="file"; filename="import.${type}"`,
'Content-Type: text/plain',
'',
data,
'',
'--boundary',
].join('\r\n'),
error(res) {
error = res;
},
});
expect(error).to.be(undefined);
return result;
}
function exportfunc(link) {
const exportresults = [];
$.ajaxSetup({
async: false,
});
$.get(`${link}/export/html`, (data) => {
const start = data.indexOf('<body>');
const end = data.indexOf('</body>');
const html = data.substr(start + 6, end - start - 6);
exportresults.push(['html', html]);
});
$.get(`${link}/export/txt`, (data) => {
exportresults.push(['txt', data]);
});
return exportresults;
}
xit('import a pad with indents from html', function (done) {
const importurl = `${helper.padChrome$.window.location.href}/import`;
/* eslint-disable-next-line max-len */
const htmlWithIndents = '<html><body><ul class="list-indent1"><li>indent line 1</li><li>indent line 2</li><ul class="list-indent2"><li>indent2 line 1</li><li>indent2 line 2</li></ul></ul></body></html>';
importrequest(htmlWithIndents, importurl, 'html');
helper.waitFor(() => expect(getinnertext()).to.be(
'<ul class="list-indent1"><li><span class="">indent line 1</span></li></ul>\n' +
'<ul class="list-indent1"><li><span class="">indent line 2</span></li></ul>\n' +
'<ul class="list-indent2"><li><span class="">indent2 line 1</span></li></ul>\n' +
'<ul class="list-indent2"><li><span class="">indent2 line 2</span></li></ul>\n' +
'<br>\n'));
const results = exportfunc(helper.padChrome$.window.location.href);
/* eslint-disable-next-line max-len */
expect(results[0][1]).to.be('<ul class="indent"><li>indent line 1</li><li>indent line 2</li><ul class="indent"><li>indent2 line 1</li><li>indent2 line 2</li></ul></ul><br>');
expect(results[1][1])
.to.be('\tindent line 1\n\tindent line 2\n\t\tindent2 line 1\n\t\tindent2 line 2\n\n');
done();
});
xit('import a pad with indented lists and newlines from html', function (done) {
const importurl = `${helper.padChrome$.window.location.href}/import`;
/* eslint-disable-next-line max-len */
const htmlWithIndents = '<html><body><ul class="list-indent1"><li>indent line 1</li></ul><br/><ul class="list-indent1"><li>indent 1 line 2</li><ul class="list-indent2"><li>indent 2 times line 1</li></ul></ul><br/><ul class="list-indent1"><ul class="list-indent2"><li>indent 2 times line 2</li></ul></ul></body></html>';
importrequest(htmlWithIndents, importurl, 'html');
helper.waitFor(() => expect(getinnertext()).to.be(
'<ul class="list-indent1"><li><span class="">indent line 1</span></li></ul>\n' +
'<br>\n' +
'<ul class="list-indent1"><li><span class="">indent 1 line 2</span></li></ul>\n' +
'<ul class="list-indent2"><li><span class="">indent 2 times line 1</span></li></ul>\n' +
'<br>\n' +
'<ul class="list-indent2"><li><span class="">indent 2 times line 2</span></li></ul>\n' +
'<br>\n'));
const results = exportfunc(helper.padChrome$.window.location.href);
/* eslint-disable-next-line max-len */
expect(results[0][1]).to.be('<ul class="indent"><li>indent line 1</li></ul><br><ul class="indent"><li>indent 1 line 2</li><ul class="indent"><li>indent 2 times line 1</li></ul></ul><br><ul><ul class="indent"><li>indent 2 times line 2</li></ul></ul><br>');
/* eslint-disable-next-line max-len */
expect(results[1][1]).to.be('\tindent line 1\n\n\tindent 1 line 2\n\t\tindent 2 times line 1\n\n\t\tindent 2 times line 2\n\n');
done();
});
xit('import with 8 levels of indents and newlines and attributes from html', function (done) {
const importurl = `${helper.padChrome$.window.location.href}/import`;
/* eslint-disable-next-line max-len */
const htmlWithIndents = '<html><body><ul class="list-indent1"><li>indent line 1</li></ul><br/><ul class="list-indent1"><li>indent line 2</li><ul class="list-indent2"><li>indent2 line 1</li></ul></ul><br/><ul class="list-indent1"><ul class="list-indent2"><ul class="list-indent3"><ul class="list-indent4"><li><span class="b s i u"><b><i><s><u>indent4 line 2 bisu</u></s></i></b></span></li><li><span class="b s "><b><s>indent4 line 2 bs</s></b></span></li><li><span class="u"><u>indent4 line 2 u</u></span><span class="u i s"><i><s><u>uis</u></s></i></span></li><ul class="list-indent5"><ul class="list-indent6"><ul class="list-indent7"><ul class="list-indent8"><li><span class="">foo</span></li><li><span class="b s"><b><s>foobar bs</b></s></span></li></ul></ul></ul></ul><ul class="list-indent5"><li>foobar</li></ul></ul></ul></ul></body></html>';
importrequest(htmlWithIndents, importurl, 'html');
helper.waitFor(() => expect(getinnertext()).to.be(
'<ul class="list-indent1"><li><span class="">indent line 1</span></li></ul>\n<br>\n' +
'<ul class="list-indent1"><li><span class="">indent line 2</span></li></ul>\n' +
'<ul class="list-indent2"><li><span class="">indent2 line 1</span></li></ul>\n<br>\n' +
'<ul class="list-indent4"><li><span class="b i s u"><b><i><s><u>indent4 ' +
'line 2 bisu</u></s></i></b></span></li></ul>\n' +
'<ul class="list-indent4"><li><span class="b s"><b><s>' +
'indent4 line 2 bs</s></b></span></li></ul>\n' +
'<ul class="list-indent4"><li><span class="u"><u>indent4 line 2 u</u>' +
'</span><span class="i s u"><i><s><u>uis</u></s></i></span></li></ul>\n' +
'<ul class="list-indent8"><li><span class="">foo</span></li></ul>\n' +
'<ul class="list-indent8"><li><span class="b s"><b><s>foobar bs</s></b>' +
'</span></li></ul>\n' +
'<ul class="list-indent5"><li><span class="">foobar</span></li></ul>\n' +
'<br>\n'));
const results = exportfunc(helper.padChrome$.window.location.href);
/* eslint-disable-next-line max-len */
expect(results[0][1]).to.be('<ul class="indent"><li>indent line 1</li></ul><br><ul class="indent"><li>indent line 2</li><ul class="indent"><li>indent2 line 1</li></ul></ul><br><ul><ul><ul><ul class="indent"><li><strong><em><s><u>indent4 line 2 bisu</u></s></em></strong></li><li><strong><s>indent4 line 2 bs</s></strong></li><li><u>indent4 line 2 u<em><s>uis</s></em></u></li><ul><ul><ul><ul class="indent"><li>foo</li><li><strong><s>foobar bs</s></strong></li></ul></ul></ul><li>foobar</li></ul></ul></ul></ul></ul><br>');
/* eslint-disable-next-line max-len */
expect(results[1][1]).to.be('\tindent line 1\n\n\tindent line 2\n\t\tindent2 line 1\n\n\t\t\t\tindent4 line 2 bisu\n\t\t\t\tindent4 line 2 bs\n\t\t\t\tindent4 line 2 uuis\n\t\t\t\t\t\t\t\tfoo\n\t\t\t\t\t\t\t\tfoobar bs\n\t\t\t\t\tfoobar\n\n');
done();
});
});

View file

@ -0,0 +1,315 @@
'use strict';
describe('indentation button', function () {
// create a new pad before each test run
beforeEach(function (cb) {
helper.newPad(cb);
this.timeout(60000);
});
it('indent text with keypress', function (done) {
const inner$ = helper.padInner$;
// get the first text element out of the inner iframe
const $firstTextElement = inner$('div').first();
// select this text element
$firstTextElement.sendkeys('{selectall}');
const e = new inner$.Event(helper.evtType);
e.keyCode = 9; // tab :|
inner$('#innerdocbody').trigger(e);
helper.waitFor(() => inner$('div').first().find('ul li').length === 1).done(done);
});
it('indent text with button', function (done) {
const inner$ = helper.padInner$;
const chrome$ = helper.padChrome$;
const $indentButton = chrome$('.buttonicon-indent');
$indentButton.click();
helper.waitFor(() => inner$('div').first().find('ul li').length === 1).done(done);
});
it('keeps the indent on enter for the new line', function (done) {
const inner$ = helper.padInner$;
const chrome$ = helper.padChrome$;
const $indentButton = chrome$('.buttonicon-indent');
$indentButton.click();
// type a bit, make a line break and type again
const $firstTextElement = inner$('div span').first();
$firstTextElement.sendkeys('line 1');
$firstTextElement.sendkeys('{enter}');
$firstTextElement.sendkeys('line 2');
$firstTextElement.sendkeys('{enter}');
helper.waitFor(() => inner$('div span').first().text().indexOf('line 2') === -1).done(() => {
const $newSecondLine = inner$('div').first().next();
const hasULElement = $newSecondLine.find('ul li').length === 1;
expect(hasULElement).to.be(true);
expect($newSecondLine.text()).to.be('line 2');
done();
});
});
it('indents text with spaces on enter if previous line ends ' +
"with ':', '[', '(', or '{'", function (done) {
const inner$ = helper.padInner$;
// type a bit, make a line break and type again
const $firstTextElement = inner$('div').first();
$firstTextElement.sendkeys("line with ':'{enter}");
$firstTextElement.sendkeys("line with '['{enter}");
$firstTextElement.sendkeys("line with '('{enter}");
$firstTextElement.sendkeys("line with '{{}'{enter}");
helper.waitFor(() => {
// wait for Etherpad to split four lines into separated divs
const $fourthLine = inner$('div').first().next().next().next();
return $fourthLine.text().indexOf("line with '{'") === 0;
}).done(() => {
// we validate bottom to top for easier implementation
// curly braces
const $lineWithCurlyBraces = inner$('div').first().next().next().next();
$lineWithCurlyBraces.sendkeys('{{}');
// cannot use sendkeys('{enter}') here, browser does not read the command properly
pressEnter();
const $lineAfterCurlyBraces = inner$('div').first().next().next().next().next();
expect($lineAfterCurlyBraces.text()).to.match(/\s{4}/); // tab === 4 spaces
// parenthesis
const $lineWithParenthesis = inner$('div').first().next().next();
$lineWithParenthesis.sendkeys('(');
pressEnter();
const $lineAfterParenthesis = inner$('div').first().next().next().next();
expect($lineAfterParenthesis.text()).to.match(/\s{4}/);
// bracket
const $lineWithBracket = inner$('div').first().next();
$lineWithBracket.sendkeys('[');
pressEnter();
const $lineAfterBracket = inner$('div').first().next().next();
expect($lineAfterBracket.text()).to.match(/\s{4}/);
// colon
const $lineWithColon = inner$('div').first();
$lineWithColon.sendkeys(':');
pressEnter();
const $lineAfterColon = inner$('div').first().next();
expect($lineAfterColon.text()).to.match(/\s{4}/);
done();
});
});
it('appends indentation to the indent of previous line if previous line ends ' +
"with ':', '[', '(', or '{'", function (done) {
const inner$ = helper.padInner$;
// type a bit, make a line break and type again
const $firstTextElement = inner$('div').first();
$firstTextElement.sendkeys(" line with some indentation and ':'{enter}");
$firstTextElement.sendkeys('line 2{enter}');
helper.waitFor(() => {
// wait for Etherpad to split two lines into separated divs
const $secondLine = inner$('div').first().next();
return $secondLine.text().indexOf('line 2') === 0;
}).done(() => {
const $lineWithColon = inner$('div').first();
$lineWithColon.sendkeys(':');
pressEnter();
const $lineAfterColon = inner$('div').first().next();
// previous line indentation + regular tab (4 spaces)
expect($lineAfterColon.text()).to.match(/\s{6}/);
done();
});
});
it("issue #2772 shows '*' when multiple indented lines " +
' receive a style and are outdented', async function () {
const inner$ = helper.padInner$;
const chrome$ = helper.padChrome$;
// make sure pad has more than one line
inner$('div').first().sendkeys('First{enter}Second{enter}');
await helper.waitForPromise(() => inner$('div').first().text().trim() === 'First');
// indent first 2 lines
const $lines = inner$('div');
const $firstLine = $lines.first();
let $secondLine = $lines.slice(1, 2);
helper.selectLines($firstLine, $secondLine);
const $indentButton = chrome$('.buttonicon-indent');
$indentButton.click();
await helper.waitForPromise(() => inner$('div').first().find('ul li').length === 1);
// apply bold
const $boldButton = chrome$('.buttonicon-bold');
$boldButton.click();
await helper.waitForPromise(() => inner$('div').first().find('b').length === 1);
// outdent first 2 lines
const $outdentButton = chrome$('.buttonicon-outdent');
$outdentButton.click();
await helper.waitForPromise(() => inner$('div').first().find('ul li').length === 0);
// check if '*' is displayed
$secondLine = inner$('div').slice(1, 2);
expect($secondLine.text().trim()).to.be('Second');
});
/*
it("makes text indented and outdented", function() {
//get the inner iframe
var $inner = testHelper.$getPadInner();
//get the first text element out of the inner iframe
var firstTextElement = $inner.find("div").first();
//select this text element
testHelper.selectText(firstTextElement[0], $inner);
//get the indentation button and click it
var $indentButton = testHelper.$getPadChrome().find(".buttonicon-indent");
$indentButton.click();
var newFirstTextElement = $inner.find("div").first();
// is there a list-indent class element now?
var firstChild = newFirstTextElement.children(":first");
var isUL = firstChild.is('ul');
//expect it to be the beginning of a list
expect(isUL).to.be(true);
var secondChild = firstChild.children(":first");
var isLI = secondChild.is('li');
//expect it to be part of a list
expect(isLI).to.be(true);
//indent again
$indentButton.click();
var newFirstTextElement = $inner.find("div").first();
// is there a list-indent class element now?
var firstChild = newFirstTextElement.children(":first");
var hasListIndent2 = firstChild.hasClass('list-indent2');
//expect it to be part of a list
expect(hasListIndent2).to.be(true);
//make sure the text hasn't changed
expect(newFirstTextElement.text()).to.eql(firstTextElement.text());
// test outdent
//get the unindentation button and click it twice
var $outdentButton = testHelper.$getPadChrome().find(".buttonicon-outdent");
$outdentButton.click();
$outdentButton.click();
var newFirstTextElement = $inner.find("div").first();
// is there a list-indent class element now?
var firstChild = newFirstTextElement.children(":first");
var isUL = firstChild.is('ul');
//expect it not to be the beginning of a list
expect(isUL).to.be(false);
var secondChild = firstChild.children(":first");
var isLI = secondChild.is('li');
//expect it to not be part of a list
expect(isLI).to.be(false);
//make sure the text hasn't changed
expect(newFirstTextElement.text()).to.eql(firstTextElement.text());
// Next test tests multiple line indentation
//select this text element
testHelper.selectText(firstTextElement[0], $inner);
//indent twice
$indentButton.click();
$indentButton.click();
//get the first text element out of the inner iframe
var firstTextElement = $inner.find("div").first();
//select this text element
testHelper.selectText(firstTextElement[0], $inner);
/* this test creates the below content, both should have double indentation
line1
line2
firstTextElement.sendkeys('{rightarrow}'); // simulate a keypress of enter
firstTextElement.sendkeys('{enter}'); // simulate a keypress of enter
firstTextElement.sendkeys('line 1'); // simulate writing the first line
firstTextElement.sendkeys('{enter}'); // simulate a keypress of enter
firstTextElement.sendkeys('line 2'); // simulate writing the second line
//get the second text element out of the inner iframe
setTimeout(function(){ // THIS IS REALLY BAD
var secondTextElement = $('iframe').contents()
.find('iframe').contents()
.find('iframe').contents().find('body > div').get(1); // THIS IS UGLY
// is there a list-indent class element now?
var firstChild = secondTextElement.children(":first");
var isUL = firstChild.is('ul');
//expect it to be the beginning of a list
expect(isUL).to.be(true);
var secondChild = secondChild.children(":first");
var isLI = secondChild.is('li');
//expect it to be part of a list
expect(isLI).to.be(true);
//get the first text element out of the inner iframe
var thirdTextElement = $('iframe').contents()
.find('iframe').contents()
.find('iframe').contents()
.find('body > div').get(2); // THIS IS UGLY TOO
// is there a list-indent class element now?
var firstChild = thirdTextElement.children(":first");
var isUL = firstChild.is('ul');
//expect it to be the beginning of a list
expect(isUL).to.be(true);
var secondChild = firstChild.children(":first");
var isLI = secondChild.is('li');
//expect it to be part of a list
expect(isLI).to.be(true);
},1000);
});*/
});
const pressEnter = () => {
const inner$ = helper.padInner$;
const e = new inner$.Event(helper.evtType);
e.keyCode = 13; // enter :|
inner$('#innerdocbody').trigger(e);
};

View file

@ -0,0 +1,67 @@
'use strict';
describe('italic some text', function () {
// create a new pad before each test run
beforeEach(function (cb) {
helper.newPad(cb);
this.timeout(60000);
});
it('makes text italic using button', function (done) {
const inner$ = helper.padInner$;
const chrome$ = helper.padChrome$;
// get the first text element out of the inner iframe
const $firstTextElement = inner$('div').first();
// select this text element
$firstTextElement.sendkeys('{selectall}');
// get the bold button and click it
const $boldButton = chrome$('.buttonicon-italic');
$boldButton.click();
// ace creates a new dom element when you press a button, just get the first text element again
const $newFirstTextElement = inner$('div').first();
// is there a <i> element now?
const isItalic = $newFirstTextElement.find('i').length === 1;
// expect it to be bold
expect(isItalic).to.be(true);
// make sure the text hasn't changed
expect($newFirstTextElement.text()).to.eql($firstTextElement.text());
done();
});
it('makes text italic using keypress', function (done) {
const inner$ = helper.padInner$;
// get the first text element out of the inner iframe
const $firstTextElement = inner$('div').first();
// select this text element
$firstTextElement.sendkeys('{selectall}');
const e = new inner$.Event(helper.evtType);
e.ctrlKey = true; // Control key
e.which = 105; // i
inner$('#innerdocbody').trigger(e);
// ace creates a new dom element when you press a button, just get the first text element again
const $newFirstTextElement = inner$('div').first();
// is there a <i> element now?
const isItalic = $newFirstTextElement.find('i').length === 1;
// expect it to be bold
expect(isItalic).to.be(true);
// make sure the text hasn't changed
expect($newFirstTextElement.text()).to.eql($firstTextElement.text());
done();
});
});

View file

@ -0,0 +1,125 @@
'use strict';
const deletecookie = (name) => {
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
};
describe('Language select and change', function () {
// Destroy language cookies
deletecookie('language', null);
// create a new pad before each test run
beforeEach(function (cb) {
helper.newPad(cb);
this.timeout(60000);
});
// Destroy language cookies
it('makes text german', function (done) {
const chrome$ = helper.padChrome$;
// click on the settings button to make settings visible
const $settingsButton = chrome$('.buttonicon-settings');
$settingsButton.click();
// click the language button
const $language = chrome$('#languagemenu');
const $languageoption = $language.find('[value=de]');
// select german
$languageoption.attr('selected', 'selected');
$language.change();
helper.waitFor(() => chrome$('.buttonicon-bold').parent()[0].title === 'Fett (Strg-B)')
.done(() => {
// get the value of the bold button
const $boldButton = chrome$('.buttonicon-bold').parent();
// get the title of the bold button
const boldButtonTitle = $boldButton[0].title;
// check if the language is now german
expect(boldButtonTitle).to.be('Fett (Strg-B)');
done();
});
});
it('makes text English', function (done) {
const chrome$ = helper.padChrome$;
// click on the settings button to make settings visible
const $settingsButton = chrome$('.buttonicon-settings');
$settingsButton.click();
// click the language button
const $language = chrome$('#languagemenu');
// select english
$language.val('en');
$language.change();
// get the value of the bold button
const $boldButton = chrome$('.buttonicon-bold').parent();
helper.waitFor(() => $boldButton[0].title !== 'Fett (Strg+B)')
.done(() => {
// get the value of the bold button
const $boldButton = chrome$('.buttonicon-bold').parent();
// get the title of the bold button
const boldButtonTitle = $boldButton[0].title;
// check if the language is now English
expect(boldButtonTitle).to.be('Bold (Ctrl+B)');
done();
});
});
it('changes direction when picking an rtl lang', function (done) {
const chrome$ = helper.padChrome$;
// click on the settings button to make settings visible
const $settingsButton = chrome$('.buttonicon-settings');
$settingsButton.click();
// click the language button
const $language = chrome$('#languagemenu');
const $languageoption = $language.find('[value=ar]');
// select arabic
// $languageoption.attr('selected','selected'); // Breaks the test..
$language.val('ar');
$languageoption.change();
helper.waitFor(() => chrome$('html')[0].dir !== 'ltr')
.done(() => {
// check if the document's direction was changed
expect(chrome$('html')[0].dir).to.be('rtl');
done();
});
});
it('changes direction when picking an ltr lang', function (done) {
const chrome$ = helper.padChrome$;
// click on the settings button to make settings visible
const $settingsButton = chrome$('.buttonicon-settings');
$settingsButton.click();
// click the language button
const $language = chrome$('#languagemenu');
const $languageoption = $language.find('[value=en]');
// select english
// select arabic
$languageoption.attr('selected', 'selected');
$language.val('en');
$languageoption.change();
helper.waitFor(() => chrome$('html')[0].dir !== 'rtl')
.done(() => {
// check if the document's direction was changed
expect(chrome$('html')[0].dir).to.be('ltr');
done();
});
});
});

View file

@ -0,0 +1,52 @@
'use strict';
describe('author of pad edition', function () {
// author 1 creates a new pad with some content (regular lines and lists)
before(function (done) {
const padId = helper.newPad(() => {
// make sure pad has at least 3 lines
const $firstLine = helper.padInner$('div').first();
$firstLine.html('Hello World');
// wait for lines to be processed by Etherpad
helper.waitFor(() => $firstLine.text() === 'Hello World').done(() => {
// Reload pad, to make changes as a second user. Need a timeout here to make sure
// all changes were saved before reloading
setTimeout(() => {
// Expire cookie, so author is changed after reloading the pad.
// See https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie#Example_4_Reset_the_previous_cookie
helper.padChrome$.document.cookie =
'token=foo;expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
helper.newPad(done, padId);
}, 1000);
});
});
this.timeout(60000);
});
// author 2 makes some changes on the pad
it('Clears Authorship by second user', function (done) {
clearAuthorship(done);
});
const clearAuthorship = (done) => {
const inner$ = helper.padInner$;
const chrome$ = helper.padChrome$;
// override the confirm dialogue functioon
helper.padChrome$.window.confirm = function () {
return true;
};
// get the clear authorship colors button and click it
const $clearauthorshipcolorsButton = chrome$('.buttonicon-clearauthorship');
$clearauthorshipcolorsButton.click();
// does the first divs span include an author class?
const hasAuthorClass = inner$('div span').first().attr('class').indexOf('author') !== -1;
expect(hasAuthorClass).to.be(false);
done();
};
});

View file

@ -0,0 +1,187 @@
'use strict';
describe('assign ordered list', function () {
// create a new pad before each test run
beforeEach(function (cb) {
helper.newPad(cb);
this.timeout(60000);
});
it('inserts ordered list text', function (done) {
const inner$ = helper.padInner$;
const chrome$ = helper.padChrome$;
const $insertorderedlistButton = chrome$('.buttonicon-insertorderedlist');
$insertorderedlistButton.click();
helper.waitFor(() => inner$('div').first().find('ol li').length === 1).done(done);
});
context('when user presses Ctrl+Shift+N', function () {
context('and pad shortcut is enabled', function () {
beforeEach(function () {
makeSureShortcutIsEnabled('cmdShiftN');
triggerCtrlShiftShortcut('N');
});
it('inserts unordered list', function (done) {
helper.waitFor(() => helper.padInner$('div').first().find('ol li').length === 1).done(done);
});
});
context('and pad shortcut is disabled', function () {
beforeEach(function () {
makeSureShortcutIsDisabled('cmdShiftN');
triggerCtrlShiftShortcut('N');
});
it('does not insert unordered list', function (done) {
helper.waitFor(
() => helper.padInner$('div').first().find('ol li').length === 1).done(() => {
expect().fail(() => 'Unordered list inserted, should ignore shortcut');
}).fail(() => {
done();
});
});
});
});
context('when user presses Ctrl+Shift+1', function () {
context('and pad shortcut is enabled', function () {
beforeEach(function () {
makeSureShortcutIsEnabled('cmdShift1');
triggerCtrlShiftShortcut('1');
});
it('inserts unordered list', function (done) {
helper.waitFor(() => helper.padInner$('div').first().find('ol li').length === 1).done(done);
});
});
context('and pad shortcut is disabled', function () {
beforeEach(function () {
makeSureShortcutIsDisabled('cmdShift1');
triggerCtrlShiftShortcut('1');
});
it('does not insert unordered list', function (done) {
helper.waitFor(
() => helper.padInner$('div').first().find('ol li').length === 1).done(() => {
expect().fail(() => 'Unordered list inserted, should ignore shortcut');
}).fail(() => {
done();
});
});
});
});
xit('issue #1125 keeps the numbered list on enter for the new line', function (done) {
// EMULATES PASTING INTO A PAD
const inner$ = helper.padInner$;
const chrome$ = helper.padChrome$;
const $insertorderedlistButton = chrome$('.buttonicon-insertorderedlist');
$insertorderedlistButton.click();
// type a bit, make a line break and type again
const $firstTextElement = inner$('div span').first();
$firstTextElement.sendkeys('line 1');
$firstTextElement.sendkeys('{enter}');
$firstTextElement.sendkeys('line 2');
$firstTextElement.sendkeys('{enter}');
helper.waitFor(() => inner$('div span').first().text().indexOf('line 2') === -1).done(() => {
const $newSecondLine = inner$('div').first().next();
const hasOLElement = $newSecondLine.find('ol li').length === 1;
expect(hasOLElement).to.be(true);
expect($newSecondLine.text()).to.be('line 2');
const hasLineNumber = $newSecondLine.find('ol').attr('start') === 2;
// This doesn't work because pasting in content doesn't work
expect(hasLineNumber).to.be(true);
done();
});
});
const triggerCtrlShiftShortcut = (shortcutChar) => {
const inner$ = helper.padInner$;
const e = new inner$.Event(helper.evtType);
e.ctrlKey = true;
e.shiftKey = true;
e.which = shortcutChar.toString().charCodeAt(0);
inner$('#innerdocbody').trigger(e);
};
const makeSureShortcutIsDisabled = (shortcut) => {
helper.padChrome$.window.clientVars.padShortcutEnabled[shortcut] = false;
};
const makeSureShortcutIsEnabled = (shortcut) => {
helper.padChrome$.window.clientVars.padShortcutEnabled[shortcut] = true;
};
});
describe('Pressing Tab in an OL increases and decreases indentation', function () {
// create a new pad before each test run
beforeEach(function (cb) {
helper.newPad(cb);
this.timeout(60000);
});
it('indent and de-indent list item with keypress', function (done) {
const inner$ = helper.padInner$;
const chrome$ = helper.padChrome$;
// get the first text element out of the inner iframe
const $firstTextElement = inner$('div').first();
// select this text element
$firstTextElement.sendkeys('{selectall}');
const $insertorderedlistButton = chrome$('.buttonicon-insertorderedlist');
$insertorderedlistButton.click();
const e = new inner$.Event(helper.evtType);
e.keyCode = 9; // tab
inner$('#innerdocbody').trigger(e);
expect(inner$('div').first().find('.list-number2').length === 1).to.be(true);
e.shiftKey = true; // shift
e.keyCode = 9; // tab
inner$('#innerdocbody').trigger(e);
helper.waitFor(() => inner$('div').first().find('.list-number1').length === 1).done(done);
});
});
describe('Pressing indent/outdent button in an OL increases and ' +
'decreases indentation and bullet / ol formatting', function () {
// create a new pad before each test run
beforeEach(function (cb) {
helper.newPad(cb);
this.timeout(60000);
});
it('indent and de-indent list item with indent button', function (done) {
const inner$ = helper.padInner$;
const chrome$ = helper.padChrome$;
// get the first text element out of the inner iframe
const $firstTextElement = inner$('div').first();
// select this text element
$firstTextElement.sendkeys('{selectall}');
const $insertorderedlistButton = chrome$('.buttonicon-insertorderedlist');
$insertorderedlistButton.click();
const $indentButton = chrome$('.buttonicon-indent');
$indentButton.click(); // make it indented twice
expect(inner$('div').first().find('.list-number2').length === 1).to.be(true);
const $outdentButton = chrome$('.buttonicon-outdent');
$outdentButton.click(); // make it deindented to 1
helper.waitFor(() => inner$('div').first().find('.list-number1').length === 1).done(done);
});
});

View file

@ -0,0 +1,131 @@
'use strict';
describe('Pad modal', function () {
context('when modal is a "force reconnect" message', function () {
const MODAL_SELECTOR = '#connectivity';
beforeEach(function (done) {
helper.newPad(() => {
// force a "slowcommit" error
helper.padChrome$.window.pad.handleChannelStateChange('DISCONNECTED', 'slowcommit');
// wait for modal to be displayed
const $modal = helper.padChrome$(MODAL_SELECTOR);
helper.waitFor(() => $modal.hasClass('popup-show'), 50000).done(done);
});
this.timeout(60000);
});
it('disables editor', function (done) {
expect(isEditorDisabled()).to.be(true);
done();
});
context('and user clicks on editor', function () {
beforeEach(function () {
clickOnPadInner();
});
it('does not close the modal', function (done) {
const $modal = helper.padChrome$(MODAL_SELECTOR);
const modalIsVisible = $modal.hasClass('popup-show');
expect(modalIsVisible).to.be(true);
done();
});
});
context('and user clicks on pad outer', function () {
beforeEach(function () {
clickOnPadOuter();
});
it('does not close the modal', function (done) {
const $modal = helper.padChrome$(MODAL_SELECTOR);
const modalIsVisible = $modal.hasClass('popup-show');
expect(modalIsVisible).to.be(true);
done();
});
});
});
// we use "settings" here, but other modals have the same behaviour
context('when modal is not an error message', function () {
const MODAL_SELECTOR = '#settings';
beforeEach(function (done) {
helper.newPad(() => {
openSettingsAndWaitForModalToBeVisible(done);
});
this.timeout(60000);
});
// This test breaks safari testing
/*
it('does not disable editor', function(done) {
expect(isEditorDisabled()).to.be(false);
done();
});
*/
context('and user clicks on editor', function () {
beforeEach(function () {
clickOnPadInner();
});
it('closes the modal', function (done) {
expect(isModalOpened(MODAL_SELECTOR)).to.be(false);
done();
});
});
context('and user clicks on pad outer', function () {
beforeEach(function () {
clickOnPadOuter();
});
it('closes the modal', function (done) {
expect(isModalOpened(MODAL_SELECTOR)).to.be(false);
done();
});
});
});
const clickOnPadInner = () => {
const $editor = helper.padInner$('#innerdocbody');
$editor.click();
};
const clickOnPadOuter = () => {
const $lineNumbersColumn = helper.padOuter$('#sidedivinner');
$lineNumbersColumn.click();
};
const openSettingsAndWaitForModalToBeVisible = (done) => {
helper.padChrome$('.buttonicon-settings').click();
// wait for modal to be displayed
const modalSelector = '#settings';
helper.waitFor(() => isModalOpened(modalSelector), 10000).done(done);
};
const isEditorDisabled = () => {
const editorDocument = helper.padOuter$("iframe[name='ace_inner']").get(0).contentDocument;
const editorBody = editorDocument.getElementById('innerdocbody');
const editorIsDisabled = editorBody.contentEditable === 'false' || // IE/Safari
editorDocument.designMode === 'off'; // other browsers
return editorIsDisabled;
};
const isModalOpened = (modalSelector) => {
const $modal = helper.padChrome$(modalSelector);
return $modal.hasClass('popup-show');
};
});

View file

@ -0,0 +1,64 @@
'use strict';
describe('undo button then redo button', function () {
beforeEach(function (cb) {
helper.newPad(cb); // creates a new pad
this.timeout(60000);
});
it('redo some typing with button', function (done) {
const inner$ = helper.padInner$;
const chrome$ = helper.padChrome$;
// get the first text element inside the editable space
const $firstTextElement = inner$('div span').first();
const originalValue = $firstTextElement.text(); // get the original value
const newString = 'Foo';
$firstTextElement.sendkeys(newString); // send line 1 to the pad
const modifiedValue = $firstTextElement.text(); // get the modified value
expect(modifiedValue).not.to.be(originalValue); // expect the value to change
// get undo and redo buttons
const $undoButton = chrome$('.buttonicon-undo');
const $redoButton = chrome$('.buttonicon-redo');
// click the buttons
$undoButton.click(); // removes foo
$redoButton.click(); // resends foo
helper.waitFor(() => inner$('div span').first().text() === newString).done(() => {
const finalValue = inner$('div').first().text();
expect(finalValue).to.be(modifiedValue); // expect the value to change
done();
});
});
it('redo some typing with keypress', function (done) {
const inner$ = helper.padInner$;
// get the first text element inside the editable space
const $firstTextElement = inner$('div span').first();
const originalValue = $firstTextElement.text(); // get the original value
const newString = 'Foo';
$firstTextElement.sendkeys(newString); // send line 1 to the pad
const modifiedValue = $firstTextElement.text(); // get the modified value
expect(modifiedValue).not.to.be(originalValue); // expect the value to change
let e = inner$.Event(helper.evtType);
e.ctrlKey = true; // Control key
e.which = 90; // z
inner$('#innerdocbody').trigger(e);
e = inner$.Event(helper.evtType);
e.ctrlKey = true; // Control key
e.which = 121; // y
inner$('#innerdocbody').trigger(e);
helper.waitFor(() => inner$('div span').first().text() === newString).done(() => {
const finalValue = inner$('div').first().text();
expect(finalValue).to.be(modifiedValue); // expect the value to change
done();
});
});
});

View file

@ -0,0 +1,91 @@
'use strict';
// Test for https://github.com/ether/etherpad-lite/issues/1763
// This test fails in Opera, IE and Safari
// Opera fails due to a weird way of handling the order of execution,
// yet actual performance seems fine
// Safari fails due the delay being too great yet the actual performance seems fine
// Firefox might panic that the script is taking too long so will fail
// IE will fail due to running out of memory as it can't fit 2M chars in memory.
// Just FYI Google Docs crashes on large docs whilst trying to Save,
// it's likely the limitations we are
// experiencing are more to do with browser limitations than improper implementation.
// A ueber fix for this would be to have a separate lower cpu priority
// thread that handles operations that aren't
// visible to the user.
// Adapted from John McLear's original test case.
xdescribe('Responsiveness of Editor', function () {
// create a new pad before each test run
beforeEach(function (cb) {
helper.newPad(cb);
this.timeout(6000);
});
// JM commented out on 8th Sep 2020 for a release, after release this needs uncommenting
// And the test needs to be fixed to work in Firefox 52 on Windows 7.
// I am not sure why it fails on this specific platform
// The errors show this.timeout... then crash the browser but
// I am sure something is actually causing the stack trace and
// I just need to narrow down what, offers to help accepted.
it('Fast response to keypress in pad with large amount of contents', function (done) {
// skip on Windows Firefox 52.0
if (window.bowser &&
window.bowser.windows && window.bowser.firefox && window.bowser.version === '52.0') {
this.skip();
}
const inner$ = helper.padInner$;
const chars = '0000000000'; // row of placeholder chars
const amount = 200000; // number of blocks of chars we will insert
const length = (amount * (chars.length) + 1); // include a counter for each space
let text = ''; // the text we're gonna insert
this.timeout(amount * 150); // Changed from 100 to 150 to allow Mac OSX Safari to be slow.
// get keys to send
const keyMultiplier = 10; // multiplier * 10 == total number of key events
let keysToSend = '';
for (let i = 0; i <= keyMultiplier; i++) {
keysToSend += chars;
}
const textElement = inner$('div');
textElement.sendkeys('{selectall}'); // select all
textElement.sendkeys('{del}'); // clear the pad text
for (let i = 0; i <= amount; i++) {
text = `${text + chars} `; // add the chars and space to the text contents
}
inner$('div').first().text(text); // Put the text contents into the pad
// Wait for the new contents to be on the pad
helper.waitFor(() => inner$('div').text().length > length).done(() => {
// has the text changed?
expect(inner$('div').text().length).to.be.greaterThan(length);
const start = Date.now(); // get the start time
// send some new text to the screen (ensure all 3 key events are sent)
const el = inner$('div').first();
for (let i = 0; i < keysToSend.length; ++i) {
const x = keysToSend.charCodeAt(i);
['keyup', 'keypress', 'keydown'].forEach((type) => {
const e = new $.Event(type);
e.keyCode = x;
el.trigger(e);
});
}
helper.waitFor(() => { // Wait for the ability to process
const el = inner$('body');
if (el[0].textContent.length > amount) return true;
}).done(() => {
const end = Date.now(); // get the current time
const delay = end - start; // get the delay as the current time minus the start time
expect(delay).to.be.below(600);
done();
}, 5000);
}, 10000);
});
});

View file

@ -0,0 +1,43 @@
'use strict';
describe('scrolls to line', function () {
// create a new pad with URL hash set before each test run
beforeEach(function (cb) {
helper.newPad({
hash: 'L4',
cb,
});
this.timeout(10000);
});
it('Scrolls down to Line 4', async function () {
this.timeout(10000);
const chrome$ = helper.padChrome$;
await helper.waitForPromise(() => {
const topOffset = parseInt(chrome$('iframe').first('iframe')
.contents().find('#outerdocbody').css('top'));
return (topOffset >= 100);
});
});
});
describe('doesnt break on weird hash input', function () {
// create a new pad with URL hash set before each test run
beforeEach(function (cb) {
helper.newPad({
hash: '#DEEZ123123NUTS',
cb,
});
this.timeout(10000);
});
it('Does NOT change scroll', async function () {
this.timeout(10000);
const chrome$ = helper.padChrome$;
await helper.waitForPromise(() => {
const topOffset = parseInt(chrome$('iframe').first('iframe')
.contents().find('#outerdocbody').css('top'));
return (!topOffset); // no css top should be set.
});
});
});

View file

@ -0,0 +1,161 @@
'use strict';
describe('select formatting buttons when selection has style applied', function () {
const STYLES = ['italic', 'bold', 'underline', 'strikethrough'];
const SHORTCUT_KEYS = ['I', 'B', 'U', '5']; // italic, bold, underline, strikethrough
const FIRST_LINE = 0;
before(function (cb) {
helper.newPad(cb);
this.timeout(60000);
});
const applyStyleOnLine = function (style, line) {
const chrome$ = helper.padChrome$;
selectLine(line);
const $formattingButton = chrome$(`.buttonicon-${style}`);
$formattingButton.click();
};
const isButtonSelected = function (style) {
const chrome$ = helper.padChrome$;
const $formattingButton = chrome$(`.buttonicon-${style}`);
return $formattingButton.parent().hasClass('selected');
};
const selectLine = function (lineNumber, offsetStart, offsetEnd) {
const inner$ = helper.padInner$;
const $line = inner$('div').eq(lineNumber);
helper.selectLines($line, $line, offsetStart, offsetEnd);
};
const placeCaretOnLine = function (lineNumber) {
const inner$ = helper.padInner$;
const $line = inner$('div').eq(lineNumber);
$line.sendkeys('{leftarrow}');
};
const undo = function () {
const $undoButton = helper.padChrome$('.buttonicon-undo');
$undoButton.click();
};
const testIfFormattingButtonIsDeselected = function (style) {
it(`deselects the ${style} button`, function (done) {
helper.waitFor(() => isButtonSelected(style) === false).done(done);
});
};
const testIfFormattingButtonIsSelected = function (style) {
it(`selects the ${style} button`, function (done) {
helper.waitFor(() => isButtonSelected(style)).done(done);
});
};
const applyStyleOnLineAndSelectIt = function (line, style, cb) {
applyStyleOnLineOnFullLineAndRemoveSelection(line, style, selectLine, cb);
};
const applyStyleOnLineAndPlaceCaretOnit = function (line, style, cb) {
applyStyleOnLineOnFullLineAndRemoveSelection(line, style, placeCaretOnLine, cb);
};
const applyStyleOnLineOnFullLineAndRemoveSelection = function (line, style, selectTarget, cb) {
// see if line html has changed
const inner$ = helper.padInner$;
const oldLineHTML = inner$.find('div')[line];
applyStyleOnLine(style, line);
helper.waitFor(() => {
const lineHTML = inner$.find('div')[line];
return lineHTML !== oldLineHTML;
});
// remove selection from previous line
selectLine(line + 1);
// setTimeout(function() {
// select the text or place the caret on a position that
// has the formatting text applied previously
selectTarget(line);
cb();
// }, 1000);
};
const pressFormattingShortcutOnSelection = function (key) {
const inner$ = helper.padInner$;
// get the first text element out of the inner iframe
const $firstTextElement = inner$('div').first();
// select this text element
$firstTextElement.sendkeys('{selectall}');
const e = new inner$.Event(helper.evtType);
e.ctrlKey = true; // Control key
e.which = key.charCodeAt(0); // I, U, B, 5
inner$('#innerdocbody').trigger(e);
};
STYLES.forEach((style) => {
context(`when selection is in a text with ${style} applied`, function () {
before(function (done) {
this.timeout(4000);
applyStyleOnLineAndSelectIt(FIRST_LINE, style, done);
});
after(function () {
undo();
});
testIfFormattingButtonIsSelected(style);
});
context(`when caret is in a position with ${style} applied`, function () {
before(function (done) {
this.timeout(4000);
applyStyleOnLineAndPlaceCaretOnit(FIRST_LINE, style, done);
});
after(function () {
undo();
});
testIfFormattingButtonIsSelected(style);
});
});
context('when user applies a style and the selection does not change', function () {
const style = STYLES[0]; // italic
before(function () {
applyStyleOnLine(style, FIRST_LINE);
});
// clean the style applied
after(function () {
applyStyleOnLine(style, FIRST_LINE);
});
it('selects the style button', function (done) {
expect(isButtonSelected(style)).to.be(true);
done();
});
});
SHORTCUT_KEYS.forEach((key, index) => {
const styleOfTheShortcut = STYLES[index]; // italic, bold, ...
context(`when user presses CMD + ${key}`, function () {
before(function () {
pressFormattingShortcutOnSelection(key);
});
testIfFormattingButtonIsSelected(styleOfTheShortcut);
context(`and user presses CMD + ${key} again`, function () {
before(function () {
pressFormattingShortcutOnSelection(key);
});
testIfFormattingButtonIsDeselected(styleOfTheShortcut);
});
});
});
});

View file

@ -0,0 +1,38 @@
'use strict';
describe('strikethrough button', function () {
// create a new pad before each test run
beforeEach(function (cb) {
helper.newPad(cb);
this.timeout(60000);
});
it('makes text strikethrough', function (done) {
const inner$ = helper.padInner$;
const chrome$ = helper.padChrome$;
// get the first text element out of the inner iframe
const $firstTextElement = inner$('div').first();
// select this text element
$firstTextElement.sendkeys('{selectall}');
// get the strikethrough button and click it
const $strikethroughButton = chrome$('.buttonicon-strikethrough');
$strikethroughButton.click();
// ace creates a new dom element when you press a button, just get the first text element again
const $newFirstTextElement = inner$('div').first();
// is there a <i> element now?
const isstrikethrough = $newFirstTextElement.find('s').length === 1;
// expect it to be strikethrough
expect(isstrikethrough).to.be(true);
// make sure the text hasn't changed
expect($newFirstTextElement.text()).to.eql($firstTextElement.text());
done();
});
});

View file

@ -0,0 +1,43 @@
'use strict';
// deactivated, we need a nice way to get the timeslider, this is ugly
xdescribe('timeslider button takes you to the timeslider of a pad', function () {
beforeEach(function (cb) {
helper.newPad(cb); // creates a new pad
this.timeout(60000);
});
it('timeslider contained in URL', function (done) {
const inner$ = helper.padInner$;
const chrome$ = helper.padChrome$;
// get the first text element inside the editable space
const $firstTextElement = inner$('div span').first();
const originalValue = $firstTextElement.text(); // get the original value
$firstTextElement.sendkeys('Testing'); // send line 1 to the pad
const modifiedValue = $firstTextElement.text(); // get the modified value
expect(modifiedValue).not.to.be(originalValue); // expect the value to change
helper.waitFor(() => modifiedValue !== originalValue // The value has changed so we can..
).done(() => {
const $timesliderButton = chrome$('#timesliderlink');
$timesliderButton.click(); // So click the timeslider link
helper.waitFor(() => {
const iFrameURL = chrome$.window.location.href;
if (iFrameURL) {
return iFrameURL.indexOf('timeslider') !== -1;
} else {
return false; // the URL hasnt been set yet
}
}).done(() => {
// click the buttons
const iFrameURL = chrome$.window.location.href; // get the url
const inTimeslider = iFrameURL.indexOf('timeslider') !== -1;
expect(inTimeslider).to.be(true); // expect the value to change
done();
});
});
});
});

View file

@ -0,0 +1,100 @@
'use strict';
describe('timeslider follow', function () {
// create a new pad before each test run
beforeEach(function (cb) {
helper.newPad(cb);
});
// TODO needs test if content is also followed, when user a makes edits
// while user b is in the timeslider
it("content as it's added to timeslider", async function () {
// send 6 revisions
const revs = 6;
const message = 'a\n\n\n\n\n\n\n\n\n\n';
const newLines = message.split('\n').length;
for (let i = 0; i < revs; i++) {
await helper.edit(message, newLines * i + 1);
}
await helper.gotoTimeslider(0);
await helper.waitForPromise(() => helper.contentWindow().location.hash === '#0');
const originalTop = helper.contentWindow().$('#innerdocbody').offset();
// set to follow contents as it arrives
helper.contentWindow().$('#options-followContents').prop('checked', true);
helper.contentWindow().$('#playpause_button_icon').click();
let newTop;
await helper.waitForPromise(() => {
newTop = helper.contentWindow().$('#innerdocbody').offset();
return newTop.top < originalTop.top;
});
});
/**
* Tests for bug described in #4389
* The goal is to scroll to the first line that contains a change right before
* the change is applied.
*/
it('only to lines that exist in the pad view, regression test for #4389', async function () {
await helper.clearPad();
await helper.edit('Test line\n' +
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n');
await helper.edit('Another test line', 40);
await helper.gotoTimeslider();
// set to follow contents as it arrives
helper.contentWindow().$('#options-followContents').prop('checked', true);
const oldYPosition = helper.contentWindow().$('#editorcontainerbox')[0].scrollTop;
expect(oldYPosition).to.be(0);
/**
* pad content rev 0 [default Pad text]
* pad content rev 1 ['']
* pad content rev 2 ['Test line','','', ..., '']
* pad content rev 3 ['Test line','',..., 'Another test line', ..., '']
*/
// line 40 changed
helper.contentWindow().$('#leftstep').click();
await helper.waitForPromise(() => hasFollowedToLine(40));
// line 1 is the first line that changed
helper.contentWindow().$('#leftstep').click();
await helper.waitForPromise(() => hasFollowedToLine(1));
// line 1 changed
helper.contentWindow().$('#leftstep').click();
await helper.waitForPromise(() => hasFollowedToLine(1));
// line 1 changed
helper.contentWindow().$('#rightstep').click();
await helper.waitForPromise(() => hasFollowedToLine(1));
// line 1 is the first line that changed
helper.contentWindow().$('#rightstep').click();
await helper.waitForPromise(() => hasFollowedToLine(1));
// line 40 changed
helper.contentWindow().$('#rightstep').click();
helper.waitForPromise(() => hasFollowedToLine(40));
});
});
/**
* @param {number} lineNum
* @returns {boolean} scrolled to the lineOffset?
*/
const hasFollowedToLine = (lineNum) => {
const scrollPosition = helper.contentWindow().$('#editorcontainerbox')[0].scrollTop;
const lineOffset =
helper.contentWindow().$('#innerdocbody').find(`div:nth-child(${lineNum})`)[0].offsetTop;
return Math.abs(scrollPosition - lineOffset) < 1;
};

View file

@ -0,0 +1,64 @@
'use strict';
describe('timeslider', function () {
// create a new pad before each test run
beforeEach(function (cb) {
helper.newPad(cb);
});
/**
* @todo test authorsList
*/
it("Shows a date/time in the timeslider and make sure it doesn't include NaN", async function () {
// make some changes to produce 3 revisions
const revs = 3;
for (let i = 0; i < revs; i++) {
await helper.edit('a\n');
}
await helper.gotoTimeslider(revs);
await helper.waitForPromise(() => helper.contentWindow().location.hash === `#${revs}`);
// the datetime of last edit
const timerTimeLast = new Date(helper.timesliderTimerTime()).getTime();
// the day of this revision, e.g. August 12, 2020 (stripped the string "Saved")
const dateLast = new Date(helper.revisionDateElem().substr(6)).getTime();
// the label/revision, ie Version 3
const labelLast = helper.revisionLabelElem().text();
// the datetime should be a date
expect(Number.isNaN(timerTimeLast)).to.eql(false);
// the Date object of the day should not be NaN
expect(Number.isNaN(dateLast)).to.eql(false);
// the label should be Version `Number`
expect(labelLast).to.be(`Version ${revs}`);
// Click somewhere left on the timeslider to go to revision 0
helper.sliderClick(1);
// the datetime of last edit
const timerTime = new Date(helper.timesliderTimerTime()).getTime();
// the day of this revision, e.g. August 12, 2020
const date = new Date(helper.revisionDateElem().substr(6)).getTime();
// the label/revision, e.g. Version 0
const label = helper.revisionLabelElem().text();
// the datetime should be a date
expect(Number.isNaN(timerTime)).to.eql(false);
// the last revision should be newer or have the same time
expect(timerTimeLast).to.not.be.lessThan(timerTime);
// the Date object of the day should not be NaN
expect(Number.isNaN(date)).to.eql(false);
// the label should be Version 0
expect(label).to.be('Version 0');
});
});

View file

@ -0,0 +1,31 @@
'use strict';
describe('timeslider', function () {
const padId = 735773577357 + (Math.round(Math.random() * 1000));
// create a new pad before each test run
beforeEach(function (cb) {
helper.newPad(cb, padId);
});
it('Makes sure the export URIs are as expected when the padID is numeric', async function () {
await helper.edit('a\n');
await helper.gotoTimeslider(1);
// ensure we are on revision 1
await helper.waitForPromise(() => helper.contentWindow().location.hash === '#1');
// expect URI to be similar to
// http://192.168.1.48:9001/p/2/1/export/html
// http://192.168.1.48:9001/p/735773577399/1/export/html
const rev1ExportLink = helper.contentWindow().$('#exporthtmla').attr('href');
expect(rev1ExportLink).to.contain('/1/export/html');
// Click somewhere left on the timeslider to go to revision 0
helper.sliderClick(30);
const rev0ExportLink = helper.contentWindow().$('#exporthtmla').attr('href');
expect(rev0ExportLink).to.contain('/0/export/html');
});
});

View file

@ -0,0 +1,184 @@
'use strict';
describe('timeslider', function () {
// create a new pad before each test run
beforeEach(function (cb) {
helper.newPad(cb);
this.timeout(60000);
});
it('loads adds a hundred revisions', function (done) { // passes
const inner$ = helper.padInner$;
const chrome$ = helper.padChrome$;
// make some changes to produce 100 revisions
const timePerRev = 900;
const revs = 99;
this.timeout(revs * timePerRev + 10000);
for (let i = 0; i < revs; i++) {
setTimeout(() => {
// enter 'a' in the first text element
inner$('div').first().sendkeys('a');
}, timePerRev * i);
}
chrome$('.buttonicon-savedRevision').click();
setTimeout(() => {
// go to timeslider
$('#iframe-container iframe').attr('src',
`${$('#iframe-container iframe').attr('src')}/timeslider`);
setTimeout(() => {
const timeslider$ = $('#iframe-container iframe')[0].contentWindow.$;
const $sliderBar = timeslider$('#ui-slider-bar');
const latestContents = timeslider$('#innerdocbody').text();
// Click somewhere on the timeslider
let e = new jQuery.Event('mousedown');
// sets y co-ordinate of the pad slider modal.
const base = (timeslider$('#ui-slider-bar').offset().top - 24);
e.clientX = e.pageX = 150;
e.clientY = e.pageY = base + 5;
$sliderBar.trigger(e);
e = new jQuery.Event('mousedown');
e.clientX = e.pageX = 150;
e.clientY = e.pageY = base;
$sliderBar.trigger(e);
e = new jQuery.Event('mousedown');
e.clientX = e.pageX = 150;
e.clientY = e.pageY = base - 5;
$sliderBar.trigger(e);
$sliderBar.trigger('mouseup');
setTimeout(() => {
// make sure the text has changed
expect(timeslider$('#innerdocbody').text()).not.to.eql(latestContents);
const starIsVisible = timeslider$('.star').is(':visible');
expect(starIsVisible).to.eql(true);
done();
}, 1000);
}, 6000);
}, revs * timePerRev);
});
// Disabled as jquery trigger no longer works properly
xit('changes the url when clicking on the timeslider', function (done) {
const inner$ = helper.padInner$;
// make some changes to produce 7 revisions
const timePerRev = 1000;
const revs = 20;
this.timeout(revs * timePerRev + 10000);
for (let i = 0; i < revs; i++) {
setTimeout(() => {
// enter 'a' in the first text element
inner$('div').first().sendkeys('a');
}, timePerRev * i);
}
setTimeout(() => {
// go to timeslider
$('#iframe-container iframe').attr('src',
`${$('#iframe-container iframe').attr('src')}/timeslider`);
setTimeout(() => {
const timeslider$ = $('#iframe-container iframe')[0].contentWindow.$;
const $sliderBar = timeslider$('#ui-slider-bar');
const oldUrl = $('#iframe-container iframe')[0].contentWindow.location.hash;
// Click somewhere on the timeslider
const e = new jQuery.Event('mousedown');
e.clientX = e.pageX = 150;
e.clientY = e.pageY = 60;
$sliderBar.trigger(e);
helper.waitFor(
() => $('#iframe-container iframe')[0].contentWindow.location.hash !== oldUrl, 6000)
.always(() => {
expect(
$('#iframe-container iframe')[0].contentWindow.location.hash
).not.to.eql(oldUrl);
done();
});
}, 6000);
}, revs * timePerRev);
});
it('jumps to a revision given in the url', function (done) {
const inner$ = helper.padInner$;
this.timeout(40000);
// wait for the text to be loaded
helper.waitFor(() => inner$('body').text().length !== 0, 10000).always(() => {
const newLines = inner$('body div').length;
const oldLength = inner$('body').text().length + newLines / 2;
expect(oldLength).to.not.eql(0);
inner$('div').first().sendkeys('a');
let timeslider$;
// wait for our additional revision to be added
helper.waitFor(() => {
// newLines takes the new lines into account which are strippen when using
// inner$('body').text(), one <div> is used for one line in ACE.
const lenOkay = inner$('body').text().length + newLines / 2 !== oldLength;
// this waits for the color to be added to our <span>, which means that the revision
// was accepted by the server.
const colorOkay = inner$('span').first().attr('class').indexOf('author-') === 0;
return lenOkay && colorOkay;
}, 10000).always(() => {
// go to timeslider with a specific revision set
$('#iframe-container iframe').attr('src',
`${$('#iframe-container iframe').attr('src')}/timeslider#0`);
// wait for the timeslider to be loaded
helper.waitFor(() => {
try {
timeslider$ = $('#iframe-container iframe')[0].contentWindow.$;
} catch (e) {
// Empty catch block <3
}
if (timeslider$) {
return timeslider$('#innerdocbody').text().length === oldLength;
}
}, 10000).always(() => {
expect(timeslider$('#innerdocbody').text().length).to.eql(oldLength);
done();
});
});
});
});
it('checks the export url', function (done) {
const inner$ = helper.padInner$;
this.timeout(11000);
inner$('div').first().sendkeys('a');
setTimeout(() => {
// go to timeslider
$('#iframe-container iframe').attr('src',
`${$('#iframe-container iframe').attr('src')}/timeslider#0`);
let timeslider$;
let exportLink;
helper.waitFor(() => {
try {
timeslider$ = $('#iframe-container iframe')[0].contentWindow.$;
} catch (e) {
// Empty catch block <3
}
if (!timeslider$) return false;
exportLink = timeslider$('#exportplaina').attr('href');
if (!exportLink) return false;
return exportLink.substr(exportLink.length - 12) === '0/export/txt';
}, 6000).always(() => {
expect(exportLink.substr(exportLink.length - 12)).to.eql('0/export/txt');
done();
});
}, 2500);
});
});

View file

@ -0,0 +1,55 @@
'use strict';
describe('undo button', function () {
beforeEach(function (cb) {
helper.newPad(cb); // creates a new pad
this.timeout(60000);
});
it('undo some typing by clicking undo button', function (done) {
const inner$ = helper.padInner$;
const chrome$ = helper.padChrome$;
// get the first text element inside the editable space
const $firstTextElement = inner$('div span').first();
const originalValue = $firstTextElement.text(); // get the original value
$firstTextElement.sendkeys('foo'); // send line 1 to the pad
const modifiedValue = $firstTextElement.text(); // get the modified value
expect(modifiedValue).not.to.be(originalValue); // expect the value to change
// get clear authorship button as a variable
const $undoButton = chrome$('.buttonicon-undo');
// click the button
$undoButton.click();
helper.waitFor(() => inner$('div span').first().text() === originalValue).done(() => {
const finalValue = inner$('div span').first().text();
expect(finalValue).to.be(originalValue); // expect the value to change
done();
});
});
it('undo some typing using a keypress', function (done) {
const inner$ = helper.padInner$;
// get the first text element inside the editable space
const $firstTextElement = inner$('div span').first();
const originalValue = $firstTextElement.text(); // get the original value
$firstTextElement.sendkeys('foo'); // send line 1 to the pad
const modifiedValue = $firstTextElement.text(); // get the modified value
expect(modifiedValue).not.to.be(originalValue); // expect the value to change
const e = new inner$.Event(helper.evtType);
e.ctrlKey = true; // Control key
e.which = 90; // z
inner$('#innerdocbody').trigger(e);
helper.waitFor(() => inner$('div span').first().text() === originalValue).done(() => {
const finalValue = inner$('div span').first().text();
expect(finalValue).to.be(originalValue); // expect the value to change
done();
});
});
});

View file

@ -0,0 +1,165 @@
'use strict';
describe('assign unordered list', function () {
// create a new pad before each test run
beforeEach(function (cb) {
helper.newPad(cb);
this.timeout(60000);
});
it('insert unordered list text then removes by outdent', function (done) {
const inner$ = helper.padInner$;
const chrome$ = helper.padChrome$;
const originalText = inner$('div').first().text();
const $insertunorderedlistButton = chrome$('.buttonicon-insertunorderedlist');
$insertunorderedlistButton.click();
helper.waitFor(() => {
const newText = inner$('div').first().text();
if (newText === originalText) {
return inner$('div').first().find('ul li').length === 1;
}
}).done(() => {
// remove indentation by bullet and ensure text string remains the same
chrome$('.buttonicon-outdent').click();
helper.waitFor(() => {
const newText = inner$('div').first().text();
return (newText === originalText);
}).done(() => {
done();
});
});
});
});
describe('unassign unordered list', function () {
// create a new pad before each test run
beforeEach(function (cb) {
helper.newPad(cb);
this.timeout(60000);
});
it('insert unordered list text then remove by clicking list again', function (done) {
const inner$ = helper.padInner$;
const chrome$ = helper.padChrome$;
const originalText = inner$('div').first().text();
const $insertunorderedlistButton = chrome$('.buttonicon-insertunorderedlist');
$insertunorderedlistButton.click();
helper.waitFor(() => {
const newText = inner$('div').first().text();
if (newText === originalText) {
return inner$('div').first().find('ul li').length === 1;
}
}).done(() => {
// remove indentation by bullet and ensure text string remains the same
$insertunorderedlistButton.click();
helper.waitFor(() => {
const isList = inner$('div').find('ul').length === 1;
// sohuldn't be list
return (isList === false);
}).done(() => {
done();
});
});
});
});
describe('keep unordered list on enter key', function () {
// create a new pad before each test run
beforeEach(function (cb) {
helper.newPad(cb);
this.timeout(60000);
});
it('Keeps the unordered list on enter for the new line', function (done) {
const inner$ = helper.padInner$;
const chrome$ = helper.padChrome$;
const $insertorderedlistButton = chrome$('.buttonicon-insertunorderedlist');
$insertorderedlistButton.click();
// type a bit, make a line break and type again
const $firstTextElement = inner$('div span').first();
$firstTextElement.sendkeys('line 1');
$firstTextElement.sendkeys('{enter}');
$firstTextElement.sendkeys('line 2');
$firstTextElement.sendkeys('{enter}');
helper.waitFor(() => inner$('div span').first().text().indexOf('line 2') === -1).done(() => {
const $newSecondLine = inner$('div').first().next();
const hasULElement = $newSecondLine.find('ul li').length === 1;
expect(hasULElement).to.be(true);
expect($newSecondLine.text()).to.be('line 2');
done();
});
});
});
describe('Pressing Tab in an UL increases and decreases indentation', function () {
// create a new pad before each test run
beforeEach(function (cb) {
helper.newPad(cb);
this.timeout(60000);
});
it('indent and de-indent list item with keypress', function (done) {
const inner$ = helper.padInner$;
const chrome$ = helper.padChrome$;
// get the first text element out of the inner iframe
const $firstTextElement = inner$('div').first();
// select this text element
$firstTextElement.sendkeys('{selectall}');
const $insertorderedlistButton = chrome$('.buttonicon-insertunorderedlist');
$insertorderedlistButton.click();
const e = inner$.Event(helper.evtType);
e.keyCode = 9; // tab
inner$('#innerdocbody').trigger(e);
expect(inner$('div').first().find('.list-bullet2').length === 1).to.be(true);
e.shiftKey = true; // shift
e.keyCode = 9; // tab
inner$('#innerdocbody').trigger(e);
helper.waitFor(() => inner$('div').first().find('.list-bullet1').length === 1).done(done);
});
});
describe('Pressing indent/outdent button in an UL increases and decreases indentation ' +
'and bullet / ol formatting', function () {
// create a new pad before each test run
beforeEach(function (cb) {
helper.newPad(cb);
this.timeout(60000);
});
it('indent and de-indent list item with indent button', function (done) {
const inner$ = helper.padInner$;
const chrome$ = helper.padChrome$;
// get the first text element out of the inner iframe
const $firstTextElement = inner$('div').first();
// select this text element
$firstTextElement.sendkeys('{selectall}');
const $insertunorderedlistButton = chrome$('.buttonicon-insertunorderedlist');
$insertunorderedlistButton.click();
const $indentButton = chrome$('.buttonicon-indent');
$indentButton.click(); // make it indented twice
expect(inner$('div').first().find('.list-bullet2').length === 1).to.be(true);
const $outdentButton = chrome$('.buttonicon-outdent');
$outdentButton.click(); // make it deindented to 1
helper.waitFor(() => inner$('div').first().find('.list-bullet1').length === 1).done(done);
});
});

View file

@ -0,0 +1,76 @@
'use strict';
describe('urls', function () {
// Returns the first text element. Note that any change to the text element will result in the
// element being replaced with another object.
const txt = () => helper.padInner$('div').first();
before(async function () {
this.timeout(60000);
await new Promise((resolve, reject) => helper.newPad((err) => {
if (err != null) return reject(err);
resolve();
}));
});
beforeEach(async function () {
await helper.clearPad();
});
describe('entering a URL makes a link', function () {
for (const url of ['https://etherpad.org', 'www.etherpad.org']) {
it(url, async function () {
const url = 'https://etherpad.org';
await helper.edit(url);
await helper.waitForPromise(() => txt().find('a').length === 1, 2000);
const link = txt().find('a');
expect(link.attr('href')).to.be(url);
expect(link.text()).to.be(url);
});
}
});
describe('special characters inside URL', function () {
for (const char of '-:@_.,~%+/?=&#!;()$\'*') {
const url = `https://etherpad.org/${char}foo`;
it(url, async function () {
await helper.edit(url);
await helper.waitForPromise(() => txt().find('a').length === 1);
const link = txt().find('a');
expect(link.attr('href')).to.be(url);
expect(link.text()).to.be(url);
});
}
});
describe('punctuation after URL is ignored', function () {
for (const char of ':.,;?!)\'*]') {
const want = 'https://etherpad.org';
const input = want + char;
it(input, async function () {
await helper.edit(input);
await helper.waitForPromise(() => txt().find('a').length === 1);
const link = txt().find('a');
expect(link.attr('href')).to.be(want);
expect(link.text()).to.be(want);
});
}
});
// Square brackets are in the RFC3986 reserved set so they can legally appear in URIs, but they
// are explicitly excluded from linkification because including them is usually not desired (e.g.,
// it can interfere with wiki/markdown link syntax).
describe('square brackets are excluded from linkified URLs', function () {
for (const char of '[]') {
const want = 'https://etherpad.org/';
const input = `${want}${char}foo`;
it(input, async function () {
await helper.edit(input);
await helper.waitForPromise(() => txt().find('a').length === 1);
const link = txt().find('a');
expect(link.attr('href')).to.be(want);
expect(link.text()).to.be(want);
});
}
});
});

View file

@ -0,0 +1,69 @@
'use strict';
describe('Automatic pad reload on Force Reconnect message', function () {
let padId, $originalPadFrame;
beforeEach(function (done) {
padId = helper.newPad(() => {
// enable userdup error to have timer to force reconnect
const $errorMessageModal = helper.padChrome$('#connectivity .userdup');
$errorMessageModal.addClass('with_reconnect_timer');
// make sure there's a timeout set, otherwise automatic reconnect won't be enabled
helper.padChrome$.window.clientVars.automaticReconnectionTimeout = 2;
// open same pad on another iframe, to force userdup error
const $otherIframeWithSamePad = $(`<iframe src="/p/${padId}" style="height: 1px;"></iframe>`);
$originalPadFrame = $('#iframe-container iframe');
$otherIframeWithSamePad.insertAfter($originalPadFrame);
// wait for modal to be displayed
helper.waitFor(() => $errorMessageModal.is(':visible'), 50000).done(done);
});
this.timeout(60000);
});
it('displays a count down timer to automatically reconnect', function (done) {
const $errorMessageModal = helper.padChrome$('#connectivity .userdup');
const $countDownTimer = $errorMessageModal.find('.reconnecttimer');
expect($countDownTimer.is(':visible')).to.be(true);
done();
});
context('and user clicks on Cancel', function () {
beforeEach(function () {
const $errorMessageModal = helper.padChrome$('#connectivity .userdup');
$errorMessageModal.find('#cancelreconnect').click();
});
it('does not show Cancel button nor timer anymore', function (done) {
const $errorMessageModal = helper.padChrome$('#connectivity .userdup');
const $countDownTimer = $errorMessageModal.find('.reconnecttimer');
const $cancelButton = $errorMessageModal.find('#cancelreconnect');
expect($countDownTimer.is(':visible')).to.be(false);
expect($cancelButton.is(':visible')).to.be(false);
done();
});
});
context('and user does not click on Cancel until timer expires', function () {
let padWasReloaded = false;
beforeEach(function () {
$originalPadFrame.one('load', () => {
padWasReloaded = true;
});
});
it('reloads the pad', function (done) {
helper.waitFor(() => padWasReloaded, 5000).done(done);
this.timeout(5000);
});
});
});