test_wysiwyg_collaboration.js 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891
  1. /** @odoo-module **/
  2. import { patch, unpatch } from "@web/core/utils/patch";
  3. import {
  4. parseTextualSelection,
  5. setTestSelection,
  6. renderTextualSelection,
  7. patchEditorIframe,
  8. } from '@web_editor/js/editor/odoo-editor/test/utils';
  9. import { stripHistoryIds } from '@web_editor/js/backend/html_field';
  10. import Wysiwyg from 'web_editor.wysiwyg';
  11. import { Mutex } from '@web/core/utils/concurrency';
  12. function makeSpy() {
  13. const spy = function() {
  14. spy.callCount++;
  15. return this._super.apply(this, arguments);
  16. };
  17. spy.callCount = 0;
  18. return spy;
  19. }
  20. class PeerTest {
  21. constructor(infos) {
  22. this.peerId = infos.peerId;
  23. this.wysiwyg = infos.wysiwyg;
  24. this.iframe = infos.iframe;
  25. this.document = this.iframe.contentWindow.document;
  26. this.wrapper = infos.wrapper;
  27. this.pool = infos.pool;
  28. this.peers = infos.pool.peers;
  29. this._connections = new Set();
  30. this.onlineMutex = new Mutex();
  31. this.isOnline = true;
  32. }
  33. async startEditor() {
  34. this._started = this.wysiwyg.appendTo(this.wrapper);
  35. await this._started;
  36. if (this.initialParsedSelection) {
  37. setTestSelection(this.initialParsedSelection, this.document);
  38. this.wysiwyg.odooEditor._recordHistorySelection();
  39. } else {
  40. document.getSelection().removeAllRanges();
  41. }
  42. clearInterval(this.wysiwyg._collaborationInterval);
  43. return this._started;
  44. }
  45. async destroyEditor() {
  46. for (const peer of this._connections) {
  47. peer._connections.delete(this);
  48. }
  49. this.wysiwyg.destroy();
  50. }
  51. async focus() {
  52. await this.started;
  53. return this.wysiwyg._joinPeerToPeer();
  54. }
  55. async openDataChannel(peer) {
  56. this._connections.add(peer);
  57. peer._connections.add(this);
  58. const ptpFrom = this.wysiwyg.ptp;
  59. const ptpTo = peer.wysiwyg.ptp;
  60. ptpFrom.clientsInfos[peer.peerId] = {};
  61. ptpTo.clientsInfos[this.peerId] = {};
  62. // Simulate the rtc_data_channel_open on both peers.
  63. await this.wysiwyg.ptp.notifySelf('rtc_data_channel_open', {
  64. connectionClientId: peer.peerId,
  65. });
  66. await peer.wysiwyg.ptp.notifySelf('rtc_data_channel_open', {
  67. connectionClientId: this.peerId,
  68. });
  69. }
  70. async removeDataChannel(peer) {
  71. this._connections.delete(peer);
  72. peer._connections.delete(this);
  73. const ptpFrom = this.wysiwyg.ptp;
  74. const ptpTo = peer.wysiwyg.ptp;
  75. delete ptpFrom.clientsInfos[peer.peerId];
  76. delete ptpTo.clientsInfos[this.peerId];
  77. this.onlineMutex = new Mutex();
  78. this._onlineResolver = undefined;
  79. }
  80. makeStep(fn) {
  81. fn(this);
  82. }
  83. getValue() {
  84. this.wysiwyg.odooEditor.observerUnactive('PeerTest.getValue');
  85. renderTextualSelection(this.document);
  86. const html = this.wysiwyg.$editable[0].innerHTML;
  87. const selection = parseTextualSelection(this.wysiwyg.$editable[0]);
  88. if (selection) {
  89. setTestSelection(selection, this.document);
  90. }
  91. this.wysiwyg.odooEditor.observerActive('PeerTest.getValue');
  92. return stripHistoryIds(html);
  93. }
  94. writeToServer() {
  95. this.pool.lastRecordSaved = this.wysiwyg.getValue();
  96. const lastId = this.wysiwyg._getLastHistoryStepId(this.pool.lastRecordSaved);
  97. for (const peer of Object.values(this.peers)) {
  98. if (peer === this || !peer._started) continue;
  99. peer.onlineMutex.exec(() => {
  100. return peer.wysiwyg._onServerLastIdUpdate(String(lastId));
  101. });
  102. }
  103. }
  104. async setOnline() {
  105. this.isOnline = true;
  106. this._onlineResolver && this._onlineResolver();
  107. return this.onlineMutex.getUnlockedDef();
  108. }
  109. setOffline() {
  110. this.isOnline = false;
  111. if (this._onlineResolver) return;
  112. this.onlineMutex.exec(async () => {
  113. await new Promise((resolve) => {
  114. this._onlineResolver = () => {
  115. this._onlineResolver = null;
  116. resolve();
  117. }
  118. });
  119. });
  120. }
  121. }
  122. function insert(string) {
  123. return (peer) => {
  124. peer.wysiwyg.odooEditor.execCommand('insert', string);
  125. }
  126. }
  127. const initialValue = '<p data-last-history-steps="1">a[]</p>';
  128. class PeerPool {
  129. constructor(peers) {
  130. this.peers = {};
  131. }
  132. }
  133. async function createPeers(peers) {
  134. const pool = new PeerPool();
  135. let lastGeneratedId = 0;
  136. for (const peerId of peers) {
  137. const peerWysiwygWrapper = document.createElement('div');
  138. peerWysiwygWrapper.classList.add('peer_wysiwyg_wrapper');
  139. peerWysiwygWrapper.classList.add('client_' + peerId);
  140. const iframe = document.createElement('iframe');
  141. if (navigator.userAgent.toLowerCase().indexOf('firefox') > -1) {
  142. // Firefox reset the page without this hack.
  143. // With this hack, chrome does not render content.
  144. iframe.setAttribute('src', ' javascript:void(0);');
  145. }
  146. document.querySelector('#qunit-fixture').append(iframe);
  147. patchEditorIframe(iframe);
  148. iframe.contentDocument.body.append(peerWysiwygWrapper);
  149. iframe.contentWindow.$ = $;
  150. const fakeWysiwygParent = {
  151. _trigger_up: () => {},
  152. };
  153. const wysiwyg = new Wysiwyg(fakeWysiwygParent, {
  154. value: initialValue,
  155. collaborative: true,
  156. collaborationChannel: {
  157. collaborationFieldName: "fake_field",
  158. collaborationModelName: "fake.model",
  159. collaborationResId: 1
  160. },
  161. document: iframe.contentWindow.document,
  162. });
  163. patch(wysiwyg, 'web_editor', {
  164. _generateClientId() {
  165. return peerId;
  166. },
  167. // Hacky hook as we know this method is called after setting the value in the wysiwyg start and before sending the value to odooEditor.
  168. _getLastHistoryStepId() {
  169. pool.peers[peerId].initialParsedSelection = parseTextualSelection(wysiwyg.$editable[0]);
  170. return this._super(...arguments);
  171. },
  172. call: () => {},
  173. getSession: () => ({notification_type: true}),
  174. _rpc(params) {
  175. if (params.route === '/web_editor/get_ice_servers') {
  176. return [];
  177. } else if (params.route === '/web_editor/bus_broadcast') {
  178. const currentPeer = pool.peers[peerId];
  179. for (const peer of currentPeer._connections) {
  180. peer.wysiwyg.ptp.handleNotification(structuredClone(params.params.bus_data));
  181. }
  182. } else if (params.model === "res.users" && params.method === "search_read") {
  183. return [{ name: "admin" }];
  184. }
  185. },
  186. _getNewPtp() {
  187. const ptp = this._super(...arguments);
  188. patch(ptp, "web_editor_peer_to_peer", {
  189. removeClient(peerId) {
  190. this.notifySelf('ptp_remove', peerId);
  191. delete this.clientsInfos[peerId];
  192. },
  193. notifyAllClients(...args) {
  194. // This is not needed because the opening of the
  195. // dataChannel is done through `openDataChannel` and we
  196. // do not want to simulate the events that thrigger the
  197. // openning of the dataChannel.
  198. if (args[0] === 'ptp_join') {
  199. return;
  200. }
  201. this._super(...args);
  202. },
  203. _getPtpClients() {
  204. return pool.peers[peerId]._connections.map((peer) => {
  205. return { id: peer.peerId }
  206. });
  207. },
  208. async _channelNotify(peerId, transportPayload) {
  209. if (!pool.peers[peerId].isOnline) return;
  210. pool.peers[peerId].wysiwyg.ptp.handleNotification(structuredClone(transportPayload));
  211. },
  212. _createClient() {
  213. throw new Error('Should not be called.');
  214. },
  215. _addIceCandidate() {
  216. throw new Error('Should not be called.');
  217. },
  218. _recoverConnection() {
  219. throw new Error('Should not be called.');
  220. },
  221. _killPotentialZombie() {
  222. throw new Error('Should not be called.');
  223. },
  224. });
  225. return ptp;
  226. },
  227. _getCurrentRecord() {
  228. return {
  229. id: 1,
  230. fake_field: pool.lastRecordSaved,
  231. }
  232. },
  233. _getCollaborationClientAvatarUrl() {
  234. return '';
  235. },
  236. async startEdition() {
  237. await this._super(...arguments);
  238. patch(this.odooEditor, 'odooEditor', {
  239. _generateId() {
  240. // Ensure the id are deterministically gererated for
  241. // when we need to sort by them. (eg. in the
  242. // callaboration sorting of steps)
  243. lastGeneratedId++;
  244. return lastGeneratedId.toString();
  245. },
  246. });
  247. }
  248. });
  249. pool.peers[peerId] = new PeerTest({
  250. peerId,
  251. wysiwyg,
  252. iframe,
  253. wrapper: peerWysiwygWrapper,
  254. pool,
  255. });
  256. }
  257. return pool;
  258. }
  259. function removePeers(peers) {
  260. for (const peer of Object.values(peers)) {
  261. peer.wysiwyg.destroy();
  262. peer.wrapper.remove();
  263. }
  264. }
  265. QUnit.module('web_editor', {
  266. before() {
  267. patch(Wysiwyg, 'web_editor', {
  268. activeCollaborationChannelNames: {
  269. has: () => false,
  270. add: () => {},
  271. delete: () => {},
  272. }
  273. });
  274. },
  275. after() {
  276. unpatch(Wysiwyg, "web_editor");
  277. }
  278. }, () => {
  279. QUnit.module('Collaboration', {}, () => {
  280. /**
  281. * Detect stale when <already focused | not already focused>
  282. */
  283. QUnit.module('Focus', {}, () => {
  284. QUnit.test('Focused client should not receive step if no data channel is open', async (assert) => {
  285. assert.expect(3);
  286. const pool = await createPeers(['p1', 'p2', 'p3']);
  287. const peers = pool.peers;
  288. await peers.p1.startEditor();
  289. await peers.p2.startEditor();
  290. await peers.p3.startEditor();
  291. await peers.p1.focus();
  292. await peers.p2.focus();
  293. await peers.p1.makeStep(insert('b'));
  294. assert.equal(peers.p1.getValue(), `<p>ab[]</p>`, 'p1 should have the document changed');
  295. assert.equal(peers.p2.getValue(), `<p>a[]</p>`, 'p2 should not have the document changed');
  296. assert.equal(peers.p3.getValue(), `<p>a[]</p>`, 'p3 should not have the document changed');
  297. removePeers(peers);
  298. });
  299. QUnit.test('Focused client should receive step while unfocused should not (if the datachannel is open before the step)', async (assert) => {
  300. assert.expect(3);
  301. const pool = await createPeers(['p1', 'p2', 'p3']);
  302. const peers = pool.peers;
  303. await peers.p1.startEditor();
  304. await peers.p2.startEditor();
  305. await peers.p3.startEditor();
  306. await peers.p1.focus();
  307. await peers.p2.focus();
  308. await peers.p1.openDataChannel(peers.p2);
  309. await peers.p1.makeStep(insert('b'));
  310. assert.equal(peers.p1.getValue(), `<p>ab[]</p>`, 'p1 should have the same document as p2');
  311. assert.equal(peers.p2.getValue(), `[]<p>ab</p>`, 'p2 should have the same document as p1');
  312. assert.equal(peers.p3.getValue(), `<p>a[]</p>`, 'p3 should not have the document changed');
  313. removePeers(peers);
  314. });
  315. QUnit.test('Focused client should receive step while unfocused should not (if the datachannel is open after the step)', async (assert) => {
  316. assert.expect(3);
  317. const pool = await createPeers(['p1', 'p2', 'p3']);
  318. const peers = pool.peers;
  319. await peers.p1.startEditor();
  320. await peers.p2.startEditor();
  321. await peers.p3.startEditor();
  322. await peers.p1.focus();
  323. await peers.p2.focus();
  324. await peers.p1.makeStep(insert('b'));
  325. await peers.p1.openDataChannel(peers.p2);
  326. assert.equal(peers.p1.getValue(), `<p>ab[]</p>`, 'p1 should have the same document as p2');
  327. assert.equal(peers.p2.getValue(), `[]<p>ab</p>`, 'p2 should have the same document as p1');
  328. assert.equal(peers.p3.getValue(), `<p>a[]</p>`, 'p3 should not have the document changed because it has not focused');
  329. removePeers(peers);
  330. });
  331. });
  332. QUnit.module('Stale detection & recovery', {}, () => {
  333. QUnit.module('detect stale while unfocused', async () => {
  334. QUnit.test('should do nothing until focus', async (assert) => {
  335. assert.expect(10);
  336. const pool = await createPeers(['p1', 'p2', 'p3']);
  337. const peers = pool.peers;
  338. await peers.p1.startEditor();
  339. await peers.p2.startEditor();
  340. await peers.p3.startEditor();
  341. await peers.p1.focus();
  342. await peers.p2.focus();
  343. await peers.p1.openDataChannel(peers.p2);
  344. await peers.p1.makeStep(insert('b'));
  345. await peers.p1.writeToServer();
  346. assert.equal(peers.p1.wysiwyg._isDocumentStale, false, 'p1 should not have a stale document');
  347. assert.equal(peers.p1.getValue(), `<p>ab[]</p>`, 'p1 should have the same document as p2');
  348. assert.equal(peers.p2.wysiwyg._isDocumentStale, false, 'p2 should not have a stale document');
  349. assert.equal(peers.p2.getValue(), `[]<p>ab</p>`, 'p2 should have the same document as p1');
  350. assert.equal(peers.p3.wysiwyg._isDocumentStale, true, 'p3 should have a stale document');
  351. assert.equal(peers.p3.getValue(), `<p>a[]</p>`, 'p3 should not have the same document as p1');
  352. await peers.p3.focus();
  353. await peers.p1.openDataChannel(peers.p3);
  354. // This timeout is necessary for the selection to be set
  355. await new Promise(resolve => setTimeout(resolve));
  356. assert.equal(peers.p3.wysiwyg._isDocumentStale, false, 'p3 should not have a stale document');
  357. assert.equal(peers.p3.getValue(), `<p>[]ab</p>`, 'p3 should have the same document as p1');
  358. await peers.p1.makeStep(insert('c'));
  359. assert.equal(peers.p1.getValue(), `<p>abc[]</p>`, 'p1 should have the same document as p3');
  360. assert.equal(peers.p3.getValue(), `<p>[]abc</p>`, 'p3 should have the same document as p1');
  361. removePeers(peers);
  362. });
  363. });
  364. QUnit.module('detect stale while focused', async () => {
  365. QUnit.module('recover from missing steps', async () => {
  366. QUnit.test('should recover from missing steps', async (assert) => {
  367. assert.expect(18);
  368. const pool = await createPeers(['p1', 'p2', 'p3']);
  369. const peers = pool.peers;
  370. await peers.p1.startEditor();
  371. await peers.p2.startEditor();
  372. await peers.p3.startEditor();
  373. await peers.p1.focus();
  374. await peers.p2.focus();
  375. await peers.p3.focus();
  376. await peers.p1.openDataChannel(peers.p2);
  377. await peers.p1.openDataChannel(peers.p3);
  378. await peers.p2.openDataChannel(peers.p3);
  379. const p3Spies = {
  380. _recoverFromStaleDocument: makeSpy(),
  381. _resetFromServerAndResyncWithClients: makeSpy(),
  382. _processMissingSteps: makeSpy(),
  383. _applySnapshot: makeSpy(),
  384. };
  385. patch(peers.p3.wysiwyg, 'test', p3Spies);
  386. assert.equal(peers.p1.wysiwyg._historyShareId, peers.p2.wysiwyg._historyShareId, 'p1 and p2 should have the same _historyShareId');
  387. assert.equal(peers.p1.wysiwyg._historyShareId, peers.p3.wysiwyg._historyShareId, 'p1 and p3 should have the same _historyShareId');
  388. assert.equal(peers.p1.getValue(), `<p>a[]</p>`, 'p1 should have the same document as p2');
  389. assert.equal(peers.p2.getValue(), `[]<p>a</p>`, 'p2 should have the same document as p1');
  390. assert.equal(peers.p3.getValue(), `[]<p>a</p>`, 'p3 should have the same document as p1');
  391. await peers.p3.setOffline();
  392. await peers.p1.makeStep(insert('b'));
  393. assert.equal(peers.p1.getValue(), `<p>ab[]</p>`, 'p1 should have the same document as p2');
  394. assert.equal(peers.p2.getValue(), `<p>[]ab</p>`, 'p2 should have the same document as p1');
  395. assert.equal(peers.p3.getValue(), `<p>[]a</p>`, 'p3 should not have the same document as p1');
  396. await peers.p1.writeToServer();
  397. assert.equal(peers.p1.wysiwyg._isDocumentStale, false, 'p1 should not have a stale document');
  398. assert.equal(peers.p2.wysiwyg._isDocumentStale, false, 'p2 should not have a stale document');
  399. assert.equal(peers.p3.wysiwyg._isDocumentStale, false, 'p3 should not have a stale document');
  400. await peers.p3.setOnline();
  401. unpatch(peers.p3.wysiwyg, 'test');
  402. assert.equal(p3Spies._recoverFromStaleDocument.callCount, 1, 'p3 _recoverFromStaleDocument should have been called once');
  403. assert.equal(p3Spies._processMissingSteps.callCount, 1, 'p3 _processMissingSteps should have been called once');
  404. assert.equal(p3Spies._applySnapshot.callCount, 0, 'p3 _applySnapshot should not have been called');
  405. assert.equal(p3Spies._resetFromServerAndResyncWithClients.callCount, 0, 'p3 _resetFromServerAndResyncWithClients should not have been called');
  406. assert.equal(peers.p1.getValue(), `<p>ab[]</p>`, 'p1 should have the same document as p2');
  407. assert.equal(peers.p2.getValue(), `<p>[]ab</p>`, 'p2 should have the same document as p1');
  408. assert.equal(peers.p3.getValue(), `<p>[]ab</p>`, 'p3 should have the same document as p1');
  409. removePeers(peers);
  410. });
  411. });
  412. QUnit.module('recover from snapshot', async () => {
  413. QUnit.test('should wait for all peer to recover from snapshot', async (assert) => {
  414. assert.expect(19);
  415. const pool = await createPeers(['p1', 'p2', 'p3']);
  416. const peers = pool.peers;
  417. await peers.p1.startEditor();
  418. await peers.p2.startEditor();
  419. await peers.p3.startEditor();
  420. await peers.p1.focus();
  421. await peers.p2.focus();
  422. await peers.p3.focus();
  423. await peers.p1.openDataChannel(peers.p2);
  424. await peers.p1.openDataChannel(peers.p3);
  425. await peers.p2.openDataChannel(peers.p3);
  426. peers.p2.setOffline();
  427. peers.p3.setOffline();
  428. const p2Spies = {
  429. _recoverFromStaleDocument: makeSpy(),
  430. _resetFromServerAndResyncWithClients: makeSpy(),
  431. _processMissingSteps: makeSpy(),
  432. _applySnapshot: makeSpy(),
  433. };
  434. patch(peers.p2.wysiwyg, 'test', p2Spies);
  435. const p3Spies = {
  436. _recoverFromStaleDocument: makeSpy(),
  437. _resetFromServerAndResyncWithClients: makeSpy(),
  438. _processMissingSteps: makeSpy(),
  439. _applySnapshot: makeSpy(),
  440. _onRecoveryClientTimeout: makeSpy(),
  441. };
  442. patch(peers.p3.wysiwyg, 'test', p3Spies);
  443. await peers.p1.makeStep(insert('b'));
  444. await peers.p1.writeToServer();
  445. assert.equal(peers.p1.getValue(), `<p>ab[]</p>`, 'p1 have inserted char b');
  446. assert.equal(peers.p2.getValue(), `[]<p>a</p>`, 'p2 should not have the same document as p1');
  447. assert.equal(peers.p3.getValue(), `[]<p>a</p>`, 'p3 should not have the same document as p1');
  448. peers.p1.destroyEditor();
  449. assert.equal(p2Spies._recoverFromStaleDocument.callCount, 0, 'p2 _recoverFromStaleDocument should not have been called');
  450. assert.equal(p2Spies._resetFromServerAndResyncWithClients.callCount, 0, 'p2 _resetFromServerAndResyncWithClients should not have been called');
  451. assert.equal(p2Spies._processMissingSteps.callCount, 0, 'p2 _processMissingSteps should not have been called');
  452. assert.equal(p2Spies._applySnapshot.callCount, 0, 'p2 _applySnapshot should not have been called');
  453. await peers.p2.setOnline();
  454. assert.equal(peers.p2.getValue(), `[]<p>ab</p>`, 'p2 should have the same document as p1');
  455. assert.equal(peers.p3.getValue(), `<p>[]a</p>`, 'p3 should not have the same document as p1');
  456. assert.equal(p2Spies._recoverFromStaleDocument.callCount, 1, 'p2 _recoverFromStaleDocument should have been called once');
  457. assert.equal(p2Spies._resetFromServerAndResyncWithClients.callCount, 1, 'p2 _resetFromServerAndResyncWithClients should have been called once');
  458. assert.equal(p2Spies._processMissingSteps.callCount, 0, 'p2 _processMissingSteps should not have been called');
  459. assert.equal(p2Spies._applySnapshot.callCount, 0, 'p2 _applySnapshot should not have been called');
  460. await peers.p3.setOnline();
  461. assert.equal(peers.p3.getValue(), `[]<p>ab</p>`, 'p3 should have the same document as p1');
  462. assert.equal(p3Spies._recoverFromStaleDocument.callCount, 1, 'p3 _recoverFromStaleDocument should have been called once');
  463. assert.equal(p3Spies._resetFromServerAndResyncWithClients.callCount, 0, 'p3 _resetFromServerAndResyncWithClients should not have been called');
  464. assert.equal(p3Spies._processMissingSteps.callCount, 1, 'p3 _processMissingSteps should have been called once');
  465. assert.equal(p3Spies._applySnapshot.callCount, 1, 'p3 _applySnapshot should have been called once');
  466. assert.equal(p3Spies._onRecoveryClientTimeout.callCount, 0, 'p3 _onRecoveryClientTimeout should not have been called');
  467. unpatch(peers.p2.wysiwyg, 'test');
  468. unpatch(peers.p3.wysiwyg, 'test');
  469. removePeers(peers);
  470. });
  471. QUnit.test('should recover from snapshot after PTP_MAX_RECOVERY_TIME if some peer do not respond', async (assert) => {
  472. assert.expect(19);
  473. const pool = await createPeers(['p1', 'p2', 'p3']);
  474. const peers = pool.peers;
  475. await peers.p1.startEditor();
  476. await peers.p2.startEditor();
  477. await peers.p3.startEditor();
  478. await peers.p1.focus();
  479. await peers.p2.focus();
  480. await peers.p3.focus();
  481. await peers.p1.openDataChannel(peers.p2);
  482. await peers.p1.openDataChannel(peers.p3);
  483. await peers.p2.openDataChannel(peers.p3);
  484. peers.p2.setOffline();
  485. peers.p3.setOffline();
  486. const p2Spies = {
  487. _recoverFromStaleDocument: makeSpy(),
  488. _resetFromServerAndResyncWithClients: makeSpy(),
  489. _processMissingSteps: makeSpy(),
  490. _applySnapshot: makeSpy(),
  491. };
  492. patch(peers.p2.wysiwyg, 'test', p2Spies);
  493. const p3Spies = {
  494. _recoverFromStaleDocument: makeSpy(),
  495. _resetFromServerAndResyncWithClients: makeSpy(),
  496. _processMissingSteps: makeSpy(),
  497. _applySnapshot: makeSpy(),
  498. _onRecoveryClientTimeout: makeSpy(),
  499. };
  500. patch(peers.p3.wysiwyg, 'test', p3Spies);
  501. await peers.p1.makeStep(insert('b'));
  502. await peers.p1.writeToServer();
  503. peers.p1.setOffline();
  504. assert.equal(peers.p1.getValue(), `<p>ab[]</p>`, 'p1 have inserted char b');
  505. assert.equal(peers.p2.getValue(), `[]<p>a</p>`, 'p2 should not have the same document as p1');
  506. assert.equal(peers.p3.getValue(), `[]<p>a</p>`, 'p3 should not have the same document as p1');
  507. assert.equal(p2Spies._recoverFromStaleDocument.callCount, 0, 'p2 _recoverFromStaleDocument should not have been called');
  508. assert.equal(p2Spies._resetFromServerAndResyncWithClients.callCount, 0, 'p2 _resetFromServerAndResyncWithClients should not have been called');
  509. assert.equal(p2Spies._processMissingSteps.callCount, 0, 'p2 _processMissingSteps should not have been called');
  510. assert.equal(p2Spies._applySnapshot.callCount, 0, 'p2 _applySnapshot should not have been called');
  511. await peers.p2.setOnline();
  512. assert.equal(peers.p2.getValue(), `[]<p>ab</p>`, 'p2 should have the same document as p1');
  513. assert.equal(peers.p3.getValue(), `<p>[]a</p>`, 'p3 should not have the same document as p1');
  514. assert.equal(p2Spies._recoverFromStaleDocument.callCount, 1, 'p2 _recoverFromStaleDocument should have been called once');
  515. assert.equal(p2Spies._resetFromServerAndResyncWithClients.callCount, 1, 'p2 _resetFromServerAndResyncWithClients should have been called once');
  516. assert.equal(p2Spies._processMissingSteps.callCount, 0, 'p2 _processMissingSteps should not have been called');
  517. assert.equal(p2Spies._applySnapshot.callCount, 0, 'p2 _applySnapshot should not have been called');
  518. await peers.p3.setOnline();
  519. assert.equal(peers.p3.getValue(), `[]<p>ab</p>`, 'p3 should have the same document as p1');
  520. assert.equal(p3Spies._recoverFromStaleDocument.callCount, 1, 'p3 _recoverFromStaleDocument should have been called once');
  521. assert.equal(p3Spies._resetFromServerAndResyncWithClients.callCount, 0, 'p3 _resetFromServerAndResyncWithClients should have been called once');
  522. assert.equal(p3Spies._processMissingSteps.callCount, 1, 'p3 _processMissingSteps should have been called once');
  523. assert.equal(p3Spies._applySnapshot.callCount, 1, 'p3 _applySnapshot should have been called once');
  524. assert.equal(p3Spies._onRecoveryClientTimeout.callCount, 1, 'p3 _onRecoveryClientTimeout should have been called once');
  525. unpatch(peers.p2.wysiwyg, 'test');
  526. unpatch(peers.p3.wysiwyg, 'test');
  527. removePeers(peers);
  528. });
  529. });
  530. QUnit.module('recover from server', async () => {
  531. QUnit.test('should recover from server if no snapshot have been processed', async (assert) => {
  532. assert.expect(16);
  533. const pool = await createPeers(['p1', 'p2', 'p3']);
  534. const peers = pool.peers;
  535. await peers.p1.startEditor();
  536. await peers.p2.startEditor();
  537. await peers.p3.startEditor();
  538. await peers.p1.focus();
  539. await peers.p2.focus();
  540. await peers.p3.focus();
  541. await peers.p1.openDataChannel(peers.p2);
  542. await peers.p1.openDataChannel(peers.p3);
  543. await peers.p2.openDataChannel(peers.p3);
  544. peers.p2.setOffline();
  545. peers.p3.setOffline();
  546. const p2Spies = {
  547. _recoverFromStaleDocument: makeSpy(),
  548. _resetFromServerAndResyncWithClients: makeSpy(),
  549. _processMissingSteps: makeSpy(),
  550. _applySnapshot: makeSpy(),
  551. _onRecoveryClientTimeout: makeSpy(),
  552. _resetFromClient: makeSpy(),
  553. };
  554. patch(peers.p2.wysiwyg, 'test', p2Spies);
  555. const p3Spies = {
  556. _recoverFromStaleDocument: makeSpy(),
  557. _resetFromServerAndResyncWithClients: makeSpy(),
  558. _processMissingSteps: makeSpy(),
  559. _applySnapshot: makeSpy(),
  560. _onRecoveryClientTimeout: makeSpy(),
  561. _resetFromClient: makeSpy(),
  562. };
  563. patch(peers.p3.wysiwyg, 'test', p3Spies);
  564. await peers.p1.makeStep(insert('b'));
  565. await peers.p1.writeToServer();
  566. assert.equal(peers.p1.getValue(), `<p>ab[]</p>`, 'p1 have inserted char b');
  567. assert.equal(peers.p2.getValue(), `[]<p>a</p>`, 'p2 should not have the same document as p1');
  568. assert.equal(peers.p3.getValue(), `[]<p>a</p>`, 'p3 should not have the same document as p1');
  569. peers.p1.destroyEditor();
  570. assert.equal(p2Spies._recoverFromStaleDocument.callCount, 0, 'p2 _recoverFromStaleDocument should not have been called');
  571. assert.equal(p2Spies._resetFromServerAndResyncWithClients.callCount, 0, 'p2 _resetFromServerAndResyncWithClients should not have been called');
  572. assert.equal(p2Spies._processMissingSteps.callCount, 0, 'p2 _processMissingSteps should not have been called');
  573. assert.equal(p2Spies._applySnapshot.callCount, 0, 'p2 _applySnapshot should not have been called');
  574. assert.equal(p2Spies._onRecoveryClientTimeout.callCount, 0, 'p2 _onRecoveryClientTimeout should not have been called');
  575. assert.equal(p2Spies._resetFromClient.callCount, 0, 'p2 _resetFromClient should not have been called');
  576. // Because we do not wait for the end of the
  577. // p2.setOnline promise, p3 will not be able to reset
  578. // from p2 wich allow us to test that p3 reset from the
  579. // server as a fallback.
  580. peers.p2.setOnline();
  581. await peers.p3.setOnline();
  582. assert.equal(peers.p3.getValue(), `[]<p>ab</p>`, 'p3 should have the same document as p1');
  583. assert.equal(p3Spies._recoverFromStaleDocument.callCount, 1, 'p3 _recoverFromStaleDocument should have been called once');
  584. assert.equal(p3Spies._resetFromServerAndResyncWithClients.callCount, 1, 'p3 _resetFromServerAndResyncWithClients should have been called once');
  585. assert.equal(p3Spies._processMissingSteps.callCount, 0, 'p3 _processMissingSteps should not have been called');
  586. assert.equal(p3Spies._applySnapshot.callCount, 1, 'p3 _applySnapshot should have been called once');
  587. assert.equal(p3Spies._onRecoveryClientTimeout.callCount, 0, 'p3 _onRecoveryClientTimeout should not have been called');
  588. assert.equal(p3Spies._resetFromClient.callCount, 1, 'p3 _resetFromClient should have been called once');
  589. unpatch(peers.p2.wysiwyg, 'test');
  590. unpatch(peers.p3.wysiwyg, 'test');
  591. removePeers(peers);
  592. });
  593. QUnit.test('should recover from server if there is no peer connected', async (assert) => {
  594. assert.expect(14);
  595. const pool = await createPeers(['p1', 'p2']);
  596. const peers = pool.peers;
  597. await peers.p1.startEditor();
  598. await peers.p2.startEditor();
  599. await peers.p1.focus();
  600. await peers.p2.focus();
  601. await peers.p1.openDataChannel(peers.p2);
  602. peers.p2.setOffline();
  603. const p2Spies = {
  604. _recoverFromStaleDocument: makeSpy(),
  605. _resetFromServerAndResyncWithClients: makeSpy(),
  606. _processMissingSteps: makeSpy(),
  607. _applySnapshot: makeSpy(),
  608. _onRecoveryClientTimeout: makeSpy(),
  609. _resetFromClient: makeSpy(),
  610. };
  611. patch(peers.p2.wysiwyg, 'test', p2Spies);
  612. await peers.p1.makeStep(insert('b'));
  613. await peers.p1.writeToServer();
  614. assert.equal(peers.p1.getValue(), `<p>ab[]</p>`, 'p1 have inserted char b');
  615. assert.equal(peers.p2.getValue(), `[]<p>a</p>`, 'p2 should not have the same document as p1');
  616. peers.p1.destroyEditor();
  617. assert.equal(p2Spies._recoverFromStaleDocument.callCount, 0, 'p2 _recoverFromStaleDocument should not have been called');
  618. assert.equal(p2Spies._resetFromServerAndResyncWithClients.callCount, 0, 'p2 _resetFromServerAndResyncWithClients should not have been called');
  619. assert.equal(p2Spies._processMissingSteps.callCount, 0, 'p2 _processMissingSteps should not have been called');
  620. assert.equal(p2Spies._applySnapshot.callCount, 0, 'p2 _applySnapshot should not have been called');
  621. assert.equal(p2Spies._resetFromClient.callCount, 0, 'p2 _resetFromClient should not have been called');
  622. await peers.p2.setOnline();
  623. assert.equal(peers.p2.getValue(), `[]<p>ab</p>`, 'p2 should have the same document as p1');
  624. assert.equal(p2Spies._recoverFromStaleDocument.callCount, 1, 'p2 _recoverFromStaleDocument should have been called once');
  625. assert.equal(p2Spies._resetFromServerAndResyncWithClients.callCount, 1, 'p2 _resetFromServerAndResyncWithClients should have been called once');
  626. assert.equal(p2Spies._processMissingSteps.callCount, 0, 'p2 _processMissingSteps should not have been called');
  627. assert.equal(p2Spies._applySnapshot.callCount, 0, 'p2 _applySnapshot should not have been called');
  628. assert.equal(p2Spies._onRecoveryClientTimeout.callCount, 0, 'p2 _onRecoveryClientTimeout should not have been called');
  629. assert.equal(p2Spies._resetFromClient.callCount, 0, 'p2 _resetFromClient should not have been called');
  630. unpatch(peers.p2.wysiwyg, 'test');
  631. removePeers(peers);
  632. });
  633. QUnit.test('should recover from server if there is no response after PTP_MAX_RECOVERY_TIME', async (assert) => {
  634. assert.expect(16);
  635. const pool = await createPeers(['p1', 'p2', 'p3']);
  636. const peers = pool.peers;
  637. await peers.p1.startEditor();
  638. await peers.p2.startEditor();
  639. await peers.p3.startEditor();
  640. await peers.p1.focus();
  641. await peers.p2.focus();
  642. await peers.p1.openDataChannel(peers.p2);
  643. await peers.p1.openDataChannel(peers.p3);
  644. await peers.p2.openDataChannel(peers.p3);
  645. peers.p2.setOffline();
  646. peers.p3.setOffline();
  647. const p2Spies = {
  648. _recoverFromStaleDocument: makeSpy(),
  649. _resetFromServerAndResyncWithClients: makeSpy(),
  650. _processMissingSteps: makeSpy(),
  651. _applySnapshot: makeSpy(),
  652. _onRecoveryClientTimeout: makeSpy(),
  653. _resetFromClient: makeSpy(),
  654. };
  655. patch(peers.p2.wysiwyg, 'test', p2Spies);
  656. await peers.p1.makeStep(insert('b'));
  657. await peers.p1.writeToServer();
  658. peers.p1.setOffline();
  659. assert.equal(peers.p1.getValue(), `<p>ab[]</p>`, 'p1 have inserted char b');
  660. assert.equal(peers.p2.getValue(), `[]<p>a</p>`, 'p2 should not have the same document as p1');
  661. assert.equal(peers.p3.getValue(), `[]<p>a</p>`, 'p3 should not have the same document as p1');
  662. assert.equal(p2Spies._recoverFromStaleDocument.callCount, 0, 'p2 _recoverFromStaleDocument should not have been called');
  663. assert.equal(p2Spies._resetFromServerAndResyncWithClients.callCount, 0, 'p2 _resetFromServerAndResyncWithClients should not have been called');
  664. assert.equal(p2Spies._processMissingSteps.callCount, 0, 'p2 _processMissingSteps should not have been called');
  665. assert.equal(p2Spies._applySnapshot.callCount, 0, 'p2 _applySnapshot should not have been called');
  666. assert.equal(p2Spies._resetFromClient.callCount, 0, 'p2 _resetFromClient should not have been called');
  667. await peers.p2.setOnline();
  668. assert.equal(peers.p2.getValue(), `[]<p>ab</p>`, 'p2 should have the same document as p1');
  669. assert.equal(peers.p3.getValue(), `<p>[]a</p>`, 'p3 should not have the same document as p1');
  670. assert.equal(p2Spies._recoverFromStaleDocument.callCount, 1, 'p2 _recoverFromStaleDocument should have been called once');
  671. assert.equal(p2Spies._resetFromServerAndResyncWithClients.callCount, 1, 'p2 _resetFromServerAndResyncWithClients should have been called once');
  672. assert.equal(p2Spies._processMissingSteps.callCount, 0, 'p2 _processMissingSteps should not have been called');
  673. assert.equal(p2Spies._applySnapshot.callCount, 0, 'p2 _applySnapshot should not have been called');
  674. assert.equal(p2Spies._onRecoveryClientTimeout.callCount, 1, 'p2 _onRecoveryClientTimeout should have been called once');
  675. // p1 and p3 are considered offline but not
  676. // disconnected. It means that p2 will try to recover
  677. // from p1 and p3 even if they are currently
  678. // unavailable. This test is usefull to check that the
  679. // code path to _resetFromClient is properly taken.
  680. assert.equal(p2Spies._resetFromClient.callCount, 2, 'p2 _resetFromClient should have been called twice');
  681. unpatch(peers.p2.wysiwyg, 'test');
  682. removePeers(peers);
  683. });
  684. });
  685. });
  686. });
  687. QUnit.module('Disconnect & reconnect', {}, () => {
  688. QUnit.test('should sync history when disconnecting and reconnecting to internet', async (assert) => {
  689. assert.expect(2);
  690. const pool = await createPeers(['p1', 'p2']);
  691. const peers = pool.peers;
  692. await peers.p1.startEditor();
  693. await peers.p2.startEditor();
  694. await peers.p1.focus();
  695. await peers.p2.focus();
  696. await peers.p1.openDataChannel(peers.p2);
  697. await peers.p1.makeStep(insert('b'));
  698. await peers.p1.setOffline();
  699. peers.p1.removeDataChannel(peers.p2);
  700. const setSelection = (peer) => {
  701. const selection = peer.document.getSelection();
  702. const pElement = peer.wysiwyg.odooEditor.editable.querySelector('p')
  703. const range = new Range();
  704. range.setStart(pElement, 1);
  705. range.setEnd(pElement, 1);
  706. selection.removeAllRanges();
  707. selection.addRange(range);
  708. }
  709. const addP = (peer, content) => {
  710. const p = document.createElement('p');
  711. p.textContent = content;
  712. peer.wysiwyg.odooEditor.editable.append(p);
  713. peer.wysiwyg.odooEditor.historyStep();
  714. }
  715. setSelection(peers.p1);
  716. await peers.p1.makeStep(insert('c'));
  717. addP(peers.p1, 'd');
  718. setSelection(peers.p2);
  719. await peers.p2.makeStep(insert('e'));
  720. addP(peers.p2, 'f');
  721. peers.p1.setOnline();
  722. peers.p2.setOnline();
  723. // todo: p1PromiseForMissingStep and p2PromiseForMissingStep
  724. // should be removed when the fix of undetected missing step
  725. // will be merged. (task-3208277)
  726. const p1PromiseForMissingStep = new Promise((resolve) => {
  727. patch(peers.p2.wysiwyg, 'missingSteps', {
  728. async _processMissingSteps() {
  729. const _super = this._super;
  730. // Wait for the p2PromiseForMissingStep to resolve
  731. // to avoid undetected missing step.
  732. await p2PromiseForMissingStep;
  733. _super(...arguments);
  734. resolve();
  735. }
  736. })
  737. });
  738. const p2PromiseForMissingStep = new Promise((resolve) => {
  739. patch(peers.p1.wysiwyg, 'missingSteps', {
  740. async _processMissingSteps() {
  741. this._super(...arguments);
  742. resolve();
  743. }
  744. })
  745. });
  746. await peers.p1.openDataChannel(peers.p2);
  747. await p1PromiseForMissingStep;
  748. assert.equal(peers.p1.getValue(), `<p>ac[]eb</p><p>d</p><p>f</p>`, 'p1 should have the value merged with p2');
  749. assert.equal(peers.p2.getValue(), `<p>ace[]b</p><p>d</p><p>f</p>`, 'p2 should have the value merged with p1');
  750. removePeers(peers);
  751. });
  752. });
  753. });
  754. });