etherpad-lite/src/tests/frontend/helper.js

324 lines
10 KiB
JavaScript
Raw Normal View History

'use strict';
const helper = {};
(() => {
let $iframe;
const jsLibraries = {};
2021-05-10 00:45:43 +02:00
helper.init = async () => {
[
jsLibraries.jquery,
jsLibraries.sendkeys,
] = await Promise.all([
$.get('../../static/js/vendors/jquery.js'),
$.get('lib/sendkeys.js'),
]);
// make sure we don't override existing jquery
jsLibraries.jquery = `if (typeof $ === 'undefined') {\n${jsLibraries.jquery}\n}`;
};
helper.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 = ($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;
2012-10-06 21:29:37 +02:00
// IE 8+9 Hack to make eval appear
// https://stackoverflow.com/q/2720444
win.execScript && win.execScript('null');
2012-10-06 21:29:37 +02:00
win.eval(jsLibraries.jquery);
win.eval(jsLibraries.sendkeys);
win.$.window = win;
win.$.document = doc;
2012-10-06 21:29:37 +02:00
return win.$;
};
2012-10-06 21:29:37 +02:00
helper.clearSessionCookies = () => {
window.Cookies.remove('token');
window.Cookies.remove('language');
};
2012-11-04 00:48:10 +01:00
// Can only happen when the iframe exists, so we're doing it separately from other cookies
helper.clearPadPrefCookie = () => {
const {padcookie} = helper.padChrome$.window.require('ep_etherpad-lite/static/js/pad_cookie');
padcookie.clear();
};
// Overwrite all prefs in pad cookie.
helper.setPadPrefCookie = (prefs) => {
const {padcookie} = helper.padChrome$.window.require('ep_etherpad-lite/static/js/pad_cookie');
padcookie.clear();
for (const [key, value] of Object.entries(prefs)) padcookie.setPref(key, value);
};
// 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;
// Deprecated; use helper.aNewPad() instead.
helper.newPad = (opts, id) => {
if (!id) id = `FRONTEND_TEST_${helper.randomString(20)}`;
opts = Object.assign({id}, typeof opts === 'function' ? {cb: opts} : opts);
const {cb = (err) => { if (err != null) throw err; }} = opts;
delete opts.cb;
helper.aNewPad(opts).then((id) => cb(null, id), (err) => cb(err || new Error(err)));
return id;
};
helper.aNewPad = async (opts = {}) => {
opts = Object.assign({
_retry: 0,
clearCookies: true,
id: `FRONTEND_TEST_${helper.randomString(20)}`,
}, opts);
2012-11-04 00:48:10 +01:00
// 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();
2012-11-04 00:48:10 +01:00
}
$iframe = $(`<iframe src='/p/${opts.id}${hash || ''}${encodedParams || ''}'></iframe>`);
// 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);
await new Promise((resolve) => $iframe.one('load', resolve));
helper.padChrome$ = getFrameJQuery($('#iframe-container iframe'));
helper.padChrome$.padeditor =
helper.padChrome$.window.require('ep_etherpad-lite/static/js/pad_editor').padeditor;
if (opts.clearCookies) {
helper.clearPadPrefCookie();
}
if (opts.padPrefs) {
helper.setPadPrefCookie(opts.padPrefs);
}
try {
await helper.waitForPromise(
() => !$iframe.contents().find('#editorloadingbox').is(':visible'), 10000);
} catch (err) {
if (opts._retry++ >= 4) throw new Error('Pad never loaded');
return await helper.aNewPad(opts);
}
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();
return opts.id;
};
helper.newAdmin = async (page) => {
// define the iframe
$iframe = $(`<iframe src='/admin/${page}'></iframe>`);
// clean up inner iframe references
helper.admin$ = null;
// remove old iframe
$('#iframe-container iframe').remove();
// set new iframe
$('#iframe-container').append($iframe);
$iframe.one('load', () => {
helper.admin$ = getFrameJQuery($('#iframe-container iframe'));
});
};
helper.waitFor = (conditionFunc, timeoutTime = 1900, intervalTime = 10) => {
// Create an Error object to use if the condition is never satisfied. This is created here so
// that the Error has a useful stack trace associated with it.
const timeoutError =
new Error(`waitFor condition never became true ${conditionFunc.toString()}`);
Low hanging lint frontend tests (#4695) * lint: low hanging specs/alphabet.js * lint: low hanging specs/authorship_of_editions.js * lint: low hanging specs/bold.js * lint: low hanging specs/caret.js * lint: low hanging specs/change_user_color.js * lint: low hanging specs/change_user_name.js * lint: low hanging specs/chat.js * lint: low hanging specs/chat_load_messages.js * lint: low hanging specs/clear_authorship_colors.js * lint: low hanging specs/delete.js * lint: low hanging specs/drag_and_drop.js * lint: low hanging specs/embed_value.js * lint: low hanging specs/enter.js * lint: low hanging specs/font_type.js * lint: low hanging specs/helper.js * lint: low hanging specs/importexport.js * lint: low hanging specs/importindents.js * lint: low hanging specs/indentation.js * lint: low hanging specs/italic.js * lint: low hanging specs/language.js * lint: low hanging specs/multiple_authors_clear_authorship_colors.js * lint: low hanging specs/ordered_list.js * lint: low hanging specs/pad_modal.js * lint: low hanging specs/redo.js * lint: low hanging specs/responsiveness.js * lint: low hanging specs/select_formatting_buttons.js * lint: low hanging specs/strikethrough.js * lint: low hanging specs/timeslider.js * lint: low hanging specs/timeslider_labels.js * lint: low hanging specs/timeslider_numeric_padID.js * lint: low hanging specs/timeslider_revisions.js * lint: low hanging specs/undo.js * lint: low hanging specs/unordered_list.js * lint: low hanging specs/xxauto_reconnect.js * lint: attempt to do remote_runner.js * lint: helper linting * lint: rate limit linting * use constructor for Event to make eslint happier * for squash: lint fix refinements * for squash: lint fix refinements Co-authored-by: Richard Hansen <rhansen@rhansen.org>
2021-02-01 21:23:14 +01:00
const deferred = new $.Deferred();
const _fail = deferred.fail.bind(deferred);
let listenForFail = false;
deferred.fail = (...args) => {
listenForFail = true;
return _fail(...args);
};
const check = async () => {
try {
if (!await conditionFunc()) return;
deferred.resolve();
} catch (err) {
deferred.reject(err);
2012-10-06 21:29:37 +02:00
}
clearInterval(intervalCheck);
clearTimeout(timeout);
};
const intervalCheck = setInterval(check, intervalTime);
2012-10-06 21:29:37 +02:00
const timeout = setTimeout(() => {
2012-10-06 21:29:37 +02:00
clearInterval(intervalCheck);
deferred.reject(timeoutError);
if (!listenForFail) {
throw timeoutError;
}
2012-10-06 21:29:37 +02:00
}, 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}
*
*/
// 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().
helper.waitForPromise = async (...args) => await helper.waitFor(...args).fail(() => {});
helper.selectLines = ($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);
};
// Temporarily reduces minimum time between commits and calls the provided function with a single
// argument: a function that immediately incorporates all pad edits (as opposed to waiting for the
// idle timer to fire).
helper.withFastCommit = async (fn) => {
const incorp = () => helper.padChrome$.padeditor.ace.callWithAce(
(ace) => ace.ace_inCallStackIfNecessary('helper.edit', () => ace.ace_fastIncorp()));
const cc = helper.padChrome$.window.pad.collabClient;
const {commitDelay} = cc;
cc.commitDelay = 0;
try {
return await fn(incorp);
} finally {
cc.commitDelay = commitDelay;
}
};
const getTextNodeAndOffsetOf = ($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 || (() => {});
})();