tests: Add regression tests for character composition race
See: https://github.com/ether/etherpad-lite/issues/4978pull/4986/head
parent
1fdaf95c3b
commit
f2034ad368
|
@ -0,0 +1,147 @@
|
|||
'use strict';
|
||||
|
||||
helper.multipleUsers = {
|
||||
thisUser: null,
|
||||
otherUser: null,
|
||||
|
||||
// open the same pad on different frames (allows concurrent editions to pad)
|
||||
async init() {
|
||||
// do some cleanup, in case any of the tests failed on the previous run
|
||||
const currentToken = _createTokenForCurrentUser();
|
||||
const otherToken = _createTokenForAnotherUser();
|
||||
_removeExistingTokensFromCookie();
|
||||
|
||||
this.thisUser = {
|
||||
$frame: $('#iframe-container iframe'),
|
||||
token: currentToken,
|
||||
// we'll switch between pads, need to store current values of helper.pad*
|
||||
// to be able to restore those values later
|
||||
padChrome$: helper.padChrome$,
|
||||
padOuter$: helper.padOuter$,
|
||||
padInner$: helper.padInner$,
|
||||
};
|
||||
|
||||
this.otherUser = {
|
||||
token: otherToken,
|
||||
};
|
||||
|
||||
// need to perform as the other user, otherwise we'll get the userdup error message
|
||||
await this.performAsOtherUser(this._createFrameForOtherUser.bind(this));
|
||||
},
|
||||
|
||||
async performAsOtherUser(action) {
|
||||
_startActingLike(this.otherUser);
|
||||
await action();
|
||||
// go back to initial state when we're done
|
||||
_startActingLike(this.thisUser);
|
||||
},
|
||||
|
||||
close() {
|
||||
this.thisUser.$frame.attr('style', ''); // make the default ocopy the full height
|
||||
this.otherUser.$frame.remove();
|
||||
},
|
||||
|
||||
async _loadJQueryCodeForOtherFrame() {
|
||||
const code = await $.get('/static/js/jquery.js');
|
||||
|
||||
// make sure we don't override existing jquery
|
||||
const jQueryCode = `if(typeof $ === "undefined") {\n${code}\n}`;
|
||||
const sendkeysCode = await $.get('/tests/frontend/lib/sendkeys.js');
|
||||
const codesToLoad = [jQueryCode, sendkeysCode];
|
||||
|
||||
this.otherUser.padChrome$ = _getFrameJQuery(codesToLoad, this.otherUser.$frame);
|
||||
this.otherUser.padOuter$ =
|
||||
_getFrameJQuery(codesToLoad, this.otherUser.padChrome$('iframe[name="ace_outer"]'));
|
||||
this.otherUser.padInner$ =
|
||||
_getFrameJQuery(codesToLoad, this.otherUser.padOuter$('iframe[name="ace_inner"]'));
|
||||
|
||||
// update helper vars now that they are available
|
||||
helper.padChrome$ = this.otherUser.padChrome$;
|
||||
helper.padOuter$ = this.otherUser.padOuter$;
|
||||
helper.padInner$ = this.otherUser.padInner$;
|
||||
},
|
||||
|
||||
async _createFrameForOtherUser() {
|
||||
// create the iframe
|
||||
const padUrl = this.thisUser.$frame.attr('src');
|
||||
this.otherUser.$frame = $(`<iframe id="other_pad" src="${padUrl}"></iframe>`);
|
||||
|
||||
// place one iframe (visually) below the other
|
||||
this.thisUser.$frame.attr('style', 'height: 50%');
|
||||
this.otherUser.$frame.attr('style', 'height: 50%; top: 50%');
|
||||
this.otherUser.$frame.insertAfter(this.thisUser.$frame);
|
||||
|
||||
// wait for other pad to load
|
||||
await new Promise((resolve) => this.otherUser.$frame.one('load', resolve));
|
||||
|
||||
const $editorLoadingMessage = this.otherUser.$frame.contents().find('#editorloadingbox');
|
||||
const $errorMessageModal = this.thisUser.$frame.contents().find('#connectivity .userdup');
|
||||
|
||||
await helper.waitForPromise(() => {
|
||||
const finishedLoadingOtherFrame = !$editorLoadingMessage.is(':visible');
|
||||
// make sure we don't get the userdup by mistake
|
||||
const didNotDetectUserDup = !$errorMessageModal.is(':visible');
|
||||
|
||||
return finishedLoadingOtherFrame && didNotDetectUserDup;
|
||||
}, 50000);
|
||||
|
||||
// need to get values for this.otherUser.pad* vars
|
||||
await this._loadJQueryCodeForOtherFrame();
|
||||
},
|
||||
};
|
||||
|
||||
// adapted form helper.js on Etherpad code
|
||||
const _getFrameJQuery = (codesToLoad, $iframe) => {
|
||||
const win = $iframe[0].contentWindow;
|
||||
const doc = win.document;
|
||||
|
||||
for (let i = 0; i < codesToLoad.length; i++) {
|
||||
win.eval(codesToLoad[i]);
|
||||
}
|
||||
|
||||
win.$.window = win;
|
||||
win.$.document = doc;
|
||||
|
||||
return win.$;
|
||||
};
|
||||
|
||||
const _getDocumentWithCookie = () => (
|
||||
helper.padChrome$
|
||||
? helper.padChrome$.document
|
||||
: helper.multipleUsers.thisUser.$frame.get(0).contentDocument
|
||||
);
|
||||
|
||||
const _setTokenOnCookie = (token) => {
|
||||
_getDocumentWithCookie().cookie = `token=${token};secure`;
|
||||
};
|
||||
|
||||
const _getTokenFromCookie = () => {
|
||||
const fullCookie = _getDocumentWithCookie().cookie;
|
||||
return fullCookie.replace(/.*token=([^;]*).*/, '$1').trim();
|
||||
};
|
||||
|
||||
const _createTokenForCurrentUser = () => (
|
||||
_getTokenFromCookie().replace(/-other_user.*/g, '')
|
||||
);
|
||||
|
||||
const _createTokenForAnotherUser = () => {
|
||||
const currentToken = _createTokenForCurrentUser();
|
||||
return `${currentToken}-other_user${helper.randomString(4)}`;
|
||||
};
|
||||
|
||||
const _startActingLike = (user) => {
|
||||
// update helper references, so other methods will act as if the main frame
|
||||
// was the one we're using from now on
|
||||
helper.padChrome$ = user.padChrome$;
|
||||
helper.padOuter$ = user.padOuter$;
|
||||
helper.padInner$ = user.padInner$;
|
||||
|
||||
_setTokenOnCookie(user.token);
|
||||
};
|
||||
|
||||
const _removeExistingTokensFromCookie = () => {
|
||||
// Expire cookie, to make sure it is removed by the browser.
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie#Example_4_Reset_the_previous_cookie
|
||||
_getDocumentWithCookie().cookie = 'token=foo;expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/p';
|
||||
_getDocumentWithCookie().cookie = 'token=foo;expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
|
||||
};
|
|
@ -20,6 +20,7 @@
|
|||
<script src="helper.js"></script>
|
||||
<script src="helper/methods.js"></script>
|
||||
<script src="helper/ui.js"></script>
|
||||
<script src="helper/multipleUsers.js"></script>
|
||||
|
||||
<script src="specs_list.js"></script>
|
||||
<script src="runner.js"></script>
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
'use strict';
|
||||
|
||||
describe('Messages in the COLLABROOM', function () {
|
||||
const user1Text = 'text created by user 1';
|
||||
const user2Text = 'text created by user 2';
|
||||
|
||||
const triggerEvent = (eventName) => {
|
||||
const event = new helper.padInner$.Event(eventName);
|
||||
helper.padInner$('#innerdocbody').trigger(event);
|
||||
};
|
||||
|
||||
const replaceLineText = async (lineNumber, newText) => {
|
||||
const inner$ = helper.padInner$;
|
||||
|
||||
// get the line element
|
||||
const $line = inner$('div').eq(lineNumber);
|
||||
|
||||
// simulate key presses to delete content
|
||||
$line.sendkeys('{selectall}'); // select all
|
||||
$line.sendkeys('{del}'); // clear the first line
|
||||
$line.sendkeys(newText); // insert the string
|
||||
|
||||
await helper.waitForPromise(() => inner$('div').eq(lineNumber).text() === newText);
|
||||
};
|
||||
|
||||
before(async function () {
|
||||
this.timeout(10000);
|
||||
await helper.aNewPad();
|
||||
await helper.multipleUsers.init();
|
||||
});
|
||||
|
||||
it('bug #4978 regression test', async function () {
|
||||
// The bug was triggered by receiving a change from another user while simultaneously composing
|
||||
// a character and waiting for an acknowledgement of a previously sent change.
|
||||
|
||||
// User 1 starts sending a change to the server.
|
||||
let sendStarted;
|
||||
const finishSend = (() => {
|
||||
const socketJsonObj = helper.padChrome$.window.pad.socket.json;
|
||||
const sendBackup = socketJsonObj.send;
|
||||
let startSend;
|
||||
sendStarted = new Promise((resolve) => { startSend = resolve; });
|
||||
let finishSend;
|
||||
const sendP = new Promise((resolve) => { finishSend = resolve; });
|
||||
socketJsonObj.send = (...args) => {
|
||||
startSend();
|
||||
sendP.then(() => {
|
||||
socketJsonObj.send = sendBackup;
|
||||
socketJsonObj.send(...args);
|
||||
});
|
||||
};
|
||||
return finishSend;
|
||||
})();
|
||||
await replaceLineText(0, user1Text);
|
||||
await sendStarted;
|
||||
|
||||
// User 1 starts a character composition.
|
||||
triggerEvent('compositionstart');
|
||||
|
||||
// User 1 receives a change from user 2. (User 1 will not incorporate the change until the
|
||||
// composition is completed.)
|
||||
const user2ChangeArrivedAtUser1 = new Promise((resolve) => {
|
||||
const cc = helper.padChrome$.window.pad.collabClient;
|
||||
const origHM = cc.handleMessageFromServer;
|
||||
cc.handleMessageFromServer = (evt) => {
|
||||
if (evt.type === 'COLLABROOM' && evt.data.type === 'NEW_CHANGES') {
|
||||
cc.handleMessageFromServer = origHM;
|
||||
resolve();
|
||||
}
|
||||
return origHM.call(cc, evt);
|
||||
};
|
||||
});
|
||||
await helper.multipleUsers.performAsOtherUser(async () => await replaceLineText(1, user2Text));
|
||||
await user2ChangeArrivedAtUser1;
|
||||
|
||||
// User 1 finishes sending the change to the server. User 2 should see the changes right away.
|
||||
finishSend();
|
||||
await helper.multipleUsers.performAsOtherUser(async () => await helper.waitForPromise(
|
||||
() => helper.padInner$('div').eq(0).text() === user1Text));
|
||||
|
||||
// User 1 finishes the character composition. User 2's change should then become visible.
|
||||
triggerEvent('compositionend');
|
||||
await helper.waitForPromise(() => helper.padInner$('div').eq(1).text() === user2Text);
|
||||
|
||||
// Users 1 and 2 make some more changes.
|
||||
await helper.multipleUsers.performAsOtherUser(async () => await replaceLineText(3, user2Text));
|
||||
await replaceLineText(2, user1Text);
|
||||
|
||||
// All changes should appear in both views.
|
||||
const assertContent = async () => await helper.waitForPromise(() => {
|
||||
const expectedLines = [
|
||||
user1Text,
|
||||
user2Text,
|
||||
user1Text,
|
||||
user2Text,
|
||||
];
|
||||
return expectedLines.every((txt, i) => helper.padInner$('div').eq(i).text() === txt);
|
||||
});
|
||||
await assertContent();
|
||||
await helper.multipleUsers.performAsOtherUser(assertContent);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue