123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891 |
- /** @odoo-module **/
- import { patch, unpatch } from "@web/core/utils/patch";
- import {
- parseTextualSelection,
- setTestSelection,
- renderTextualSelection,
- patchEditorIframe,
- } from '@web_editor/js/editor/odoo-editor/test/utils';
- import { stripHistoryIds } from '@web_editor/js/backend/html_field';
- import Wysiwyg from 'web_editor.wysiwyg';
- import { Mutex } from '@web/core/utils/concurrency';
- function makeSpy() {
- const spy = function() {
- spy.callCount++;
- return this._super.apply(this, arguments);
- };
- spy.callCount = 0;
- return spy;
- }
- class PeerTest {
- constructor(infos) {
- this.peerId = infos.peerId;
- this.wysiwyg = infos.wysiwyg;
- this.iframe = infos.iframe;
- this.document = this.iframe.contentWindow.document;
- this.wrapper = infos.wrapper;
- this.pool = infos.pool;
- this.peers = infos.pool.peers;
- this._connections = new Set();
- this.onlineMutex = new Mutex();
- this.isOnline = true;
- }
- async startEditor() {
- this._started = this.wysiwyg.appendTo(this.wrapper);
- await this._started;
- if (this.initialParsedSelection) {
- setTestSelection(this.initialParsedSelection, this.document);
- this.wysiwyg.odooEditor._recordHistorySelection();
- } else {
- document.getSelection().removeAllRanges();
- }
- clearInterval(this.wysiwyg._collaborationInterval);
- return this._started;
- }
- async destroyEditor() {
- for (const peer of this._connections) {
- peer._connections.delete(this);
- }
- this.wysiwyg.destroy();
- }
- async focus() {
- await this.started;
- return this.wysiwyg._joinPeerToPeer();
- }
- async openDataChannel(peer) {
- this._connections.add(peer);
- peer._connections.add(this);
- const ptpFrom = this.wysiwyg.ptp;
- const ptpTo = peer.wysiwyg.ptp;
- ptpFrom.clientsInfos[peer.peerId] = {};
- ptpTo.clientsInfos[this.peerId] = {};
- // Simulate the rtc_data_channel_open on both peers.
- await this.wysiwyg.ptp.notifySelf('rtc_data_channel_open', {
- connectionClientId: peer.peerId,
- });
- await peer.wysiwyg.ptp.notifySelf('rtc_data_channel_open', {
- connectionClientId: this.peerId,
- });
- }
- async removeDataChannel(peer) {
- this._connections.delete(peer);
- peer._connections.delete(this);
- const ptpFrom = this.wysiwyg.ptp;
- const ptpTo = peer.wysiwyg.ptp;
- delete ptpFrom.clientsInfos[peer.peerId];
- delete ptpTo.clientsInfos[this.peerId];
- this.onlineMutex = new Mutex();
- this._onlineResolver = undefined;
- }
- makeStep(fn) {
- fn(this);
- }
- getValue() {
- this.wysiwyg.odooEditor.observerUnactive('PeerTest.getValue');
- renderTextualSelection(this.document);
- const html = this.wysiwyg.$editable[0].innerHTML;
- const selection = parseTextualSelection(this.wysiwyg.$editable[0]);
- if (selection) {
- setTestSelection(selection, this.document);
- }
- this.wysiwyg.odooEditor.observerActive('PeerTest.getValue');
- return stripHistoryIds(html);
- }
- writeToServer() {
- this.pool.lastRecordSaved = this.wysiwyg.getValue();
- const lastId = this.wysiwyg._getLastHistoryStepId(this.pool.lastRecordSaved);
- for (const peer of Object.values(this.peers)) {
- if (peer === this || !peer._started) continue;
- peer.onlineMutex.exec(() => {
- return peer.wysiwyg._onServerLastIdUpdate(String(lastId));
- });
- }
- }
- async setOnline() {
- this.isOnline = true;
- this._onlineResolver && this._onlineResolver();
- return this.onlineMutex.getUnlockedDef();
- }
- setOffline() {
- this.isOnline = false;
- if (this._onlineResolver) return;
- this.onlineMutex.exec(async () => {
- await new Promise((resolve) => {
- this._onlineResolver = () => {
- this._onlineResolver = null;
- resolve();
- }
- });
- });
- }
- }
- function insert(string) {
- return (peer) => {
- peer.wysiwyg.odooEditor.execCommand('insert', string);
- }
- }
- const initialValue = '<p data-last-history-steps="1">a[]</p>';
- class PeerPool {
- constructor(peers) {
- this.peers = {};
- }
- }
- async function createPeers(peers) {
- const pool = new PeerPool();
- let lastGeneratedId = 0;
- for (const peerId of peers) {
- const peerWysiwygWrapper = document.createElement('div');
- peerWysiwygWrapper.classList.add('peer_wysiwyg_wrapper');
- peerWysiwygWrapper.classList.add('client_' + peerId);
- const iframe = document.createElement('iframe');
- if (navigator.userAgent.toLowerCase().indexOf('firefox') > -1) {
- // Firefox reset the page without this hack.
- // With this hack, chrome does not render content.
- iframe.setAttribute('src', ' javascript:void(0);');
- }
- document.querySelector('#qunit-fixture').append(iframe);
- patchEditorIframe(iframe);
- iframe.contentDocument.body.append(peerWysiwygWrapper);
- iframe.contentWindow.$ = $;
- const fakeWysiwygParent = {
- _trigger_up: () => {},
- };
- const wysiwyg = new Wysiwyg(fakeWysiwygParent, {
- value: initialValue,
- collaborative: true,
- collaborationChannel: {
- collaborationFieldName: "fake_field",
- collaborationModelName: "fake.model",
- collaborationResId: 1
- },
- document: iframe.contentWindow.document,
- });
- patch(wysiwyg, 'web_editor', {
- _generateClientId() {
- return peerId;
- },
- // Hacky hook as we know this method is called after setting the value in the wysiwyg start and before sending the value to odooEditor.
- _getLastHistoryStepId() {
- pool.peers[peerId].initialParsedSelection = parseTextualSelection(wysiwyg.$editable[0]);
- return this._super(...arguments);
- },
- call: () => {},
- getSession: () => ({notification_type: true}),
- _rpc(params) {
- if (params.route === '/web_editor/get_ice_servers') {
- return [];
- } else if (params.route === '/web_editor/bus_broadcast') {
- const currentPeer = pool.peers[peerId];
- for (const peer of currentPeer._connections) {
- peer.wysiwyg.ptp.handleNotification(structuredClone(params.params.bus_data));
- }
- } else if (params.model === "res.users" && params.method === "search_read") {
- return [{ name: "admin" }];
- }
- },
- _getNewPtp() {
- const ptp = this._super(...arguments);
- patch(ptp, "web_editor_peer_to_peer", {
- removeClient(peerId) {
- this.notifySelf('ptp_remove', peerId);
- delete this.clientsInfos[peerId];
- },
- notifyAllClients(...args) {
- // This is not needed because the opening of the
- // dataChannel is done through `openDataChannel` and we
- // do not want to simulate the events that thrigger the
- // openning of the dataChannel.
- if (args[0] === 'ptp_join') {
- return;
- }
- this._super(...args);
- },
- _getPtpClients() {
- return pool.peers[peerId]._connections.map((peer) => {
- return { id: peer.peerId }
- });
- },
- async _channelNotify(peerId, transportPayload) {
- if (!pool.peers[peerId].isOnline) return;
- pool.peers[peerId].wysiwyg.ptp.handleNotification(structuredClone(transportPayload));
- },
- _createClient() {
- throw new Error('Should not be called.');
- },
- _addIceCandidate() {
- throw new Error('Should not be called.');
- },
- _recoverConnection() {
- throw new Error('Should not be called.');
- },
- _killPotentialZombie() {
- throw new Error('Should not be called.');
- },
- });
- return ptp;
- },
- _getCurrentRecord() {
- return {
- id: 1,
- fake_field: pool.lastRecordSaved,
- }
- },
- _getCollaborationClientAvatarUrl() {
- return '';
- },
- async startEdition() {
- await this._super(...arguments);
- patch(this.odooEditor, 'odooEditor', {
- _generateId() {
- // Ensure the id are deterministically gererated for
- // when we need to sort by them. (eg. in the
- // callaboration sorting of steps)
- lastGeneratedId++;
- return lastGeneratedId.toString();
- },
- });
- }
- });
- pool.peers[peerId] = new PeerTest({
- peerId,
- wysiwyg,
- iframe,
- wrapper: peerWysiwygWrapper,
- pool,
- });
- }
- return pool;
- }
- function removePeers(peers) {
- for (const peer of Object.values(peers)) {
- peer.wysiwyg.destroy();
- peer.wrapper.remove();
- }
- }
- QUnit.module('web_editor', {
- before() {
- patch(Wysiwyg, 'web_editor', {
- activeCollaborationChannelNames: {
- has: () => false,
- add: () => {},
- delete: () => {},
- }
- });
- },
- after() {
- unpatch(Wysiwyg, "web_editor");
- }
- }, () => {
- QUnit.module('Collaboration', {}, () => {
- /**
- * Detect stale when <already focused | not already focused>
- */
- QUnit.module('Focus', {}, () => {
- QUnit.test('Focused client should not receive step if no data channel is open', async (assert) => {
- assert.expect(3);
- const pool = await createPeers(['p1', 'p2', 'p3']);
- const peers = pool.peers;
- await peers.p1.startEditor();
- await peers.p2.startEditor();
- await peers.p3.startEditor();
- await peers.p1.focus();
- await peers.p2.focus();
- await peers.p1.makeStep(insert('b'));
- assert.equal(peers.p1.getValue(), `<p>ab[]</p>`, 'p1 should have the document changed');
- assert.equal(peers.p2.getValue(), `<p>a[]</p>`, 'p2 should not have the document changed');
- assert.equal(peers.p3.getValue(), `<p>a[]</p>`, 'p3 should not have the document changed');
- removePeers(peers);
- });
- QUnit.test('Focused client should receive step while unfocused should not (if the datachannel is open before the step)', async (assert) => {
- assert.expect(3);
- const pool = await createPeers(['p1', 'p2', 'p3']);
- const peers = pool.peers;
- await peers.p1.startEditor();
- await peers.p2.startEditor();
- await peers.p3.startEditor();
- await peers.p1.focus();
- await peers.p2.focus();
- await peers.p1.openDataChannel(peers.p2);
- await peers.p1.makeStep(insert('b'));
- assert.equal(peers.p1.getValue(), `<p>ab[]</p>`, 'p1 should have the same document as p2');
- assert.equal(peers.p2.getValue(), `[]<p>ab</p>`, 'p2 should have the same document as p1');
- assert.equal(peers.p3.getValue(), `<p>a[]</p>`, 'p3 should not have the document changed');
- removePeers(peers);
- });
- QUnit.test('Focused client should receive step while unfocused should not (if the datachannel is open after the step)', async (assert) => {
- assert.expect(3);
- const pool = await createPeers(['p1', 'p2', 'p3']);
- const peers = pool.peers;
- await peers.p1.startEditor();
- await peers.p2.startEditor();
- await peers.p3.startEditor();
- await peers.p1.focus();
- await peers.p2.focus();
- await peers.p1.makeStep(insert('b'));
- await peers.p1.openDataChannel(peers.p2);
- assert.equal(peers.p1.getValue(), `<p>ab[]</p>`, 'p1 should have the same document as p2');
- assert.equal(peers.p2.getValue(), `[]<p>ab</p>`, 'p2 should have the same document as p1');
- assert.equal(peers.p3.getValue(), `<p>a[]</p>`, 'p3 should not have the document changed because it has not focused');
- removePeers(peers);
- });
- });
- QUnit.module('Stale detection & recovery', {}, () => {
- QUnit.module('detect stale while unfocused', async () => {
- QUnit.test('should do nothing until focus', async (assert) => {
- assert.expect(10);
- const pool = await createPeers(['p1', 'p2', 'p3']);
- const peers = pool.peers;
- await peers.p1.startEditor();
- await peers.p2.startEditor();
- await peers.p3.startEditor();
- await peers.p1.focus();
- await peers.p2.focus();
- await peers.p1.openDataChannel(peers.p2);
- await peers.p1.makeStep(insert('b'));
- await peers.p1.writeToServer();
- assert.equal(peers.p1.wysiwyg._isDocumentStale, false, 'p1 should not have a stale document');
- assert.equal(peers.p1.getValue(), `<p>ab[]</p>`, 'p1 should have the same document as p2');
- assert.equal(peers.p2.wysiwyg._isDocumentStale, false, 'p2 should not have a stale document');
- assert.equal(peers.p2.getValue(), `[]<p>ab</p>`, 'p2 should have the same document as p1');
- assert.equal(peers.p3.wysiwyg._isDocumentStale, true, 'p3 should have a stale document');
- assert.equal(peers.p3.getValue(), `<p>a[]</p>`, 'p3 should not have the same document as p1');
- await peers.p3.focus();
- await peers.p1.openDataChannel(peers.p3);
- // This timeout is necessary for the selection to be set
- await new Promise(resolve => setTimeout(resolve));
- assert.equal(peers.p3.wysiwyg._isDocumentStale, false, 'p3 should not have a stale document');
- assert.equal(peers.p3.getValue(), `<p>[]ab</p>`, 'p3 should have the same document as p1');
- await peers.p1.makeStep(insert('c'));
- assert.equal(peers.p1.getValue(), `<p>abc[]</p>`, 'p1 should have the same document as p3');
- assert.equal(peers.p3.getValue(), `<p>[]abc</p>`, 'p3 should have the same document as p1');
- removePeers(peers);
- });
- });
- QUnit.module('detect stale while focused', async () => {
- QUnit.module('recover from missing steps', async () => {
- QUnit.test('should recover from missing steps', async (assert) => {
- assert.expect(18);
- const pool = await createPeers(['p1', 'p2', 'p3']);
- const peers = pool.peers;
- await peers.p1.startEditor();
- await peers.p2.startEditor();
- await peers.p3.startEditor();
- await peers.p1.focus();
- await peers.p2.focus();
- await peers.p3.focus();
- await peers.p1.openDataChannel(peers.p2);
- await peers.p1.openDataChannel(peers.p3);
- await peers.p2.openDataChannel(peers.p3);
- const p3Spies = {
- _recoverFromStaleDocument: makeSpy(),
- _resetFromServerAndResyncWithClients: makeSpy(),
- _processMissingSteps: makeSpy(),
- _applySnapshot: makeSpy(),
- };
- patch(peers.p3.wysiwyg, 'test', p3Spies);
- assert.equal(peers.p1.wysiwyg._historyShareId, peers.p2.wysiwyg._historyShareId, 'p1 and p2 should have the same _historyShareId');
- assert.equal(peers.p1.wysiwyg._historyShareId, peers.p3.wysiwyg._historyShareId, 'p1 and p3 should have the same _historyShareId');
- assert.equal(peers.p1.getValue(), `<p>a[]</p>`, 'p1 should have the same document as p2');
- assert.equal(peers.p2.getValue(), `[]<p>a</p>`, 'p2 should have the same document as p1');
- assert.equal(peers.p3.getValue(), `[]<p>a</p>`, 'p3 should have the same document as p1');
- await peers.p3.setOffline();
- await peers.p1.makeStep(insert('b'));
- assert.equal(peers.p1.getValue(), `<p>ab[]</p>`, 'p1 should have the same document as p2');
- assert.equal(peers.p2.getValue(), `<p>[]ab</p>`, 'p2 should have the same document as p1');
- assert.equal(peers.p3.getValue(), `<p>[]a</p>`, 'p3 should not have the same document as p1');
- await peers.p1.writeToServer();
- assert.equal(peers.p1.wysiwyg._isDocumentStale, false, 'p1 should not have a stale document');
- assert.equal(peers.p2.wysiwyg._isDocumentStale, false, 'p2 should not have a stale document');
- assert.equal(peers.p3.wysiwyg._isDocumentStale, false, 'p3 should not have a stale document');
- await peers.p3.setOnline();
- unpatch(peers.p3.wysiwyg, 'test');
- assert.equal(p3Spies._recoverFromStaleDocument.callCount, 1, 'p3 _recoverFromStaleDocument should have been called once');
- assert.equal(p3Spies._processMissingSteps.callCount, 1, 'p3 _processMissingSteps should have been called once');
- assert.equal(p3Spies._applySnapshot.callCount, 0, 'p3 _applySnapshot should not have been called');
- assert.equal(p3Spies._resetFromServerAndResyncWithClients.callCount, 0, 'p3 _resetFromServerAndResyncWithClients should not have been called');
- assert.equal(peers.p1.getValue(), `<p>ab[]</p>`, 'p1 should have the same document as p2');
- assert.equal(peers.p2.getValue(), `<p>[]ab</p>`, 'p2 should have the same document as p1');
- assert.equal(peers.p3.getValue(), `<p>[]ab</p>`, 'p3 should have the same document as p1');
- removePeers(peers);
- });
- });
- QUnit.module('recover from snapshot', async () => {
- QUnit.test('should wait for all peer to recover from snapshot', async (assert) => {
- assert.expect(19);
- const pool = await createPeers(['p1', 'p2', 'p3']);
- const peers = pool.peers;
- await peers.p1.startEditor();
- await peers.p2.startEditor();
- await peers.p3.startEditor();
- await peers.p1.focus();
- await peers.p2.focus();
- await peers.p3.focus();
- await peers.p1.openDataChannel(peers.p2);
- await peers.p1.openDataChannel(peers.p3);
- await peers.p2.openDataChannel(peers.p3);
- peers.p2.setOffline();
- peers.p3.setOffline();
- const p2Spies = {
- _recoverFromStaleDocument: makeSpy(),
- _resetFromServerAndResyncWithClients: makeSpy(),
- _processMissingSteps: makeSpy(),
- _applySnapshot: makeSpy(),
- };
- patch(peers.p2.wysiwyg, 'test', p2Spies);
- const p3Spies = {
- _recoverFromStaleDocument: makeSpy(),
- _resetFromServerAndResyncWithClients: makeSpy(),
- _processMissingSteps: makeSpy(),
- _applySnapshot: makeSpy(),
- _onRecoveryClientTimeout: makeSpy(),
- };
- patch(peers.p3.wysiwyg, 'test', p3Spies);
- await peers.p1.makeStep(insert('b'));
- await peers.p1.writeToServer();
- assert.equal(peers.p1.getValue(), `<p>ab[]</p>`, 'p1 have inserted char b');
- assert.equal(peers.p2.getValue(), `[]<p>a</p>`, 'p2 should not have the same document as p1');
- assert.equal(peers.p3.getValue(), `[]<p>a</p>`, 'p3 should not have the same document as p1');
- peers.p1.destroyEditor();
- assert.equal(p2Spies._recoverFromStaleDocument.callCount, 0, 'p2 _recoverFromStaleDocument should not have been called');
- assert.equal(p2Spies._resetFromServerAndResyncWithClients.callCount, 0, 'p2 _resetFromServerAndResyncWithClients should not have been called');
- assert.equal(p2Spies._processMissingSteps.callCount, 0, 'p2 _processMissingSteps should not have been called');
- assert.equal(p2Spies._applySnapshot.callCount, 0, 'p2 _applySnapshot should not have been called');
- await peers.p2.setOnline();
- assert.equal(peers.p2.getValue(), `[]<p>ab</p>`, 'p2 should have the same document as p1');
- assert.equal(peers.p3.getValue(), `<p>[]a</p>`, 'p3 should not have the same document as p1');
- assert.equal(p2Spies._recoverFromStaleDocument.callCount, 1, 'p2 _recoverFromStaleDocument should have been called once');
- assert.equal(p2Spies._resetFromServerAndResyncWithClients.callCount, 1, 'p2 _resetFromServerAndResyncWithClients should have been called once');
- assert.equal(p2Spies._processMissingSteps.callCount, 0, 'p2 _processMissingSteps should not have been called');
- assert.equal(p2Spies._applySnapshot.callCount, 0, 'p2 _applySnapshot should not have been called');
- await peers.p3.setOnline();
- assert.equal(peers.p3.getValue(), `[]<p>ab</p>`, 'p3 should have the same document as p1');
- assert.equal(p3Spies._recoverFromStaleDocument.callCount, 1, 'p3 _recoverFromStaleDocument should have been called once');
- assert.equal(p3Spies._resetFromServerAndResyncWithClients.callCount, 0, 'p3 _resetFromServerAndResyncWithClients should not have been called');
- assert.equal(p3Spies._processMissingSteps.callCount, 1, 'p3 _processMissingSteps should have been called once');
- assert.equal(p3Spies._applySnapshot.callCount, 1, 'p3 _applySnapshot should have been called once');
- assert.equal(p3Spies._onRecoveryClientTimeout.callCount, 0, 'p3 _onRecoveryClientTimeout should not have been called');
- unpatch(peers.p2.wysiwyg, 'test');
- unpatch(peers.p3.wysiwyg, 'test');
- removePeers(peers);
- });
- QUnit.test('should recover from snapshot after PTP_MAX_RECOVERY_TIME if some peer do not respond', async (assert) => {
- assert.expect(19);
- const pool = await createPeers(['p1', 'p2', 'p3']);
- const peers = pool.peers;
- await peers.p1.startEditor();
- await peers.p2.startEditor();
- await peers.p3.startEditor();
- await peers.p1.focus();
- await peers.p2.focus();
- await peers.p3.focus();
- await peers.p1.openDataChannel(peers.p2);
- await peers.p1.openDataChannel(peers.p3);
- await peers.p2.openDataChannel(peers.p3);
- peers.p2.setOffline();
- peers.p3.setOffline();
- const p2Spies = {
- _recoverFromStaleDocument: makeSpy(),
- _resetFromServerAndResyncWithClients: makeSpy(),
- _processMissingSteps: makeSpy(),
- _applySnapshot: makeSpy(),
- };
- patch(peers.p2.wysiwyg, 'test', p2Spies);
- const p3Spies = {
- _recoverFromStaleDocument: makeSpy(),
- _resetFromServerAndResyncWithClients: makeSpy(),
- _processMissingSteps: makeSpy(),
- _applySnapshot: makeSpy(),
- _onRecoveryClientTimeout: makeSpy(),
- };
- patch(peers.p3.wysiwyg, 'test', p3Spies);
- await peers.p1.makeStep(insert('b'));
- await peers.p1.writeToServer();
- peers.p1.setOffline();
- assert.equal(peers.p1.getValue(), `<p>ab[]</p>`, 'p1 have inserted char b');
- assert.equal(peers.p2.getValue(), `[]<p>a</p>`, 'p2 should not have the same document as p1');
- assert.equal(peers.p3.getValue(), `[]<p>a</p>`, 'p3 should not have the same document as p1');
- assert.equal(p2Spies._recoverFromStaleDocument.callCount, 0, 'p2 _recoverFromStaleDocument should not have been called');
- assert.equal(p2Spies._resetFromServerAndResyncWithClients.callCount, 0, 'p2 _resetFromServerAndResyncWithClients should not have been called');
- assert.equal(p2Spies._processMissingSteps.callCount, 0, 'p2 _processMissingSteps should not have been called');
- assert.equal(p2Spies._applySnapshot.callCount, 0, 'p2 _applySnapshot should not have been called');
- await peers.p2.setOnline();
- assert.equal(peers.p2.getValue(), `[]<p>ab</p>`, 'p2 should have the same document as p1');
- assert.equal(peers.p3.getValue(), `<p>[]a</p>`, 'p3 should not have the same document as p1');
- assert.equal(p2Spies._recoverFromStaleDocument.callCount, 1, 'p2 _recoverFromStaleDocument should have been called once');
- assert.equal(p2Spies._resetFromServerAndResyncWithClients.callCount, 1, 'p2 _resetFromServerAndResyncWithClients should have been called once');
- assert.equal(p2Spies._processMissingSteps.callCount, 0, 'p2 _processMissingSteps should not have been called');
- assert.equal(p2Spies._applySnapshot.callCount, 0, 'p2 _applySnapshot should not have been called');
- await peers.p3.setOnline();
- assert.equal(peers.p3.getValue(), `[]<p>ab</p>`, 'p3 should have the same document as p1');
- assert.equal(p3Spies._recoverFromStaleDocument.callCount, 1, 'p3 _recoverFromStaleDocument should have been called once');
- assert.equal(p3Spies._resetFromServerAndResyncWithClients.callCount, 0, 'p3 _resetFromServerAndResyncWithClients should have been called once');
- assert.equal(p3Spies._processMissingSteps.callCount, 1, 'p3 _processMissingSteps should have been called once');
- assert.equal(p3Spies._applySnapshot.callCount, 1, 'p3 _applySnapshot should have been called once');
- assert.equal(p3Spies._onRecoveryClientTimeout.callCount, 1, 'p3 _onRecoveryClientTimeout should have been called once');
- unpatch(peers.p2.wysiwyg, 'test');
- unpatch(peers.p3.wysiwyg, 'test');
- removePeers(peers);
- });
- });
- QUnit.module('recover from server', async () => {
- QUnit.test('should recover from server if no snapshot have been processed', async (assert) => {
- assert.expect(16);
- const pool = await createPeers(['p1', 'p2', 'p3']);
- const peers = pool.peers;
- await peers.p1.startEditor();
- await peers.p2.startEditor();
- await peers.p3.startEditor();
- await peers.p1.focus();
- await peers.p2.focus();
- await peers.p3.focus();
- await peers.p1.openDataChannel(peers.p2);
- await peers.p1.openDataChannel(peers.p3);
- await peers.p2.openDataChannel(peers.p3);
- peers.p2.setOffline();
- peers.p3.setOffline();
- const p2Spies = {
- _recoverFromStaleDocument: makeSpy(),
- _resetFromServerAndResyncWithClients: makeSpy(),
- _processMissingSteps: makeSpy(),
- _applySnapshot: makeSpy(),
- _onRecoveryClientTimeout: makeSpy(),
- _resetFromClient: makeSpy(),
- };
- patch(peers.p2.wysiwyg, 'test', p2Spies);
- const p3Spies = {
- _recoverFromStaleDocument: makeSpy(),
- _resetFromServerAndResyncWithClients: makeSpy(),
- _processMissingSteps: makeSpy(),
- _applySnapshot: makeSpy(),
- _onRecoveryClientTimeout: makeSpy(),
- _resetFromClient: makeSpy(),
- };
- patch(peers.p3.wysiwyg, 'test', p3Spies);
- await peers.p1.makeStep(insert('b'));
- await peers.p1.writeToServer();
- assert.equal(peers.p1.getValue(), `<p>ab[]</p>`, 'p1 have inserted char b');
- assert.equal(peers.p2.getValue(), `[]<p>a</p>`, 'p2 should not have the same document as p1');
- assert.equal(peers.p3.getValue(), `[]<p>a</p>`, 'p3 should not have the same document as p1');
- peers.p1.destroyEditor();
- assert.equal(p2Spies._recoverFromStaleDocument.callCount, 0, 'p2 _recoverFromStaleDocument should not have been called');
- assert.equal(p2Spies._resetFromServerAndResyncWithClients.callCount, 0, 'p2 _resetFromServerAndResyncWithClients should not have been called');
- assert.equal(p2Spies._processMissingSteps.callCount, 0, 'p2 _processMissingSteps should not have been called');
- assert.equal(p2Spies._applySnapshot.callCount, 0, 'p2 _applySnapshot should not have been called');
- assert.equal(p2Spies._onRecoveryClientTimeout.callCount, 0, 'p2 _onRecoveryClientTimeout should not have been called');
- assert.equal(p2Spies._resetFromClient.callCount, 0, 'p2 _resetFromClient should not have been called');
- // Because we do not wait for the end of the
- // p2.setOnline promise, p3 will not be able to reset
- // from p2 wich allow us to test that p3 reset from the
- // server as a fallback.
- peers.p2.setOnline();
- await peers.p3.setOnline();
- assert.equal(peers.p3.getValue(), `[]<p>ab</p>`, 'p3 should have the same document as p1');
- assert.equal(p3Spies._recoverFromStaleDocument.callCount, 1, 'p3 _recoverFromStaleDocument should have been called once');
- assert.equal(p3Spies._resetFromServerAndResyncWithClients.callCount, 1, 'p3 _resetFromServerAndResyncWithClients should have been called once');
- assert.equal(p3Spies._processMissingSteps.callCount, 0, 'p3 _processMissingSteps should not have been called');
- assert.equal(p3Spies._applySnapshot.callCount, 1, 'p3 _applySnapshot should have been called once');
- assert.equal(p3Spies._onRecoveryClientTimeout.callCount, 0, 'p3 _onRecoveryClientTimeout should not have been called');
- assert.equal(p3Spies._resetFromClient.callCount, 1, 'p3 _resetFromClient should have been called once');
- unpatch(peers.p2.wysiwyg, 'test');
- unpatch(peers.p3.wysiwyg, 'test');
- removePeers(peers);
- });
- QUnit.test('should recover from server if there is no peer connected', async (assert) => {
- assert.expect(14);
- const pool = await createPeers(['p1', 'p2']);
- const peers = pool.peers;
- await peers.p1.startEditor();
- await peers.p2.startEditor();
- await peers.p1.focus();
- await peers.p2.focus();
- await peers.p1.openDataChannel(peers.p2);
- peers.p2.setOffline();
- const p2Spies = {
- _recoverFromStaleDocument: makeSpy(),
- _resetFromServerAndResyncWithClients: makeSpy(),
- _processMissingSteps: makeSpy(),
- _applySnapshot: makeSpy(),
- _onRecoveryClientTimeout: makeSpy(),
- _resetFromClient: makeSpy(),
- };
- patch(peers.p2.wysiwyg, 'test', p2Spies);
- await peers.p1.makeStep(insert('b'));
- await peers.p1.writeToServer();
- assert.equal(peers.p1.getValue(), `<p>ab[]</p>`, 'p1 have inserted char b');
- assert.equal(peers.p2.getValue(), `[]<p>a</p>`, 'p2 should not have the same document as p1');
- peers.p1.destroyEditor();
- assert.equal(p2Spies._recoverFromStaleDocument.callCount, 0, 'p2 _recoverFromStaleDocument should not have been called');
- assert.equal(p2Spies._resetFromServerAndResyncWithClients.callCount, 0, 'p2 _resetFromServerAndResyncWithClients should not have been called');
- assert.equal(p2Spies._processMissingSteps.callCount, 0, 'p2 _processMissingSteps should not have been called');
- assert.equal(p2Spies._applySnapshot.callCount, 0, 'p2 _applySnapshot should not have been called');
- assert.equal(p2Spies._resetFromClient.callCount, 0, 'p2 _resetFromClient should not have been called');
- await peers.p2.setOnline();
- assert.equal(peers.p2.getValue(), `[]<p>ab</p>`, 'p2 should have the same document as p1');
- assert.equal(p2Spies._recoverFromStaleDocument.callCount, 1, 'p2 _recoverFromStaleDocument should have been called once');
- assert.equal(p2Spies._resetFromServerAndResyncWithClients.callCount, 1, 'p2 _resetFromServerAndResyncWithClients should have been called once');
- assert.equal(p2Spies._processMissingSteps.callCount, 0, 'p2 _processMissingSteps should not have been called');
- assert.equal(p2Spies._applySnapshot.callCount, 0, 'p2 _applySnapshot should not have been called');
- assert.equal(p2Spies._onRecoveryClientTimeout.callCount, 0, 'p2 _onRecoveryClientTimeout should not have been called');
- assert.equal(p2Spies._resetFromClient.callCount, 0, 'p2 _resetFromClient should not have been called');
- unpatch(peers.p2.wysiwyg, 'test');
- removePeers(peers);
- });
- QUnit.test('should recover from server if there is no response after PTP_MAX_RECOVERY_TIME', async (assert) => {
- assert.expect(16);
- const pool = await createPeers(['p1', 'p2', 'p3']);
- const peers = pool.peers;
- await peers.p1.startEditor();
- await peers.p2.startEditor();
- await peers.p3.startEditor();
- await peers.p1.focus();
- await peers.p2.focus();
- await peers.p1.openDataChannel(peers.p2);
- await peers.p1.openDataChannel(peers.p3);
- await peers.p2.openDataChannel(peers.p3);
- peers.p2.setOffline();
- peers.p3.setOffline();
- const p2Spies = {
- _recoverFromStaleDocument: makeSpy(),
- _resetFromServerAndResyncWithClients: makeSpy(),
- _processMissingSteps: makeSpy(),
- _applySnapshot: makeSpy(),
- _onRecoveryClientTimeout: makeSpy(),
- _resetFromClient: makeSpy(),
- };
- patch(peers.p2.wysiwyg, 'test', p2Spies);
- await peers.p1.makeStep(insert('b'));
- await peers.p1.writeToServer();
- peers.p1.setOffline();
- assert.equal(peers.p1.getValue(), `<p>ab[]</p>`, 'p1 have inserted char b');
- assert.equal(peers.p2.getValue(), `[]<p>a</p>`, 'p2 should not have the same document as p1');
- assert.equal(peers.p3.getValue(), `[]<p>a</p>`, 'p3 should not have the same document as p1');
- assert.equal(p2Spies._recoverFromStaleDocument.callCount, 0, 'p2 _recoverFromStaleDocument should not have been called');
- assert.equal(p2Spies._resetFromServerAndResyncWithClients.callCount, 0, 'p2 _resetFromServerAndResyncWithClients should not have been called');
- assert.equal(p2Spies._processMissingSteps.callCount, 0, 'p2 _processMissingSteps should not have been called');
- assert.equal(p2Spies._applySnapshot.callCount, 0, 'p2 _applySnapshot should not have been called');
- assert.equal(p2Spies._resetFromClient.callCount, 0, 'p2 _resetFromClient should not have been called');
- await peers.p2.setOnline();
- assert.equal(peers.p2.getValue(), `[]<p>ab</p>`, 'p2 should have the same document as p1');
- assert.equal(peers.p3.getValue(), `<p>[]a</p>`, 'p3 should not have the same document as p1');
- assert.equal(p2Spies._recoverFromStaleDocument.callCount, 1, 'p2 _recoverFromStaleDocument should have been called once');
- assert.equal(p2Spies._resetFromServerAndResyncWithClients.callCount, 1, 'p2 _resetFromServerAndResyncWithClients should have been called once');
- assert.equal(p2Spies._processMissingSteps.callCount, 0, 'p2 _processMissingSteps should not have been called');
- assert.equal(p2Spies._applySnapshot.callCount, 0, 'p2 _applySnapshot should not have been called');
- assert.equal(p2Spies._onRecoveryClientTimeout.callCount, 1, 'p2 _onRecoveryClientTimeout should have been called once');
- // p1 and p3 are considered offline but not
- // disconnected. It means that p2 will try to recover
- // from p1 and p3 even if they are currently
- // unavailable. This test is usefull to check that the
- // code path to _resetFromClient is properly taken.
- assert.equal(p2Spies._resetFromClient.callCount, 2, 'p2 _resetFromClient should have been called twice');
- unpatch(peers.p2.wysiwyg, 'test');
- removePeers(peers);
- });
- });
- });
- });
- QUnit.module('Disconnect & reconnect', {}, () => {
- QUnit.test('should sync history when disconnecting and reconnecting to internet', async (assert) => {
- assert.expect(2);
- const pool = await createPeers(['p1', 'p2']);
- const peers = pool.peers;
- await peers.p1.startEditor();
- await peers.p2.startEditor();
- await peers.p1.focus();
- await peers.p2.focus();
- await peers.p1.openDataChannel(peers.p2);
- await peers.p1.makeStep(insert('b'));
- await peers.p1.setOffline();
- peers.p1.removeDataChannel(peers.p2);
- const setSelection = (peer) => {
- const selection = peer.document.getSelection();
- const pElement = peer.wysiwyg.odooEditor.editable.querySelector('p')
- const range = new Range();
- range.setStart(pElement, 1);
- range.setEnd(pElement, 1);
- selection.removeAllRanges();
- selection.addRange(range);
- }
- const addP = (peer, content) => {
- const p = document.createElement('p');
- p.textContent = content;
- peer.wysiwyg.odooEditor.editable.append(p);
- peer.wysiwyg.odooEditor.historyStep();
- }
- setSelection(peers.p1);
- await peers.p1.makeStep(insert('c'));
- addP(peers.p1, 'd');
- setSelection(peers.p2);
- await peers.p2.makeStep(insert('e'));
- addP(peers.p2, 'f');
- peers.p1.setOnline();
- peers.p2.setOnline();
- // todo: p1PromiseForMissingStep and p2PromiseForMissingStep
- // should be removed when the fix of undetected missing step
- // will be merged. (task-3208277)
- const p1PromiseForMissingStep = new Promise((resolve) => {
- patch(peers.p2.wysiwyg, 'missingSteps', {
- async _processMissingSteps() {
- const _super = this._super;
- // Wait for the p2PromiseForMissingStep to resolve
- // to avoid undetected missing step.
- await p2PromiseForMissingStep;
- _super(...arguments);
- resolve();
- }
- })
- });
- const p2PromiseForMissingStep = new Promise((resolve) => {
- patch(peers.p1.wysiwyg, 'missingSteps', {
- async _processMissingSteps() {
- this._super(...arguments);
- resolve();
- }
- })
- });
- await peers.p1.openDataChannel(peers.p2);
- await p1PromiseForMissingStep;
- assert.equal(peers.p1.getValue(), `<p>ac[]eb</p><p>d</p><p>f</p>`, 'p1 should have the value merged with p2');
- assert.equal(peers.p2.getValue(), `<p>ace[]b</p><p>d</p><p>f</p>`, 'p2 should have the value merged with p1');
- removePeers(peers);
- });
- });
- });
- });
|