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

335 lines
11 KiB
JavaScript

'use strict';
const helper = {};
(() => {
let $iframe;
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;
};
helper.getFrameJQuery = async ($iframe, includeSendkeys = false) => {
const win = $iframe[0].contentWindow;
const doc = win.document;
const load = async (url) => {
const elem = doc.createElement('script');
elem.setAttribute('src', url);
const p = new Promise((resolve, reject) => {
const handler = (evt) => {
elem.removeEventListener('load', handler);
elem.removeEventListener('error', handler);
if (evt.type === 'error') return reject(new Error(`failed to load ${url}`));
resolve();
};
elem.addEventListener('load', handler);
elem.addEventListener('error', handler);
});
doc.head.appendChild(elem);
await p;
};
if (!win.$) await load('../../static/js/vendors/jquery.js');
// sendkeys.js depends on jQuery, so it cannot be loaded until jQuery has finished loading. (In
// other words, do not load both jQuery and sendkeys inside a Promise.all() call.)
if (!win.bililiteRange && includeSendkeys) await load('../tests/frontend/lib/sendkeys.js');
win.$.window = win;
win.$.document = doc;
return win.$;
};
helper.clearSessionCookies = () => {
window.Cookies.remove('token');
window.Cookies.remove('language');
};
// 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)}`,
hookFns: {},
}, opts);
// Set up socket.io spying as early as possible.
/** chat messages received */
helper.chatMessages = [];
/** changeset commits from the server */
helper.commits = [];
/** userInfo messages from the server */
helper.userInfos = [];
if (opts.hookFns._socketCreated == null) opts.hookFns._socketCreated = [];
opts.hookFns._socketCreated.unshift(() => helper.spyOnSocketIO());
// 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();
}
$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 Promise.all([
new Promise((resolve) => $iframe.one('load', resolve)),
// Install the hook functions as early as possible because some of them fire right away.
new Promise((resolve, reject) => {
if ($iframe[0].contentWindow._postPluginUpdateForTestingDone) {
return reject(new Error(
'failed to set _postPluginUpdateForTesting before it would have been called'));
}
$iframe[0].contentWindow._postPluginUpdateForTesting = () => {
const {hooks} =
$iframe[0].contentWindow.require('ep_etherpad-lite/static/js/pluginfw/plugin_defs');
for (const [hookName, hookFns] of Object.entries(opts.hookFns)) {
if (hooks[hookName] == null) hooks[hookName] = [];
hooks[hookName].push(
...hookFns.map((hookFn) => ({hook_name: hookName, hook_fn: hookFn})));
}
resolve();
};
}),
]);
helper.padChrome$ = await helper.getFrameJQuery($('#iframe-container iframe'), true);
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);
}
const $loading = helper.padChrome$('#editorloadingbox');
const $container = helper.padChrome$('#editorcontainer');
try {
await helper.waitForPromise(
() => !$loading.is(':visible') && $container.hasClass('initialized'), 10000);
} catch (err) {
if (opts._retry++ >= 4) throw new Error('Pad never loaded');
return await helper.aNewPad(opts);
}
helper.padOuter$ =
await helper.getFrameJQuery(helper.padChrome$('iframe[name="ace_outer"]'), false);
helper.padInner$ =
await helper.getFrameJQuery(helper.padOuter$('iframe[name="ace_inner"]'), true);
// disable all animations, this makes tests faster and easier
helper.padChrome$.fx.off = true;
helper.padOuter$.fx.off = true;
helper.padInner$.fx.off = true;
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', async () => {
helper.admin$ = await helper.getFrameJQuery($('#iframe-container iframe'), false);
});
};
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()}`);
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);
}
clearInterval(intervalCheck);
clearTimeout(timeout);
};
const intervalCheck = setInterval(check, intervalTime);
const timeout = setTimeout(() => {
clearInterval(intervalCheck);
deferred.reject(timeoutError);
if (!listenForFail) {
throw timeoutError;
}
}, 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 || (() => {});
})();