mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-04-23 00:46:16 -04:00
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:
parent
efde0b787a
commit
2ea8ea1275
146 changed files with 191 additions and 1161 deletions
297
src/tests/frontend/helper.js
Normal file
297
src/tests/frontend/helper.js
Normal file
|
@ -0,0 +1,297 @@
|
|||
'use strict';
|
||||
const helper = {}; // eslint-disable-line no-redeclare
|
||||
|
||||
(function () {
|
||||
let $iframe; const
|
||||
jsLibraries = {};
|
||||
|
||||
helper.init = function (cb) {
|
||||
$.get('/static/js/jquery.js').done((code) => {
|
||||
// make sure we don't override existing jquery
|
||||
jsLibraries.jquery = `if(typeof $ === 'undefined') {\n${code}\n}`;
|
||||
|
||||
$.get('/tests/frontend/lib/sendkeys.js').done((code) => {
|
||||
jsLibraries.sendkeys = code;
|
||||
|
||||
cb();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
helper.randomString = function randomString(len) {
|
||||
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
||||
let randomstring = '';
|
||||
for (let i = 0; i < len; i++) {
|
||||
const rnum = Math.floor(Math.random() * chars.length);
|
||||
randomstring += chars.substring(rnum, rnum + 1);
|
||||
}
|
||||
return randomstring;
|
||||
};
|
||||
|
||||
const getFrameJQuery = function ($iframe) {
|
||||
/*
|
||||
I tried over 9001 ways to inject javascript into iframes.
|
||||
This is the only way I found that worked in IE 7+8+9, FF and Chrome
|
||||
*/
|
||||
const win = $iframe[0].contentWindow;
|
||||
const doc = win.document;
|
||||
|
||||
// IE 8+9 Hack to make eval appear
|
||||
// http://stackoverflow.com/questions/2720444/why-does-this-window-object-not-have-the-eval-function
|
||||
win.execScript && win.execScript('null');
|
||||
|
||||
win.eval(jsLibraries.jquery);
|
||||
win.eval(jsLibraries.sendkeys);
|
||||
|
||||
win.$.window = win;
|
||||
win.$.document = doc;
|
||||
|
||||
return win.$;
|
||||
};
|
||||
|
||||
helper.clearSessionCookies = function () {
|
||||
// Expire cookies, so author and language are changed after reloading the pad.
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie#Example_4_Reset_the_previous_cookie
|
||||
window.document.cookie = 'token=;expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
|
||||
window.document.cookie = 'language=;expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
|
||||
};
|
||||
|
||||
// Can only happen when the iframe exists, so we're doing it separately from other cookies
|
||||
helper.clearPadPrefCookie = function () {
|
||||
helper.padChrome$.document.cookie = 'prefsHttp=;expires=Thu, 01 Jan 1970 00:00:00 GMT';
|
||||
};
|
||||
|
||||
// Overwrite all prefs in pad cookie. Assumes http, not https.
|
||||
//
|
||||
// `helper.padChrome$.document.cookie` (the iframe) and `window.document.cookie`
|
||||
// seem to have independent cookies, UNLESS we put path=/ here (which we don't).
|
||||
// I don't fully understand it, but this function seems to properly simulate
|
||||
// padCookie.setPref in the client code
|
||||
helper.setPadPrefCookie = function (prefs) {
|
||||
helper.padChrome$.document.cookie =
|
||||
(`prefsHttp=${escape(JSON.stringify(prefs))};expires=Thu, 01 Jan 3000 00:00:00 GMT`);
|
||||
};
|
||||
|
||||
// Functionality for knowing what key event type is required for tests
|
||||
let evtType = 'keydown';
|
||||
// if it's IE require keypress
|
||||
if (window.navigator.userAgent.indexOf('MSIE') > -1) {
|
||||
evtType = 'keypress';
|
||||
}
|
||||
// Edge also requires keypress.
|
||||
if (window.navigator.userAgent.indexOf('Edge') > -1) {
|
||||
evtType = 'keypress';
|
||||
}
|
||||
// Opera also requires keypress.
|
||||
if (window.navigator.userAgent.indexOf('OPR') > -1) {
|
||||
evtType = 'keypress';
|
||||
}
|
||||
helper.evtType = evtType;
|
||||
|
||||
// @todo needs fixing asap
|
||||
// newPad occasionally timeouts, might be a problem with ready/onload code during page setup
|
||||
// This ensures that tests run regardless of this problem
|
||||
helper.retry = 0;
|
||||
|
||||
helper.newPad = function (cb, padName) {
|
||||
// build opts object
|
||||
let opts = {clearCookies: true};
|
||||
if (typeof cb === 'function') {
|
||||
opts.cb = cb;
|
||||
} else {
|
||||
opts = _.defaults(cb, opts);
|
||||
}
|
||||
|
||||
// if opts.params is set we manipulate the URL to include URL parameters IE ?foo=Bah.
|
||||
let encodedParams;
|
||||
if (opts.params) {
|
||||
encodedParams = `?${$.param(opts.params)}`;
|
||||
}
|
||||
let hash;
|
||||
if (opts.hash) {
|
||||
hash = `#${opts.hash}`;
|
||||
}
|
||||
|
||||
// clear cookies
|
||||
if (opts.clearCookies) {
|
||||
helper.clearSessionCookies();
|
||||
}
|
||||
|
||||
if (!padName) padName = `FRONTEND_TEST_${helper.randomString(20)}`;
|
||||
$iframe = $(`<iframe src='/p/${padName}${hash || ''}${encodedParams || ''}'></iframe>`);
|
||||
// needed for retry
|
||||
const origPadName = padName;
|
||||
|
||||
// clean up inner iframe references
|
||||
helper.padChrome$ = helper.padOuter$ = helper.padInner$ = null;
|
||||
|
||||
// remove old iframe
|
||||
$('#iframe-container iframe').remove();
|
||||
// set new iframe
|
||||
$('#iframe-container').append($iframe);
|
||||
$iframe.one('load', () => {
|
||||
helper.padChrome$ = getFrameJQuery($('#iframe-container iframe'));
|
||||
if (opts.clearCookies) {
|
||||
helper.clearPadPrefCookie();
|
||||
}
|
||||
if (opts.padPrefs) {
|
||||
helper.setPadPrefCookie(opts.padPrefs);
|
||||
}
|
||||
helper.waitFor(() => !$iframe.contents().find('#editorloadingbox')
|
||||
.is(':visible'), 10000).done(() => {
|
||||
helper.padOuter$ = getFrameJQuery(helper.padChrome$('iframe[name="ace_outer"]'));
|
||||
helper.padInner$ = getFrameJQuery(helper.padOuter$('iframe[name="ace_inner"]'));
|
||||
|
||||
// disable all animations, this makes tests faster and easier
|
||||
helper.padChrome$.fx.off = true;
|
||||
helper.padOuter$.fx.off = true;
|
||||
helper.padInner$.fx.off = true;
|
||||
|
||||
/*
|
||||
* chat messages received
|
||||
* @type {Array}
|
||||
*/
|
||||
helper.chatMessages = [];
|
||||
|
||||
/*
|
||||
* changeset commits from the server
|
||||
* @type {Array}
|
||||
*/
|
||||
helper.commits = [];
|
||||
|
||||
/*
|
||||
* userInfo messages from the server
|
||||
* @type {Array}
|
||||
*/
|
||||
helper.userInfos = [];
|
||||
|
||||
// listen for server messages
|
||||
helper.spyOnSocketIO();
|
||||
opts.cb();
|
||||
}).fail(() => {
|
||||
if (helper.retry > 3) {
|
||||
throw new Error('Pad never loaded');
|
||||
}
|
||||
helper.retry++;
|
||||
helper.newPad(cb, origPadName);
|
||||
});
|
||||
});
|
||||
|
||||
return padName;
|
||||
};
|
||||
|
||||
helper.waitFor = function (conditionFunc, timeoutTime = 1900, intervalTime = 10) {
|
||||
const deferred = new $.Deferred();
|
||||
|
||||
const _fail = deferred.fail.bind(deferred);
|
||||
let listenForFail = false;
|
||||
deferred.fail = (...args) => {
|
||||
listenForFail = true;
|
||||
return _fail(...args);
|
||||
};
|
||||
|
||||
const check = () => {
|
||||
try {
|
||||
if (!conditionFunc()) return;
|
||||
deferred.resolve();
|
||||
} catch (err) {
|
||||
deferred.reject(err);
|
||||
}
|
||||
clearInterval(intervalCheck);
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
|
||||
const intervalCheck = setInterval(check, intervalTime);
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
clearInterval(intervalCheck);
|
||||
const error = new Error(`wait for condition never became true ${conditionFunc.toString()}`);
|
||||
deferred.reject(error);
|
||||
|
||||
if (!listenForFail) {
|
||||
throw error;
|
||||
}
|
||||
}, timeoutTime);
|
||||
|
||||
// Check right away to avoid an unnecessary sleep if the condition is already true.
|
||||
check();
|
||||
|
||||
return deferred;
|
||||
};
|
||||
|
||||
/**
|
||||
* Same as `waitFor` but using Promises
|
||||
*
|
||||
* @returns {Promise}
|
||||
*
|
||||
*/
|
||||
helper.waitForPromise = async function (...args) {
|
||||
// Note: waitFor() has a strange API: On timeout it rejects, but it also throws an uncatchable
|
||||
// exception unless .fail() has been called. That uncatchable exception is disabled here by
|
||||
// passing a no-op function to .fail().
|
||||
return await this.waitFor(...args).fail(() => {});
|
||||
};
|
||||
|
||||
helper.selectLines = function ($startLine, $endLine, startOffset, endOffset) {
|
||||
// if no offset is provided, use beginning of start line and end of end line
|
||||
startOffset = startOffset || 0;
|
||||
endOffset = endOffset === undefined ? $endLine.text().length : endOffset;
|
||||
|
||||
const inner$ = helper.padInner$;
|
||||
const selection = inner$.document.getSelection();
|
||||
const range = selection.getRangeAt(0);
|
||||
|
||||
const start = getTextNodeAndOffsetOf($startLine, startOffset);
|
||||
const end = getTextNodeAndOffsetOf($endLine, endOffset);
|
||||
|
||||
range.setStart(start.node, start.offset);
|
||||
range.setEnd(end.node, end.offset);
|
||||
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
};
|
||||
|
||||
const getTextNodeAndOffsetOf = function ($targetLine, targetOffsetAtLine) {
|
||||
const $textNodes = $targetLine.find('*').contents().filter(function () {
|
||||
return this.nodeType === Node.TEXT_NODE;
|
||||
});
|
||||
|
||||
// search node where targetOffsetAtLine is reached, and its 'inner offset'
|
||||
let textNodeWhereOffsetIs = null;
|
||||
let offsetBeforeTextNode = 0;
|
||||
let offsetInsideTextNode = 0;
|
||||
$textNodes.each((index, element) => {
|
||||
const elementTotalOffset = element.textContent.length;
|
||||
textNodeWhereOffsetIs = element;
|
||||
offsetInsideTextNode = targetOffsetAtLine - offsetBeforeTextNode;
|
||||
|
||||
const foundTextNode = offsetBeforeTextNode + elementTotalOffset >= targetOffsetAtLine;
|
||||
if (foundTextNode) {
|
||||
return false; // stop .each by returning false
|
||||
}
|
||||
|
||||
offsetBeforeTextNode += elementTotalOffset;
|
||||
});
|
||||
|
||||
// edge cases
|
||||
if (textNodeWhereOffsetIs == null) {
|
||||
// there was no text node inside $targetLine, so it is an empty line (<br>).
|
||||
// Use beginning of line
|
||||
textNodeWhereOffsetIs = $targetLine.get(0);
|
||||
offsetInsideTextNode = 0;
|
||||
}
|
||||
// avoid errors if provided targetOffsetAtLine is higher than line offset (maxOffset).
|
||||
// Use max allowed instead
|
||||
const maxOffset = textNodeWhereOffsetIs.textContent.length;
|
||||
offsetInsideTextNode = Math.min(offsetInsideTextNode, maxOffset);
|
||||
|
||||
return {
|
||||
node: textNodeWhereOffsetIs,
|
||||
offset: offsetInsideTextNode,
|
||||
};
|
||||
};
|
||||
|
||||
/* Ensure console.log doesn't blow up in IE, ugly but ok for a test framework imho*/
|
||||
window.console = window.console || {};
|
||||
window.console.log = window.console.log || function () {};
|
||||
})();
|
238
src/tests/frontend/helper/methods.js
Normal file
238
src/tests/frontend/helper/methods.js
Normal file
|
@ -0,0 +1,238 @@
|
|||
'use strict';
|
||||
|
||||
/**
|
||||
* Spys on socket.io messages and saves them into several arrays
|
||||
* that are visible in tests
|
||||
*/
|
||||
helper.spyOnSocketIO = function () {
|
||||
helper.contentWindow().pad.socket.on('message', (msg) => {
|
||||
if (msg.type === 'COLLABROOM') {
|
||||
if (msg.data.type === 'ACCEPT_COMMIT') {
|
||||
helper.commits.push(msg);
|
||||
} else if (msg.data.type === 'USER_NEWINFO') {
|
||||
helper.userInfos.push(msg);
|
||||
} else if (msg.data.type === 'CHAT_MESSAGE') {
|
||||
helper.chatMessages.push(msg);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes an edit via `sendkeys` to the position of the caret and ensures ACCEPT_COMMIT
|
||||
* is returned by the server
|
||||
* It does not check if the ACCEPT_COMMIT is the edit sent, though
|
||||
* If `line` is not given, the edit goes to line no. 1
|
||||
*
|
||||
* @param {string} message The edit to make - can be anything supported by `sendkeys`
|
||||
* @param {number} [line] the optional line to make the edit on starting from 1
|
||||
* @returns {Promise}
|
||||
* @todo needs to support writing to a specified caret position
|
||||
*
|
||||
*/
|
||||
helper.edit = async function (message, line) {
|
||||
const editsNum = helper.commits.length;
|
||||
line = line ? line - 1 : 0;
|
||||
helper.linesDiv()[line].sendkeys(message);
|
||||
return helper.waitForPromise(() => editsNum + 1 === helper.commits.length);
|
||||
};
|
||||
|
||||
/**
|
||||
* The pad text as an array of divs
|
||||
*
|
||||
* @example
|
||||
* helper.linesDiv()[2].sendkeys('abc') // sends abc to the third line
|
||||
*
|
||||
* @returns {Array.<HTMLElement>} array of divs
|
||||
*/
|
||||
helper.linesDiv = function () {
|
||||
return helper.padInner$('.ace-line').map(function () {
|
||||
return $(this);
|
||||
}).get();
|
||||
};
|
||||
|
||||
/**
|
||||
* The pad text as an array of lines
|
||||
* For lines in timeslider use `helper.timesliderTextLines()`
|
||||
*
|
||||
* @returns {Array.<string>} lines of text
|
||||
*/
|
||||
helper.textLines = function () {
|
||||
return helper.linesDiv().map((div) => div.text());
|
||||
};
|
||||
|
||||
/**
|
||||
* The default pad text transmitted via `clientVars`
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
helper.defaultText = function () {
|
||||
return helper.padChrome$.window.clientVars.collab_client_vars.initialAttributedText.text;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends a chat `message` via `sendKeys`
|
||||
* You *must* include `{enter}` at the end of the string or it will
|
||||
* just fill the input field but not send the message.
|
||||
*
|
||||
* @todo Cannot send multiple messages at once
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* `helper.sendChatMessage('hi{enter}')`
|
||||
*
|
||||
* @param {string} message the chat message to be sent
|
||||
* @returns {Promise}
|
||||
*/
|
||||
helper.sendChatMessage = function (message) {
|
||||
const noOfChatMessages = helper.chatMessages.length;
|
||||
helper.padChrome$('#chatinput').sendkeys(message);
|
||||
return helper.waitForPromise(() => noOfChatMessages + 1 === helper.chatMessages.length);
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens the settings menu if its hidden via button
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
helper.showSettings = function () {
|
||||
if (!helper.isSettingsShown()) {
|
||||
helper.settingsButton().click();
|
||||
return helper.waitForPromise(() => helper.isSettingsShown(), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Hide the settings menu if its open via button
|
||||
*
|
||||
* @returns {Promise}
|
||||
* @todo untested
|
||||
*/
|
||||
helper.hideSettings = function () {
|
||||
if (helper.isSettingsShown()) {
|
||||
helper.settingsButton().click();
|
||||
return helper.waitForPromise(() => !helper.isSettingsShown(), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes the chat window sticky via settings menu if the settings menu is
|
||||
* open and sticky button is not checked
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
helper.enableStickyChatviaSettings = function () {
|
||||
const stickyChat = helper.padChrome$('#options-stickychat');
|
||||
if (helper.isSettingsShown() && !stickyChat.is(':checked')) {
|
||||
stickyChat.click();
|
||||
return helper.waitForPromise(() => helper.isChatboxSticky(), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Unsticks the chat window via settings menu if the settings menu is open
|
||||
* and sticky button is checked
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
helper.disableStickyChatviaSettings = function () {
|
||||
const stickyChat = helper.padChrome$('#options-stickychat');
|
||||
if (helper.isSettingsShown() && stickyChat.is(':checked')) {
|
||||
stickyChat.click();
|
||||
return helper.waitForPromise(() => !helper.isChatboxSticky(), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes the chat window sticky via an icon on the top right of the chat
|
||||
* window
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
helper.enableStickyChatviaIcon = function () {
|
||||
const stickyChat = helper.padChrome$('#titlesticky');
|
||||
if (helper.isChatboxShown() && !helper.isChatboxSticky()) {
|
||||
stickyChat.click();
|
||||
return helper.waitForPromise(() => helper.isChatboxSticky(), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Disables the stickyness of the chat window via an icon on the
|
||||
* upper right
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
helper.disableStickyChatviaIcon = function () {
|
||||
if (helper.isChatboxShown() && helper.isChatboxSticky()) {
|
||||
helper.titlecross().click();
|
||||
return helper.waitForPromise(() => !helper.isChatboxSticky(), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the src-attribute of the main iframe to the timeslider
|
||||
* In case a revision is given, sets the timeslider to this specific revision.
|
||||
* Defaults to going to the last revision.
|
||||
* It waits until the timer is filled with date and time, because it's one of the
|
||||
* last things that happen during timeslider load
|
||||
*
|
||||
* @param {number} [revision] the optional revision
|
||||
* @returns {Promise}
|
||||
* @todo for some reason this does only work the first time, you cannot
|
||||
* goto rev 0 and then via the same method to rev 5. Use buttons instead
|
||||
*/
|
||||
helper.gotoTimeslider = function (revision) {
|
||||
revision = Number.isInteger(revision) ? `#${revision}` : '';
|
||||
const iframe = $('#iframe-container iframe');
|
||||
iframe.attr('src', `${iframe.attr('src')}/timeslider${revision}`);
|
||||
|
||||
return helper.waitForPromise(() => helper.timesliderTimerTime() &&
|
||||
!Number.isNaN(new Date(helper.timesliderTimerTime()).getTime()), 10000);
|
||||
};
|
||||
|
||||
/**
|
||||
* Clicks in the timeslider at a specific offset
|
||||
* It's used to navigate the timeslider
|
||||
*
|
||||
* @todo no mousemove test
|
||||
* @param {number} X coordinate
|
||||
*/
|
||||
helper.sliderClick = function (X) {
|
||||
const sliderBar = helper.sliderBar();
|
||||
const edown = new jQuery.Event('mousedown');
|
||||
const eup = new jQuery.Event('mouseup');
|
||||
edown.clientX = eup.clientX = X;
|
||||
edown.clientY = eup.clientY = sliderBar.offset().top;
|
||||
|
||||
sliderBar.trigger(edown);
|
||||
sliderBar.trigger(eup);
|
||||
};
|
||||
|
||||
/**
|
||||
* The timeslider text as an array of lines
|
||||
*
|
||||
* @returns {Array.<string>} lines of text
|
||||
*/
|
||||
helper.timesliderTextLines = function () {
|
||||
return helper.contentWindow().$('.ace-line').map(function () {
|
||||
return $(this).text();
|
||||
}).get();
|
||||
};
|
||||
|
||||
helper.padIsEmpty = () => (
|
||||
!helper.padInner$.document.getSelection().isCollapsed ||
|
||||
(helper.padInner$('div').length === 1 && helper.padInner$('div').first().html() === '<br>'));
|
||||
|
||||
helper.clearPad = async () => {
|
||||
if (helper.padIsEmpty()) return;
|
||||
const commitsBefore = helper.commits.length;
|
||||
const lines = helper.linesDiv();
|
||||
helper.selectLines(lines[0], lines[lines.length - 1]);
|
||||
await helper.waitForPromise(() => !helper.padInner$.document.getSelection().isCollapsed);
|
||||
const e = new helper.padInner$.Event(helper.evtType);
|
||||
e.keyCode = 8; // delete key
|
||||
helper.padInner$('#innerdocbody').trigger(e);
|
||||
await helper.waitForPromise(helper.padIsEmpty);
|
||||
await helper.waitForPromise(() => helper.commits.length > commitsBefore);
|
||||
};
|
191
src/tests/frontend/helper/ui.js
Normal file
191
src/tests/frontend/helper/ui.js
Normal file
|
@ -0,0 +1,191 @@
|
|||
'use strict';
|
||||
|
||||
/**
|
||||
* the contentWindow is either the normal pad or timeslider
|
||||
*
|
||||
* @returns {HTMLElement} contentWindow
|
||||
*/
|
||||
helper.contentWindow = function () {
|
||||
return $('#iframe-container iframe')[0].contentWindow;
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens the chat unless it is already open via an
|
||||
* icon on the bottom right of the page
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
helper.showChat = function () {
|
||||
const chaticon = helper.chatIcon();
|
||||
if (chaticon.hasClass('visible')) {
|
||||
chaticon.click();
|
||||
return helper.waitForPromise(() => !chaticon.hasClass('visible'), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Closes the chat window if it is shown and not sticky
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
helper.hideChat = function () {
|
||||
if (helper.isChatboxShown() && !helper.isChatboxSticky()) {
|
||||
helper.titlecross().click();
|
||||
return helper.waitForPromise(() => !helper.isChatboxShown(), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the chat icon from the bottom right of the page
|
||||
*
|
||||
* @returns {HTMLElement} the chat icon
|
||||
*/
|
||||
helper.chatIcon = function () { return helper.padChrome$('#chaticon'); };
|
||||
|
||||
/**
|
||||
* The chat messages from the UI
|
||||
*
|
||||
* @returns {Array.<HTMLElement>}
|
||||
*/
|
||||
helper.chatTextParagraphs = function () { return helper.padChrome$('#chattext').children('p'); };
|
||||
|
||||
/**
|
||||
* Returns true if the chat box is sticky
|
||||
*
|
||||
* @returns {boolean} stickyness of the chat box
|
||||
*/
|
||||
helper.isChatboxSticky = function () {
|
||||
return helper.padChrome$('#chatbox').hasClass('stickyChat');
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if the chat box is shown
|
||||
*
|
||||
* @returns {boolean} visibility of the chat box
|
||||
*/
|
||||
helper.isChatboxShown = function () {
|
||||
return helper.padChrome$('#chatbox').hasClass('visible');
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the settings menu
|
||||
*
|
||||
* @returns {HTMLElement} the settings menu
|
||||
*/
|
||||
helper.settingsMenu = function () { return helper.padChrome$('#settings'); };
|
||||
|
||||
/**
|
||||
* Gets the settings button
|
||||
*
|
||||
* @returns {HTMLElement} the settings button
|
||||
*/
|
||||
helper.settingsButton = function () {
|
||||
return helper.padChrome$("button[data-l10n-id='pad.toolbar.settings.title']");
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggles user list
|
||||
*/
|
||||
helper.toggleUserList = async function () {
|
||||
const isVisible = helper.userListShown();
|
||||
const button = helper.padChrome$("button[data-l10n-id='pad.toolbar.showusers.title']");
|
||||
button.click();
|
||||
await helper.waitForPromise(() => !isVisible);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the user name input field
|
||||
*
|
||||
* @returns {HTMLElement} user name input field
|
||||
*/
|
||||
helper.usernameField = function () {
|
||||
return helper.padChrome$("input[data-l10n-id='pad.userlist.entername']");
|
||||
};
|
||||
|
||||
/**
|
||||
* Is the user list popup shown?
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
helper.userListShown = function () {
|
||||
return helper.padChrome$('div#users').hasClass('popup-show');
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the user name
|
||||
*
|
||||
*/
|
||||
helper.setUserName = async (name) => {
|
||||
const userElement = helper.usernameField();
|
||||
userElement.click();
|
||||
userElement.val(name);
|
||||
userElement.blur();
|
||||
await helper.waitForPromise(() => !helper.usernameField().hasClass('editactive'));
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the titlecross icon
|
||||
*
|
||||
* @returns {HTMLElement} the titlecross icon
|
||||
*/
|
||||
helper.titlecross = function () { return helper.padChrome$('#titlecross'); };
|
||||
|
||||
/**
|
||||
* Returns true if the settings menu is visible
|
||||
*
|
||||
* @returns {boolean} is the settings menu shown?
|
||||
*/
|
||||
helper.isSettingsShown = function () {
|
||||
return helper.padChrome$('#settings').hasClass('popup-show');
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the timer div of a timeslider that has the datetime of the revision
|
||||
*
|
||||
* @returns {HTMLElement} timer
|
||||
*/
|
||||
helper.timesliderTimer = function () {
|
||||
if (typeof helper.contentWindow().$ === 'function') {
|
||||
return helper.contentWindow().$('#timer');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the time of the revision on a timeslider
|
||||
*
|
||||
* @returns {HTMLElement} timer
|
||||
*/
|
||||
helper.timesliderTimerTime = function () {
|
||||
if (helper.timesliderTimer()) {
|
||||
return helper.timesliderTimer().text();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The ui-slidar-bar element in the timeslider
|
||||
*
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
helper.sliderBar = function () {
|
||||
return helper.contentWindow().$('#ui-slider-bar');
|
||||
};
|
||||
|
||||
/**
|
||||
* revision_date element
|
||||
* like "Saved October 10, 2020"
|
||||
*
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
helper.revisionDateElem = function () {
|
||||
return helper.contentWindow().$('#revision_date').text();
|
||||
};
|
||||
|
||||
/**
|
||||
* revision_label element
|
||||
* like "Version 1"
|
||||
*
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
helper.revisionLabelElem = function () {
|
||||
return helper.contentWindow().$('#revision_label');
|
||||
};
|
26
src/tests/frontend/index.html
Normal file
26
src/tests/frontend/index.html
Normal file
|
@ -0,0 +1,26 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<title>Frontend tests</title>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<link rel="stylesheet" href="runner.css" />
|
||||
|
||||
<div id="console"></div>
|
||||
<div id="mocha"></div>
|
||||
<div id="iframe-container"></div>
|
||||
|
||||
<script src="/static/js/jquery.js"></script>
|
||||
<script src="/static/js/browser.js"></script>
|
||||
<script src="lib/underscore.js"></script>
|
||||
|
||||
<script src="lib/mocha.js"></script>
|
||||
<script> mocha.setup({ui: 'bdd', checkLeaks: true, timeout: 60000}) </script>
|
||||
<script src="lib/expect.js"></script>
|
||||
|
||||
<script src="helper.js"></script>
|
||||
<script src="helper/methods.js"></script>
|
||||
<script src="helper/ui.js"></script>
|
||||
|
||||
<script src="specs_list.js"></script>
|
||||
<script src="runner.js"></script>
|
||||
</html>
|
1247
src/tests/frontend/lib/expect.js
Normal file
1247
src/tests/frontend/lib/expect.js
Normal file
File diff suppressed because it is too large
Load diff
18115
src/tests/frontend/lib/mocha.js
Normal file
18115
src/tests/frontend/lib/mocha.js
Normal file
File diff suppressed because one or more lines are too long
467
src/tests/frontend/lib/sendkeys.js
Normal file
467
src/tests/frontend/lib/sendkeys.js
Normal file
|
@ -0,0 +1,467 @@
|
|||
// Cross-broswer implementation of text ranges and selections
|
||||
// documentation: http://bililite.com/blog/2011/01/11/cross-browser-.and-selections/
|
||||
// Version: 1.1
|
||||
// Copyright (c) 2010 Daniel Wachsstock
|
||||
// MIT license:
|
||||
// Permission is hereby granted, free of charge, to any person
|
||||
// obtaining a copy of this software and associated documentation
|
||||
// files (the "Software"), to deal in the Software without
|
||||
// restriction, including without limitation the rights to use,
|
||||
// copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following
|
||||
// conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
// OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
(function($){
|
||||
|
||||
bililiteRange = function(el, debug){
|
||||
var ret;
|
||||
if (debug){
|
||||
ret = new NothingRange(); // Easier to force it to use the no-selection type than to try to find an old browser
|
||||
}else if (document.selection && !document.addEventListener){
|
||||
// Internet Explorer 8 and lower
|
||||
ret = new IERange();
|
||||
}else if (window.getSelection && el.setSelectionRange){
|
||||
// Standards. Element is an input or textarea
|
||||
ret = new InputRange();
|
||||
}else if (window.getSelection){
|
||||
// Standards, with any other kind of element
|
||||
ret = new W3CRange()
|
||||
}else{
|
||||
// doesn't support selection
|
||||
ret = new NothingRange();
|
||||
}
|
||||
ret._el = el;
|
||||
ret._doc = el.ownerDocument;
|
||||
ret._win = 'defaultView' in ret._doc ? ret._doc.defaultView : ret._doc.parentWindow;
|
||||
ret._textProp = textProp(el);
|
||||
ret._bounds = [0, ret.length()];
|
||||
return ret;
|
||||
}
|
||||
|
||||
function textProp(el){
|
||||
// returns the property that contains the text of the element
|
||||
if (typeof el.value != 'undefined') return 'value';
|
||||
if (typeof el.text != 'undefined') return 'text';
|
||||
if (typeof el.textContent != 'undefined') return 'textContent';
|
||||
return 'innerText';
|
||||
}
|
||||
|
||||
// base class
|
||||
function Range(){}
|
||||
Range.prototype = {
|
||||
length: function() {
|
||||
return this._el[this._textProp].replace(/\r/g, '').length; // need to correct for IE's CrLf weirdness
|
||||
},
|
||||
bounds: function(s){
|
||||
if (s === 'all'){
|
||||
this._bounds = [0, this.length()];
|
||||
}else if (s === 'start'){
|
||||
this._bounds = [0, 0];
|
||||
}else if (s === 'end'){
|
||||
this._bounds = [this.length(), this.length()];
|
||||
}else if (s === 'selection'){
|
||||
this.bounds ('all'); // first select the whole thing for constraining
|
||||
this._bounds = this._nativeSelection();
|
||||
}else if (s){
|
||||
this._bounds = s; // don't error check now; the element may change at any moment, so constrain it when we need it.
|
||||
}else{
|
||||
var b = [
|
||||
Math.max(0, Math.min (this.length(), this._bounds[0])),
|
||||
Math.max(0, Math.min (this.length(), this._bounds[1]))
|
||||
];
|
||||
return b; // need to constrain it to fit
|
||||
}
|
||||
return this; // allow for chaining
|
||||
},
|
||||
select: function(){
|
||||
this._nativeSelect(this._nativeRange(this.bounds()));
|
||||
return this; // allow for chaining
|
||||
},
|
||||
text: function(text, select){
|
||||
if (arguments.length){
|
||||
this._nativeSetText(text, this._nativeRange(this.bounds()));
|
||||
if (select == 'start'){
|
||||
this.bounds ([this._bounds[0], this._bounds[0]]);
|
||||
this.select();
|
||||
}else if (select == 'end'){
|
||||
this.bounds ([this._bounds[0]+text.length, this._bounds[0]+text.length]);
|
||||
this.select();
|
||||
}else if (select == 'all'){
|
||||
this.bounds ([this._bounds[0], this._bounds[0]+text.length]);
|
||||
this.select();
|
||||
}
|
||||
return this; // allow for chaining
|
||||
}else{
|
||||
return this._nativeGetText(this._nativeRange(this.bounds()));
|
||||
}
|
||||
},
|
||||
insertEOL: function (){
|
||||
this._nativeEOL();
|
||||
this._bounds = [this._bounds[0]+1, this._bounds[0]+1]; // move past the EOL marker
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function IERange(){}
|
||||
IERange.prototype = new Range();
|
||||
IERange.prototype._nativeRange = function (bounds){
|
||||
var rng;
|
||||
if (this._el.tagName == 'INPUT'){
|
||||
// IE 8 is very inconsistent; textareas have createTextRange but it doesn't work
|
||||
rng = this._el.createTextRange();
|
||||
}else{
|
||||
rng = this._doc.body.createTextRange ();
|
||||
rng.moveToElementText(this._el);
|
||||
}
|
||||
if (bounds){
|
||||
if (bounds[1] < 0) bounds[1] = 0; // IE tends to run elements out of bounds
|
||||
if (bounds[0] > this.length()) bounds[0] = this.length();
|
||||
if (bounds[1] < rng.text.replace(/\r/g, '').length){ // correct for IE's CrLf wierdness
|
||||
// block-display elements have an invisible, uncounted end of element marker, so we move an extra one and use the current length of the range
|
||||
rng.moveEnd ('character', -1);
|
||||
rng.moveEnd ('character', bounds[1]-rng.text.replace(/\r/g, '').length);
|
||||
}
|
||||
if (bounds[0] > 0) rng.moveStart('character', bounds[0]);
|
||||
}
|
||||
return rng;
|
||||
};
|
||||
IERange.prototype._nativeSelect = function (rng){
|
||||
rng.select();
|
||||
};
|
||||
IERange.prototype._nativeSelection = function (){
|
||||
// returns [start, end] for the selection constrained to be in element
|
||||
var rng = this._nativeRange(); // range of the element to constrain to
|
||||
var len = this.length();
|
||||
if (this._doc.selection.type != 'Text') return [0,0]; // append to the end
|
||||
var sel = this._doc.selection.createRange();
|
||||
try{
|
||||
return [
|
||||
iestart(sel, rng),
|
||||
ieend (sel, rng)
|
||||
];
|
||||
}catch (e){
|
||||
// IE gets upset sometimes about comparing text to input elements, but the selections cannot overlap, so make a best guess
|
||||
return (sel.parentElement().sourceIndex < this._el.sourceIndex) ? [0,0] : [len, len];
|
||||
}
|
||||
};
|
||||
IERange.prototype._nativeGetText = function (rng){
|
||||
return rng.text.replace(/\r/g, ''); // correct for IE's CrLf weirdness
|
||||
};
|
||||
IERange.prototype._nativeSetText = function (text, rng){
|
||||
rng.text = text;
|
||||
};
|
||||
IERange.prototype._nativeEOL = function(){
|
||||
if (typeof this._el.value != 'undefined'){
|
||||
this.text('\n'); // for input and textarea, insert it straight
|
||||
}else{
|
||||
this._nativeRange(this.bounds()).pasteHTML('<br/>');
|
||||
}
|
||||
};
|
||||
// IE internals
|
||||
function iestart(rng, constraint){
|
||||
// returns the position (in character) of the start of rng within constraint. If it's not in constraint, returns 0 if it's before, length if it's after
|
||||
var len = constraint.text.replace(/\r/g, '').length; // correct for IE's CrLf wierdness
|
||||
if (rng.compareEndPoints ('StartToStart', constraint) <= 0) return 0; // at or before the beginning
|
||||
if (rng.compareEndPoints ('StartToEnd', constraint) >= 0) return len;
|
||||
for (var i = 0; rng.compareEndPoints ('StartToStart', constraint) > 0; ++i, rng.moveStart('character', -1));
|
||||
return i;
|
||||
}
|
||||
function ieend (rng, constraint){
|
||||
// returns the position (in character) of the end of rng within constraint. If it's not in constraint, returns 0 if it's before, length if it's after
|
||||
var len = constraint.text.replace(/\r/g, '').length; // correct for IE's CrLf wierdness
|
||||
if (rng.compareEndPoints ('EndToEnd', constraint) >= 0) return len; // at or after the end
|
||||
if (rng.compareEndPoints ('EndToStart', constraint) <= 0) return 0;
|
||||
for (var i = 0; rng.compareEndPoints ('EndToStart', constraint) > 0; ++i, rng.moveEnd('character', -1));
|
||||
return i;
|
||||
}
|
||||
|
||||
// an input element in a standards document. "Native Range" is just the bounds array
|
||||
function InputRange(){}
|
||||
InputRange.prototype = new Range();
|
||||
InputRange.prototype._nativeRange = function(bounds) {
|
||||
return bounds || [0, this.length()];
|
||||
};
|
||||
InputRange.prototype._nativeSelect = function (rng){
|
||||
this._el.setSelectionRange(rng[0], rng[1]);
|
||||
};
|
||||
InputRange.prototype._nativeSelection = function(){
|
||||
return [this._el.selectionStart, this._el.selectionEnd];
|
||||
};
|
||||
InputRange.prototype._nativeGetText = function(rng){
|
||||
return this._el.value.substring(rng[0], rng[1]);
|
||||
};
|
||||
InputRange.prototype._nativeSetText = function(text, rng){
|
||||
var val = this._el.value;
|
||||
this._el.value = val.substring(0, rng[0]) + text + val.substring(rng[1]);
|
||||
};
|
||||
InputRange.prototype._nativeEOL = function(){
|
||||
this.text('\n');
|
||||
};
|
||||
|
||||
function W3CRange(){}
|
||||
W3CRange.prototype = new Range();
|
||||
W3CRange.prototype._nativeRange = function (bounds){
|
||||
var rng = this._doc.createRange();
|
||||
rng.selectNodeContents(this._el);
|
||||
if (bounds){
|
||||
w3cmoveBoundary (rng, bounds[0], true, this._el);
|
||||
rng.collapse (true);
|
||||
w3cmoveBoundary (rng, bounds[1]-bounds[0], false, this._el);
|
||||
}
|
||||
return rng;
|
||||
};
|
||||
W3CRange.prototype._nativeSelect = function (rng){
|
||||
this._win.getSelection().removeAllRanges();
|
||||
this._win.getSelection().addRange (rng);
|
||||
};
|
||||
W3CRange.prototype._nativeSelection = function (){
|
||||
// returns [start, end] for the selection constrained to be in element
|
||||
var rng = this._nativeRange(); // range of the element to constrain to
|
||||
if (this._win.getSelection().rangeCount == 0) return [this.length(), this.length()]; // append to the end
|
||||
var sel = this._win.getSelection().getRangeAt(0);
|
||||
return [
|
||||
w3cstart(sel, rng),
|
||||
w3cend (sel, rng)
|
||||
];
|
||||
}
|
||||
W3CRange.prototype._nativeGetText = function (rng){
|
||||
return rng.toString();
|
||||
};
|
||||
W3CRange.prototype._nativeSetText = function (text, rng){
|
||||
rng.deleteContents();
|
||||
rng.insertNode (this._doc.createTextNode(text));
|
||||
this._el.normalize(); // merge the text with the surrounding text
|
||||
};
|
||||
W3CRange.prototype._nativeEOL = function(){
|
||||
var rng = this._nativeRange(this.bounds());
|
||||
rng.deleteContents();
|
||||
var br = this._doc.createElement('br');
|
||||
br.setAttribute ('_moz_dirty', ''); // for Firefox
|
||||
rng.insertNode (br);
|
||||
rng.insertNode (this._doc.createTextNode('\n'));
|
||||
rng.collapse (false);
|
||||
};
|
||||
// W3C internals
|
||||
function nextnode (node, root){
|
||||
// in-order traversal
|
||||
// we've already visited node, so get kids then siblings
|
||||
if (node.firstChild) return node.firstChild;
|
||||
if (node.nextSibling) return node.nextSibling;
|
||||
if (node===root) return null;
|
||||
while (node.parentNode){
|
||||
// get uncles
|
||||
node = node.parentNode;
|
||||
if (node == root) return null;
|
||||
if (node.nextSibling) return node.nextSibling;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function w3cmoveBoundary (rng, n, bStart, el){
|
||||
// move the boundary (bStart == true ? start : end) n characters forward, up to the end of element el. Forward only!
|
||||
// if the start is moved after the end, then an exception is raised
|
||||
if (n <= 0) return;
|
||||
var node = rng[bStart ? 'startContainer' : 'endContainer'];
|
||||
if (node.nodeType == 3){
|
||||
// we may be starting somewhere into the text
|
||||
n += rng[bStart ? 'startOffset' : 'endOffset'];
|
||||
}
|
||||
while (node){
|
||||
if (node.nodeType == 3){
|
||||
if (n <= node.nodeValue.length){
|
||||
rng[bStart ? 'setStart' : 'setEnd'](node, n);
|
||||
// special case: if we end next to a <br>, include that node.
|
||||
if (n == node.nodeValue.length){
|
||||
// skip past zero-length text nodes
|
||||
for (var next = nextnode (node, el); next && next.nodeType==3 && next.nodeValue.length == 0; next = nextnode(next, el)){
|
||||
rng[bStart ? 'setStartAfter' : 'setEndAfter'](next);
|
||||
}
|
||||
if (next && next.nodeType == 1 && next.nodeName == "BR") rng[bStart ? 'setStartAfter' : 'setEndAfter'](next);
|
||||
}
|
||||
return;
|
||||
}else{
|
||||
rng[bStart ? 'setStartAfter' : 'setEndAfter'](node); // skip past this one
|
||||
n -= node.nodeValue.length; // and eat these characters
|
||||
}
|
||||
}
|
||||
node = nextnode (node, el);
|
||||
}
|
||||
}
|
||||
var START_TO_START = 0; // from the w3c definitions
|
||||
var START_TO_END = 1;
|
||||
var END_TO_END = 2;
|
||||
var END_TO_START = 3;
|
||||
// from the Mozilla documentation, for range.compareBoundaryPoints(how, sourceRange)
|
||||
// -1, 0, or 1, indicating whether the corresponding boundary-point of range is respectively before, equal to, or after the corresponding boundary-point of sourceRange.
|
||||
// * Range.END_TO_END compares the end boundary-point of sourceRange to the end boundary-point of range.
|
||||
// * Range.END_TO_START compares the end boundary-point of sourceRange to the start boundary-point of range.
|
||||
// * Range.START_TO_END compares the start boundary-point of sourceRange to the end boundary-point of range.
|
||||
// * Range.START_TO_START compares the start boundary-point of sourceRange to the start boundary-point of range.
|
||||
function w3cstart(rng, constraint){
|
||||
if (rng.compareBoundaryPoints (START_TO_START, constraint) <= 0) return 0; // at or before the beginning
|
||||
if (rng.compareBoundaryPoints (END_TO_START, constraint) >= 0) return constraint.toString().length;
|
||||
rng = rng.cloneRange(); // don't change the original
|
||||
rng.setEnd (constraint.endContainer, constraint.endOffset); // they now end at the same place
|
||||
return constraint.toString().length - rng.toString().length;
|
||||
}
|
||||
function w3cend (rng, constraint){
|
||||
if (rng.compareBoundaryPoints (END_TO_END, constraint) >= 0) return constraint.toString().length; // at or after the end
|
||||
if (rng.compareBoundaryPoints (START_TO_END, constraint) <= 0) return 0;
|
||||
rng = rng.cloneRange(); // don't change the original
|
||||
rng.setStart (constraint.startContainer, constraint.startOffset); // they now start at the same place
|
||||
return rng.toString().length;
|
||||
}
|
||||
|
||||
function NothingRange(){}
|
||||
NothingRange.prototype = new Range();
|
||||
NothingRange.prototype._nativeRange = function(bounds) {
|
||||
return bounds || [0,this.length()];
|
||||
};
|
||||
NothingRange.prototype._nativeSelect = function (rng){ // do nothing
|
||||
};
|
||||
NothingRange.prototype._nativeSelection = function(){
|
||||
return [0,0];
|
||||
};
|
||||
NothingRange.prototype._nativeGetText = function (rng){
|
||||
return this._el[this._textProp].substring(rng[0], rng[1]);
|
||||
};
|
||||
NothingRange.prototype._nativeSetText = function (text, rng){
|
||||
var val = this._el[this._textProp];
|
||||
this._el[this._textProp] = val.substring(0, rng[0]) + text + val.substring(rng[1]);
|
||||
};
|
||||
NothingRange.prototype._nativeEOL = function(){
|
||||
this.text('\n');
|
||||
};
|
||||
|
||||
})(jQuery);
|
||||
|
||||
// insert characters in a textarea or text input field
|
||||
// special characters are enclosed in {}; use {{} for the { character itself
|
||||
// documentation: http://bililite.com/blog/2008/08/20/the-fnsendkeys-plugin/
|
||||
// Version: 2.0
|
||||
// Copyright (c) 2010 Daniel Wachsstock
|
||||
// MIT license:
|
||||
// Permission is hereby granted, free of charge, to any person
|
||||
// obtaining a copy of this software and associated documentation
|
||||
// files (the "Software"), to deal in the Software without
|
||||
// restriction, including without limitation the rights to use,
|
||||
// copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following
|
||||
// conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
// OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
(function($){
|
||||
|
||||
$.fn.sendkeys = function (x, opts){
|
||||
return this.each( function(){
|
||||
var localkeys = $.extend({}, opts, $(this).data('sendkeys')); // allow for element-specific key functions
|
||||
// most elements to not keep track of their selection when they lose focus, so we have to do it for them
|
||||
var rng = $.data (this, 'sendkeys.selection');
|
||||
if (!rng){
|
||||
rng = bililiteRange(this).bounds('selection');
|
||||
$.data(this, 'sendkeys.selection', rng);
|
||||
$(this).bind('mouseup.sendkeys', function(){
|
||||
// we have to update the saved range. The routines here update the bounds with each press, but actual keypresses and mouseclicks do not
|
||||
$.data(this, 'sendkeys.selection').bounds('selection');
|
||||
}).bind('keyup.sendkeys', function(evt){
|
||||
// restore the selection if we got here with a tab (a click should select what was clicked on)
|
||||
if (evt.which == 9){
|
||||
// there's a flash of selection when we restore the focus, but I don't know how to avoid that
|
||||
$.data(this, 'sendkeys.selection').select();
|
||||
}else{
|
||||
$.data(this, 'sendkeys.selection').bounds('selection');
|
||||
}
|
||||
});
|
||||
}
|
||||
this.focus();
|
||||
if (typeof x === 'undefined') return; // no string, so we just set up the event handlers
|
||||
$.data(this, 'sendkeys.originalText', rng.text());
|
||||
x.replace(/\n/g, '{enter}'). // turn line feeds into explicit break insertions
|
||||
replace(/{[^}]*}|[^{]+/g, function(s){
|
||||
(localkeys[s] || $.fn.sendkeys.defaults[s] || $.fn.sendkeys.defaults.simplechar)(rng, s);
|
||||
});
|
||||
$(this).trigger({type: 'sendkeys', which: x});
|
||||
});
|
||||
}; // sendkeys
|
||||
|
||||
|
||||
// add the functions publicly so they can be overridden
|
||||
$.fn.sendkeys.defaults = {
|
||||
simplechar: function (rng, s){
|
||||
rng.text(s, 'end');
|
||||
for (var i =0; i < s.length; ++i){
|
||||
var x = s.charCodeAt(i);
|
||||
// a bit of cheating: rng._el is the element associated with rng.
|
||||
$(rng._el).trigger({type: 'keypress', keyCode: x, which: x, charCode: x});
|
||||
}
|
||||
},
|
||||
'{{}': function (rng){
|
||||
$.fn.sendkeys.defaults.simplechar (rng, '{')
|
||||
},
|
||||
'{enter}': function (rng){
|
||||
rng.insertEOL();
|
||||
rng.select();
|
||||
var x = '\n'.charCodeAt(0);
|
||||
$(rng._el).trigger({type: 'keypress', keyCode: x, which: x, charCode: x});
|
||||
},
|
||||
'{backspace}': function (rng){
|
||||
var b = rng.bounds();
|
||||
if (b[0] == b[1]) rng.bounds([b[0]-1, b[0]]); // no characters selected; it's just an insertion point. Remove the previous character
|
||||
rng.text('', 'end'); // delete the characters and update the selection
|
||||
},
|
||||
'{del}': function (rng){
|
||||
var b = rng.bounds();
|
||||
if (b[0] == b[1]) rng.bounds([b[0], b[0]+1]); // no characters selected; it's just an insertion point. Remove the next character
|
||||
rng.text('', 'end'); // delete the characters and update the selection
|
||||
},
|
||||
'{rightarrow}': function (rng){
|
||||
var b = rng.bounds();
|
||||
if (b[0] == b[1]) ++b[1]; // no characters selected; it's just an insertion point. Move to the right
|
||||
rng.bounds([b[1], b[1]]).select();
|
||||
},
|
||||
'{leftarrow}': function (rng){
|
||||
var b = rng.bounds();
|
||||
if (b[0] == b[1]) --b[0]; // no characters selected; it's just an insertion point. Move to the left
|
||||
rng.bounds([b[0], b[0]]).select();
|
||||
},
|
||||
'{selectall}' : function (rng){
|
||||
rng.bounds('all').select();
|
||||
},
|
||||
'{selection}': function (rng){
|
||||
$.fn.sendkeys.defaults.simplechar(rng, $.data(rng._el, 'sendkeys.originalText'));
|
||||
},
|
||||
'{mark}' : function (rng){
|
||||
var bounds = rng.bounds();
|
||||
$(rng._el).one('sendkeys', function(){
|
||||
// set up the event listener to change the selection after the sendkeys is done
|
||||
rng.bounds(bounds).select();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
})(jQuery)
|
1200
src/tests/frontend/lib/underscore.js
Normal file
1200
src/tests/frontend/lib/underscore.js
Normal file
File diff suppressed because it is too large
Load diff
246
src/tests/frontend/runner.css
Normal file
246
src/tests/frontend/runner.css
Normal file
|
@ -0,0 +1,246 @@
|
|||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#console {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#iframe-container {
|
||||
width: 80%;
|
||||
min-width: 820px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#iframe-container iframe {
|
||||
height: 100%;
|
||||
width:100%;
|
||||
}
|
||||
|
||||
#mocha {
|
||||
font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
border-right: 2px solid #999;
|
||||
flex: 1 auto;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
width:20%;
|
||||
font-size:80%;
|
||||
|
||||
}
|
||||
|
||||
#mocha #report {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#mocha li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#mocha ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
#mocha h1, #mocha h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#mocha h1 {
|
||||
margin-top: 15px;
|
||||
font-size: 1em;
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
#mocha h1 a:visited
|
||||
{
|
||||
color: #00E;
|
||||
}
|
||||
|
||||
#mocha .suite .suite h1 {
|
||||
margin-top: 0;
|
||||
font-size: .8em;
|
||||
}
|
||||
|
||||
#mocha h2 {
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#mocha .suite {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
#mocha .test {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
#mocha .test:hover h2::after {
|
||||
position: relative;
|
||||
top: 0;
|
||||
right: -10px;
|
||||
content: '(view source)';
|
||||
font-size: 12px;
|
||||
font-family: arial;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
#mocha .test.pending:hover h2::after {
|
||||
content: '(pending)';
|
||||
font-family: arial;
|
||||
}
|
||||
|
||||
#mocha .test.pass.medium .duration {
|
||||
background: #C09853;
|
||||
}
|
||||
|
||||
#mocha .test.pass.slow .duration {
|
||||
background: #B94A48;
|
||||
}
|
||||
|
||||
#mocha .test.pass::before {
|
||||
content: '✓';
|
||||
font-size: 12px;
|
||||
display: block;
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
color: #00d6b2;
|
||||
}
|
||||
|
||||
#mocha .test.pass .duration {
|
||||
font-size: 9px;
|
||||
margin-left: 5px;
|
||||
padding: 2px 5px;
|
||||
color: white;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
|
||||
-moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
|
||||
-webkit-border-radius: 5px;
|
||||
-moz-border-radius: 5px;
|
||||
-ms-border-radius: 5px;
|
||||
-o-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#mocha .test.pass.fast .duration {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#mocha .test.pending {
|
||||
color: #0b97c4;
|
||||
}
|
||||
|
||||
#mocha .test.pending::before {
|
||||
content: '◦';
|
||||
color: #0b97c4;
|
||||
}
|
||||
|
||||
#mocha .test.fail {
|
||||
color: #c00;
|
||||
}
|
||||
|
||||
#mocha .test.fail pre {
|
||||
color: black;
|
||||
}
|
||||
|
||||
#mocha .test.fail::before {
|
||||
content: '✖';
|
||||
font-size: 12px;
|
||||
display: block;
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
color: #c00;
|
||||
}
|
||||
|
||||
#mocha .test pre.error {
|
||||
color: #c00;
|
||||
}
|
||||
|
||||
#mocha .test pre {
|
||||
display: inline-block;
|
||||
font: 12px/1.5 monaco, monospace;
|
||||
margin: 5px;
|
||||
padding: 15px;
|
||||
border: 1px solid #eee;
|
||||
border-bottom-color: #ddd;
|
||||
-webkit-border-radius: 3px;
|
||||
-webkit-box-shadow: 0 1px 3px #eee;
|
||||
}
|
||||
|
||||
#report ul {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#report.pass .test.fail {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#report.fail .test.pass {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#error {
|
||||
color: #c00;
|
||||
font-size: 1.5 em;
|
||||
font-weight: 100;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
#stats {
|
||||
padding: 10px;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
color: #888;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#mocha-stats {
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
#mocha-stats .progress {
|
||||
float: right;
|
||||
padding-top: 0;
|
||||
margin-right:5px;
|
||||
}
|
||||
|
||||
#stats em {
|
||||
color: black;
|
||||
}
|
||||
|
||||
#stats a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
#stats a:hover {
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
#stats li {
|
||||
display: inline-block;
|
||||
margin: 0 5px;
|
||||
list-style: none;
|
||||
padding-top: 11px;
|
||||
}
|
||||
|
||||
code .comment { color: #ddd }
|
||||
code .init { color: #2F6FAD }
|
||||
code .string { color: #5890AD }
|
||||
code .keyword { color: #8A6343 }
|
||||
code .number { color: #2F6FAD }
|
||||
|
||||
ul{
|
||||
padding-left:5px;
|
||||
}
|
184
src/tests/frontend/runner.js
Normal file
184
src/tests/frontend/runner.js
Normal file
|
@ -0,0 +1,184 @@
|
|||
'use strict';
|
||||
|
||||
/* global specs_list */
|
||||
|
||||
$(() => {
|
||||
const stringifyException = (exception) => {
|
||||
let err = exception.stack || exception.toString();
|
||||
|
||||
// FF / Opera do not add the message
|
||||
if (!~err.indexOf(exception.message)) {
|
||||
err = `${exception.message}\n${err}`;
|
||||
}
|
||||
|
||||
// <=IE7 stringifies to [Object Error]. Since it can be overloaded, we
|
||||
// check for the result of the stringifying.
|
||||
if (err === '[object Error]') err = exception.message;
|
||||
|
||||
// Safari doesn't give you a stack. Let's at least provide a source line.
|
||||
if (!exception.stack && exception.sourceURL && exception.line !== undefined) {
|
||||
err += `\n(${exception.sourceURL}:${exception.line})`;
|
||||
}
|
||||
|
||||
return err;
|
||||
};
|
||||
|
||||
const customRunner = (runner) => {
|
||||
const stats = {suites: 0, tests: 0, passes: 0, pending: 0, failures: 0};
|
||||
let level = 0;
|
||||
|
||||
if (!runner) return;
|
||||
|
||||
runner.on('start', () => {
|
||||
stats.start = new Date();
|
||||
});
|
||||
|
||||
runner.on('suite', (suite) => {
|
||||
suite.root || stats.suites++;
|
||||
if (suite.root) return;
|
||||
append(suite.title);
|
||||
level++;
|
||||
});
|
||||
|
||||
runner.on('suite end', (suite) => {
|
||||
if (suite.root) return;
|
||||
level--;
|
||||
|
||||
if (level === 0) {
|
||||
append('');
|
||||
}
|
||||
});
|
||||
|
||||
// Scroll down test display after each test
|
||||
const mochaEl = $('#mocha')[0];
|
||||
runner.on('test', () => {
|
||||
mochaEl.scrollTop = mochaEl.scrollHeight;
|
||||
});
|
||||
|
||||
// max time a test is allowed to run
|
||||
// TODO this should be lowered once timeslider_revision.js is faster
|
||||
let killTimeout;
|
||||
runner.on('test end', () => {
|
||||
stats.tests++;
|
||||
});
|
||||
|
||||
runner.on('pass', (test) => {
|
||||
if (killTimeout) clearTimeout(killTimeout);
|
||||
killTimeout = setTimeout(() => {
|
||||
append('FINISHED - [red]no test started since 3 minutes, tests stopped[clear]');
|
||||
}, 60000 * 3);
|
||||
|
||||
const medium = test.slow() / 2;
|
||||
test.speed = test.duration > test.slow()
|
||||
? 'slow'
|
||||
: test.duration > medium
|
||||
? 'medium'
|
||||
: 'fast';
|
||||
|
||||
stats.passes++;
|
||||
append(`-> [green]PASSED[clear] : ${test.title} ${test.duration} ms`);
|
||||
});
|
||||
|
||||
runner.on('fail', (test, err) => {
|
||||
if (killTimeout) clearTimeout(killTimeout);
|
||||
killTimeout = setTimeout(() => {
|
||||
append('FINISHED - [red]no test started since 3 minutes, tests stopped[clear]');
|
||||
}, 60000 * 3);
|
||||
|
||||
stats.failures++;
|
||||
test.err = err;
|
||||
append(`-> [red]FAILED[clear] : ${test.title} ${stringifyException(test.err)}`);
|
||||
});
|
||||
|
||||
runner.on('pending', (test) => {
|
||||
if (killTimeout) clearTimeout(killTimeout);
|
||||
killTimeout = setTimeout(() => {
|
||||
append('FINISHED - [red]no test started since 3 minutes, tests stopped[clear]');
|
||||
}, 60000 * 3);
|
||||
|
||||
stats.pending++;
|
||||
append(`-> [yellow]PENDING[clear]: ${test.title}`);
|
||||
});
|
||||
|
||||
const $console = $('#console');
|
||||
const append = (text) => {
|
||||
const oldText = $console.text();
|
||||
|
||||
let space = '';
|
||||
for (let i = 0; i < level * 2; i++) {
|
||||
space += ' ';
|
||||
}
|
||||
|
||||
let splitedText = '';
|
||||
_(text.split('\n')).each((line) => {
|
||||
while (line.length > 0) {
|
||||
const split = line.substr(0, 100);
|
||||
line = line.substr(100);
|
||||
if (splitedText.length > 0) splitedText += '\n';
|
||||
splitedText += split;
|
||||
}
|
||||
});
|
||||
|
||||
// indent all lines with the given amount of space
|
||||
const newText = _(splitedText.split('\n')).map((line) => space + line).join('\\n');
|
||||
|
||||
$console.text(`${oldText + newText}\\n`);
|
||||
};
|
||||
|
||||
const total = runner.total;
|
||||
runner.on('end', () => {
|
||||
stats.end = new Date();
|
||||
stats.duration = stats.end - stats.start;
|
||||
const minutes = Math.floor(stats.duration / 1000 / 60);
|
||||
// chrome < 57 does not like this .toString().padStart('2', '0');
|
||||
const seconds = Math.round((stats.duration / 1000) % 60);
|
||||
if (stats.tests === total) {
|
||||
append(`FINISHED - ${stats.passes} tests passed, ${stats.failures} tests failed, ` +
|
||||
`${stats.pending} pending, duration: ${minutes}:${seconds}`);
|
||||
} else if (stats.tests > total) {
|
||||
append(`FINISHED - but more tests than planned returned ${stats.passes} tests passed, ` +
|
||||
`${stats.failures} tests failed, ${stats.pending} pending, ` +
|
||||
`duration: ${minutes}:${seconds}`);
|
||||
append(`${total} tests, but ${stats.tests} returned. ` +
|
||||
'There is probably a problem with your async code or error handling, ' +
|
||||
'see https://github.com/mochajs/mocha/issues/1327');
|
||||
} else {
|
||||
append(`FINISHED - but not all tests returned ${stats.passes} tests passed, ` +
|
||||
`${stats.failures} tests failed, ${stats.pending} tests pending, ` +
|
||||
`duration: ${minutes}:${seconds}`);
|
||||
append(`${total} tests, but only ${stats.tests} returned. ` +
|
||||
'Check for failed before/beforeEach-hooks (no `test end` is called for them ' +
|
||||
'and subsequent tests of the same suite are skipped), ' +
|
||||
'see https://github.com/mochajs/mocha/pull/1043');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getURLParameter = (name) => (new URLSearchParams(location.search)).get(name);
|
||||
|
||||
// get the list of specs and filter it if requested
|
||||
const specs = specs_list.slice();
|
||||
|
||||
// inject spec scripts into the dom
|
||||
const $body = $('body');
|
||||
$.each(specs, (i, spec) => {
|
||||
// if the spec isn't a plugin spec which means the spec file might be in a different subfolder
|
||||
if (!spec.startsWith('/')) {
|
||||
$body.append(`<script src="specs/${spec}"></script>`);
|
||||
} else {
|
||||
$body.append(`<script src="${spec}"></script>`);
|
||||
}
|
||||
});
|
||||
|
||||
// initialize the test helper
|
||||
helper.init(() => {
|
||||
// configure and start the test framework
|
||||
const grep = getURLParameter('grep');
|
||||
if (grep != null) {
|
||||
mocha.grep(grep);
|
||||
}
|
||||
|
||||
const runner = mocha.run();
|
||||
customRunner(runner);
|
||||
});
|
||||
});
|
25
src/tests/frontend/specs/alphabet.js
Normal file
25
src/tests/frontend/specs/alphabet.js
Normal 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);
|
||||
});
|
||||
});
|
107
src/tests/frontend/specs/authorship_of_editions.js
Normal file
107
src/tests/frontend/specs/authorship_of_editions.js
Normal 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();
|
||||
});
|
||||
};
|
||||
});
|
65
src/tests/frontend/specs/bold.js
Normal file
65
src/tests/frontend/specs/bold.js
Normal 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();
|
||||
});
|
||||
});
|
352
src/tests/frontend/specs/caret.js
Normal file
352
src/tests/frontend/specs/caret.js
Normal 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;
|
||||
};
|
107
src/tests/frontend/specs/change_user_color.js
Normal file
107
src/tests/frontend/specs/change_user_color.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
36
src/tests/frontend/specs/change_user_name.js
Normal file
36
src/tests/frontend/specs/change_user_name.js
Normal 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;
|
||||
});
|
||||
});
|
||||
});
|
116
src/tests/frontend/specs/chat.js
Normal file
116
src/tests/frontend/specs/chat.js
Normal 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);
|
||||
});
|
||||
});
|
80
src/tests/frontend/specs/chat_load_messages.js
Normal file
80
src/tests/frontend/specs/chat_load_messages.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
121
src/tests/frontend/specs/clear_authorship_colors.js
Normal file
121
src/tests/frontend/specs/clear_authorship_colors.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
33
src/tests/frontend/specs/delete.js
Normal file
33
src/tests/frontend/specs/delete.js
Normal 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();
|
||||
});
|
||||
});
|
171
src/tests/frontend/specs/drag_and_drop.js
Normal file
171
src/tests/frontend/specs/drag_and_drop.js
Normal 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);
|
||||
};
|
||||
});
|
135
src/tests/frontend/specs/embed_value.js
Normal file
135
src/tests/frontend/specs/embed_value.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
31
src/tests/frontend/specs/enter.js
Normal file
31
src/tests/frontend/specs/enter.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
34
src/tests/frontend/specs/font_type.js
Normal file
34
src/tests/frontend/specs/font_type.js
Normal 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();
|
||||
});
|
||||
});
|
468
src/tests/frontend/specs/helper.js
Normal file
468
src/tests/frontend/specs/helper.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
329
src/tests/frontend/specs/importexport.js
Normal file
329
src/tests/frontend/specs/importexport.js
Normal 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();
|
||||
});
|
||||
});
|
129
src/tests/frontend/specs/importindents.js
Normal file
129
src/tests/frontend/specs/importindents.js
Normal 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();
|
||||
});
|
||||
});
|
315
src/tests/frontend/specs/indentation.js
Normal file
315
src/tests/frontend/specs/indentation.js
Normal 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);
|
||||
};
|
67
src/tests/frontend/specs/italic.js
Normal file
67
src/tests/frontend/specs/italic.js
Normal 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();
|
||||
});
|
||||
});
|
125
src/tests/frontend/specs/language.js
Normal file
125
src/tests/frontend/specs/language.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
52
src/tests/frontend/specs/multiple_authors_clear_authorship_colors.js
Executable file
52
src/tests/frontend/specs/multiple_authors_clear_authorship_colors.js
Executable 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();
|
||||
};
|
||||
});
|
187
src/tests/frontend/specs/ordered_list.js
Normal file
187
src/tests/frontend/specs/ordered_list.js
Normal 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);
|
||||
});
|
||||
});
|
131
src/tests/frontend/specs/pad_modal.js
Normal file
131
src/tests/frontend/specs/pad_modal.js
Normal 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');
|
||||
};
|
||||
});
|
64
src/tests/frontend/specs/redo.js
Normal file
64
src/tests/frontend/specs/redo.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
91
src/tests/frontend/specs/responsiveness.js
Normal file
91
src/tests/frontend/specs/responsiveness.js
Normal 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);
|
||||
});
|
||||
});
|
43
src/tests/frontend/specs/scrollTo.js
Executable file
43
src/tests/frontend/specs/scrollTo.js
Executable 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.
|
||||
});
|
||||
});
|
||||
});
|
161
src/tests/frontend/specs/select_formatting_buttons.js
Normal file
161
src/tests/frontend/specs/select_formatting_buttons.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
38
src/tests/frontend/specs/strikethrough.js
Normal file
38
src/tests/frontend/specs/strikethrough.js
Normal 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();
|
||||
});
|
||||
});
|
43
src/tests/frontend/specs/timeslider.js
Normal file
43
src/tests/frontend/specs/timeslider.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
100
src/tests/frontend/specs/timeslider_follow.js
Normal file
100
src/tests/frontend/specs/timeslider_follow.js
Normal 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;
|
||||
};
|
64
src/tests/frontend/specs/timeslider_labels.js
Normal file
64
src/tests/frontend/specs/timeslider_labels.js
Normal 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');
|
||||
});
|
||||
});
|
31
src/tests/frontend/specs/timeslider_numeric_padID.js
Normal file
31
src/tests/frontend/specs/timeslider_numeric_padID.js
Normal 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');
|
||||
});
|
||||
});
|
184
src/tests/frontend/specs/timeslider_revisions.js
Normal file
184
src/tests/frontend/specs/timeslider_revisions.js
Normal 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);
|
||||
});
|
||||
});
|
55
src/tests/frontend/specs/undo.js
Normal file
55
src/tests/frontend/specs/undo.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
165
src/tests/frontend/specs/unordered_list.js
Normal file
165
src/tests/frontend/specs/unordered_list.js
Normal 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);
|
||||
});
|
||||
});
|
76
src/tests/frontend/specs/urls_become_clickable.js
Normal file
76
src/tests/frontend/specs/urls_become_clickable.js
Normal 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);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
69
src/tests/frontend/specs/xxauto_reconnect.js
Normal file
69
src/tests/frontend/specs/xxauto_reconnect.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
2
src/tests/frontend/travis/.gitignore
vendored
Normal file
2
src/tests/frontend/travis/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
sauce_connect.log
|
||||
sauce_connect.log.*
|
183
src/tests/frontend/travis/remote_runner.js
Normal file
183
src/tests/frontend/travis/remote_runner.js
Normal file
|
@ -0,0 +1,183 @@
|
|||
'use strict';
|
||||
|
||||
const async = require('async');
|
||||
const wd = require('wd');
|
||||
|
||||
const config = {
|
||||
host: 'ondemand.saucelabs.com',
|
||||
port: 80,
|
||||
username: process.env.SAUCE_USER,
|
||||
accessKey: process.env.SAUCE_ACCESS_KEY,
|
||||
};
|
||||
|
||||
let allTestsPassed = true;
|
||||
// overwrite the default exit code
|
||||
// in case not all worker can be run (due to saucelabs limits),
|
||||
// `queue.drain` below will not be called
|
||||
// and the script would silently exit with error code 0
|
||||
process.exitCode = 2;
|
||||
process.on('exit', (code) => {
|
||||
if (code === 2) {
|
||||
console.log('\x1B[31mFAILED\x1B[39m Not all saucelabs runner have been started.');
|
||||
}
|
||||
});
|
||||
|
||||
const sauceTestWorker = async.queue((testSettings, callback) => {
|
||||
const browser = wd.promiseChainRemote(
|
||||
config.host, config.port, config.username, config.accessKey);
|
||||
const name =
|
||||
`${process.env.GIT_HASH} - ${testSettings.browserName} ` +
|
||||
`${testSettings.version}, ${testSettings.platform}`;
|
||||
testSettings.name = name;
|
||||
testSettings.public = true;
|
||||
testSettings.build = process.env.GIT_HASH;
|
||||
// console.json can be downloaded via saucelabs,
|
||||
// don't know how to print them into output of the tests
|
||||
testSettings.extendedDebugging = true;
|
||||
testSettings.tunnelIdentifier = process.env.TRAVIS_JOB_NUMBER;
|
||||
|
||||
browser.init(testSettings).get('http://localhost:9001/tests/frontend/', () => {
|
||||
const url = `https://saucelabs.com/jobs/${browser.sessionID}`;
|
||||
console.log(`Remote sauce test '${name}' started! ${url}`);
|
||||
|
||||
// tear down the test excecution
|
||||
const stopSauce = (success, timesup) => {
|
||||
clearInterval(getStatusInterval);
|
||||
clearTimeout(timeout);
|
||||
|
||||
browser.quit(() => {
|
||||
if (!success) {
|
||||
allTestsPassed = false;
|
||||
}
|
||||
|
||||
// if stopSauce is called via timeout
|
||||
// (in contrast to via getStatusInterval) than the log of up to the last
|
||||
// five seconds may not be available here. It's an error anyway, so don't care about it.
|
||||
printLog(logIndex);
|
||||
|
||||
if (timesup) {
|
||||
console.log(`[${testSettings.browserName} ${testSettings.platform}` +
|
||||
`${testSettings.version === '' ? '' : (` ${testSettings.version}`)}]` +
|
||||
' \x1B[31mFAILED\x1B[39m allowed test duration exceeded');
|
||||
}
|
||||
console.log(`Remote sauce test '${name}' finished! ${url}`);
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* timeout if a test hangs or the job exceeds 14.5 minutes
|
||||
* It's necessary because if travis kills the saucelabs session due to inactivity,
|
||||
* we don't get any output
|
||||
* @todo this should be configured in testSettings, see
|
||||
* https://wiki.saucelabs.com/display/DOCS/Test+Configuration+Options#TestConfigurationOptions-Timeouts
|
||||
*/
|
||||
const timeout = setTimeout(() => {
|
||||
stopSauce(false, true);
|
||||
}, 870000); // travis timeout is 15 minutes, set this to a slightly lower value
|
||||
|
||||
let knownConsoleText = '';
|
||||
// how many characters of the log have been sent to travis
|
||||
let logIndex = 0;
|
||||
const getStatusInterval = setInterval(() => {
|
||||
browser.eval("$('#console').text()", (err, consoleText) => {
|
||||
if (!consoleText || err) {
|
||||
return;
|
||||
}
|
||||
knownConsoleText = consoleText;
|
||||
|
||||
if (knownConsoleText.indexOf('FINISHED') > 0) {
|
||||
const match = knownConsoleText.match(
|
||||
/FINISHED.*([0-9]+) tests passed, ([0-9]+) tests failed/);
|
||||
// finished without failures
|
||||
if (match[2] && match[2] === '0') {
|
||||
stopSauce(true);
|
||||
|
||||
// finished but some tests did not return or some tests failed
|
||||
} else {
|
||||
stopSauce(false);
|
||||
}
|
||||
} else {
|
||||
// not finished yet
|
||||
printLog(logIndex);
|
||||
logIndex = knownConsoleText.length;
|
||||
}
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
/**
|
||||
* Replaces color codes in the test runners log, appends
|
||||
* browser name, platform etc. to every line and prints them.
|
||||
*
|
||||
* @param {number} index offset from where to start
|
||||
*/
|
||||
const printLog = (index) => {
|
||||
let testResult = knownConsoleText.substring(index)
|
||||
.replace(/\[red\]/g, '\x1B[31m').replace(/\[yellow\]/g, '\x1B[33m')
|
||||
.replace(/\[green\]/g, '\x1B[32m').replace(/\[clear\]/g, '\x1B[39m');
|
||||
testResult = testResult.split('\\n').map((line) => `[${testSettings.browserName} ` +
|
||||
`${testSettings.platform}` +
|
||||
`${testSettings.version === '' ? '' : (` ${testSettings.version}`)}]` +
|
||||
`${line}`).join('\n');
|
||||
|
||||
console.log(testResult);
|
||||
};
|
||||
});
|
||||
}, 6); // run 6 tests in parrallel
|
||||
|
||||
// 1) Firefox on Linux
|
||||
sauceTestWorker.push({
|
||||
platform: 'Windows 7',
|
||||
browserName: 'firefox',
|
||||
version: '52.0',
|
||||
});
|
||||
|
||||
// 2) Chrome on Linux
|
||||
sauceTestWorker.push({
|
||||
platform: 'Windows 7',
|
||||
browserName: 'chrome',
|
||||
version: '55.0',
|
||||
args: ['--use-fake-device-for-media-stream'],
|
||||
});
|
||||
|
||||
/*
|
||||
// 3) Safari on OSX 10.15
|
||||
sauceTestWorker.push({
|
||||
'platform' : 'OS X 10.15'
|
||||
, 'browserName' : 'safari'
|
||||
, 'version' : '13.1'
|
||||
});
|
||||
*/
|
||||
|
||||
// 4) Safari on OSX 10.14
|
||||
sauceTestWorker.push({
|
||||
platform: 'OS X 10.15',
|
||||
browserName: 'safari',
|
||||
version: '13.1',
|
||||
});
|
||||
// IE 10 doesn't appear to be working anyway
|
||||
/*
|
||||
// 4) IE 10 on Win 8
|
||||
sauceTestWorker.push({
|
||||
'platform' : 'Windows 8'
|
||||
, 'browserName' : 'iexplore'
|
||||
, 'version' : '10.0'
|
||||
});
|
||||
*/
|
||||
// 5) Edge on Win 10
|
||||
sauceTestWorker.push({
|
||||
platform: 'Windows 10',
|
||||
browserName: 'microsoftedge',
|
||||
version: '83.0',
|
||||
});
|
||||
// 6) Firefox on Win 7
|
||||
sauceTestWorker.push({
|
||||
platform: 'Windows 7',
|
||||
browserName: 'firefox',
|
||||
version: '78.0',
|
||||
});
|
||||
|
||||
sauceTestWorker.drain(() => {
|
||||
process.exit(allTestsPassed ? 0 : 1);
|
||||
});
|
45
src/tests/frontend/travis/runner.sh
Executable file
45
src/tests/frontend/travis/runner.sh
Executable file
|
@ -0,0 +1,45 @@
|
|||
#!/bin/sh
|
||||
|
||||
pecho() { printf %s\\n "$*"; }
|
||||
log() { pecho "$@"; }
|
||||
error() { log "ERROR: $@" >&2; }
|
||||
fatal() { error "$@"; exit 1; }
|
||||
try() { "$@" || fatal "'$@' failed"; }
|
||||
|
||||
[ -n "${SAUCE_USERNAME}" ] || fatal "SAUCE_USERNAME is unset - exiting"
|
||||
[ -n "${SAUCE_ACCESS_KEY}" ] || fatal "SAUCE_ACCESS_KEY is unset - exiting"
|
||||
|
||||
MY_DIR=$(try cd "${0%/*}" && try pwd) || exit 1
|
||||
|
||||
# reliably move to the etherpad base folder before running it
|
||||
try cd "${MY_DIR}/../../../"
|
||||
|
||||
log "Assuming bin/installDeps.sh has already been run"
|
||||
node node_modules/ep_etherpad-lite/node/server.js --experimental-worker "${@}" &
|
||||
ep_pid=$!
|
||||
|
||||
log "Waiting for Etherpad to accept connections (http://localhost:9001)..."
|
||||
connected=false
|
||||
can_connect() {
|
||||
curl -sSfo /dev/null http://localhost:9001/ || return 1
|
||||
connected=true
|
||||
}
|
||||
now() { date +%s; }
|
||||
start=$(now)
|
||||
while [ $(($(now) - $start)) -le 15 ] && ! can_connect; do
|
||||
sleep 1
|
||||
done
|
||||
[ "$connected" = true ] \
|
||||
|| fatal "Timed out waiting for Etherpad to accept connections"
|
||||
log "Successfully connected to Etherpad on http://localhost:9001"
|
||||
|
||||
# start the remote runner
|
||||
try cd "${MY_DIR}"
|
||||
log "Starting the remote runner..."
|
||||
node remote_runner.js
|
||||
exit_code=$?
|
||||
|
||||
kill "$(cat /tmp/sauce.pid)"
|
||||
kill "$ep_pid" && wait "$ep_pid"
|
||||
log "Done."
|
||||
exit "$exit_code"
|
49
src/tests/frontend/travis/runnerBackend.sh
Executable file
49
src/tests/frontend/travis/runnerBackend.sh
Executable file
|
@ -0,0 +1,49 @@
|
|||
#!/bin/sh
|
||||
|
||||
pecho() { printf %s\\n "$*"; }
|
||||
log() { pecho "$@"; }
|
||||
error() { log "ERROR: $@" >&2; }
|
||||
fatal() { error "$@"; exit 1; }
|
||||
try() { "$@" || fatal "'$@' failed"; }
|
||||
|
||||
MY_DIR=$(try cd "${0%/*}" && try pwd) || fatal "failed to find script directory"
|
||||
|
||||
# reliably move to the etherpad base folder before running it
|
||||
try cd "${MY_DIR}/../../../"
|
||||
|
||||
try sed -e '
|
||||
s!"soffice":[^,]*!"soffice": "/usr/bin/soffice"!
|
||||
# Reduce rate limit aggressiveness
|
||||
s!"max":[^,]*!"max": 100!
|
||||
s!"points":[^,]*!"points": 1000!
|
||||
# GitHub does not like our output
|
||||
s!"loglevel":[^,]*!"loglevel": "WARN"!
|
||||
' settings.json.template >settings.json
|
||||
|
||||
log "Assuming bin/installDeps.sh has already been run"
|
||||
node node_modules/ep_etherpad-lite/node/server.js "${@}" &
|
||||
ep_pid=$!
|
||||
|
||||
log "Waiting for Etherpad to accept connections (http://localhost:9001)..."
|
||||
connected=false
|
||||
can_connect() {
|
||||
curl -sSfo /dev/null http://localhost:9001/ || return 1
|
||||
connected=true
|
||||
}
|
||||
now() { date +%s; }
|
||||
start=$(now)
|
||||
while [ $(($(now) - $start)) -le 15 ] && ! can_connect; do
|
||||
sleep 1
|
||||
done
|
||||
[ "$connected" = true ] \
|
||||
|| fatal "Timed out waiting for Etherpad to accept connections"
|
||||
log "Successfully connected to Etherpad on http://localhost:9001"
|
||||
|
||||
log "Running the backend tests..."
|
||||
try cd src
|
||||
npm test
|
||||
exit_code=$?
|
||||
|
||||
kill "$ep_pid" && wait "$ep_pid"
|
||||
log "Done."
|
||||
exit "$exit_code"
|
51
src/tests/frontend/travis/runnerLoadTest.sh
Executable file
51
src/tests/frontend/travis/runnerLoadTest.sh
Executable file
|
@ -0,0 +1,51 @@
|
|||
#!/bin/sh
|
||||
|
||||
pecho() { printf %s\\n "$*"; }
|
||||
log() { pecho "$@"; }
|
||||
error() { log "ERROR: $@" >&2; }
|
||||
fatal() { error "$@"; exit 1; }
|
||||
try() { "$@" || fatal "'$@' failed"; }
|
||||
|
||||
MY_DIR=$(try cd "${0%/*}" && try pwd) || exit 1
|
||||
|
||||
# reliably move to the etherpad base folder before running it
|
||||
try cd "${MY_DIR}/../../../"
|
||||
|
||||
try sed -e '
|
||||
s!"loadTest":[^,]*!"loadTest": true!
|
||||
# Reduce rate limit aggressiveness
|
||||
s!"points":[^,]*!"points": 1000!
|
||||
' settings.json.template >settings.json
|
||||
|
||||
log "Assuming bin/installDeps.sh has already been run"
|
||||
node node_modules/ep_etherpad-lite/node/server.js "${@}" >/dev/null &
|
||||
ep_pid=$!
|
||||
|
||||
log "Waiting for Etherpad to accept connections (http://localhost:9001)..."
|
||||
connected=false
|
||||
can_connect() {
|
||||
curl -sSfo /dev/null http://localhost:9001/ || return 1
|
||||
connected=true
|
||||
}
|
||||
now() { date +%s; }
|
||||
start=$(now)
|
||||
while [ $(($(now) - $start)) -le 15 ] && ! can_connect; do
|
||||
sleep 1
|
||||
done
|
||||
[ "$connected" = true ] \
|
||||
|| fatal "Timed out waiting for Etherpad to accept connections"
|
||||
log "Successfully connected to Etherpad on http://localhost:9001"
|
||||
|
||||
# Build the minified files
|
||||
try curl http://localhost:9001/p/minifyme -f -s >/dev/null
|
||||
|
||||
# just in case, let's wait for another 10 seconds before going on
|
||||
sleep 10
|
||||
|
||||
log "Running the load tests..."
|
||||
etherpad-loadtest -d 25
|
||||
exit_code=$?
|
||||
|
||||
kill "$ep_pid" && wait "$ep_pid"
|
||||
log "Done."
|
||||
exit "$exit_code"
|
37
src/tests/frontend/travis/sauce_tunnel.sh
Executable file
37
src/tests/frontend/travis/sauce_tunnel.sh
Executable file
|
@ -0,0 +1,37 @@
|
|||
#!/bin/sh
|
||||
|
||||
pecho() { printf %s\\n "$*"; }
|
||||
log() { pecho "$@"; }
|
||||
error() { log "ERROR: $@" >&2; }
|
||||
fatal() { error "$@"; exit 1; }
|
||||
try() { "$@" || fatal "'$@' failed"; }
|
||||
|
||||
[ -n "${SAUCE_USERNAME}" ] || fatal "SAUCE_USERNAME is unset - exiting"
|
||||
[ -n "${SAUCE_ACCESS_KEY}" ] || fatal "SAUCE_ACCESS_KEY is unset - exiting"
|
||||
|
||||
# download and unzip the sauce connector
|
||||
#
|
||||
# ACHTUNG: as of 2019-12-21, downloading sc-latest-linux.tar.gz does not work.
|
||||
# It is necessary to explicitly download a specific version, for example
|
||||
# https://saucelabs.com/downloads/sc-4.5.4-linux.tar.gz Supported versions are
|
||||
# currently listed at:
|
||||
# https://wiki.saucelabs.com/display/DOCS/Downloading+Sauce+Connect+Proxy
|
||||
try curl -o /tmp/sauce.tar.gz \
|
||||
https://saucelabs.com/downloads/sc-4.6.2-linux.tar.gz
|
||||
try tar zxf /tmp/sauce.tar.gz --directory /tmp
|
||||
try mv /tmp/sc-*-linux /tmp/sauce_connect
|
||||
|
||||
# start the sauce connector in background and make sure it doesn't output the
|
||||
# secret key
|
||||
try rm -f /tmp/tunnel
|
||||
/tmp/sauce_connect/bin/sc \
|
||||
--user "${SAUCE_USERNAME}" \
|
||||
--key "${SAUCE_ACCESS_KEY}" \
|
||||
-i "${TRAVIS_JOB_NUMBER}" \
|
||||
--pidfile /tmp/sauce.pid \
|
||||
--readyfile /tmp/tunnel >/dev/null &
|
||||
|
||||
# wait for the tunnel to build up
|
||||
while ! [ -e "/tmp/tunnel" ]; do
|
||||
sleep 1
|
||||
done
|
Loading…
Add table
Add a link
Reference in a new issue