mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-04-22 08:26: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
|
@ -1,297 +0,0 @@
|
|||
'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 () {};
|
||||
})();
|
Loading…
Add table
Add a link
Reference in a new issue