diff --git a/addons/mail/static/src/model/model_field_command.js b/addons/mail/static/src/model/model_field_command.js index 8ca47ff8802..f119140c8be 100644 --- a/addons/mail/static/src/model/model_field_command.js +++ b/addons/mail/static/src/model/model_field_command.js @@ -107,6 +107,20 @@ function insertAndReplace(data = {}) { return new FieldCommand('insert-and-replace', data); } +/** + * Returns a insert-and-unlink command to give to the model manager at create/update. + * `insertAndUnlink` command can be used for relation fields. + * - Create new record(s) from data if the record(s) do not exist; + * - or update the record(s) if they can be found from identifying data; + * - and then unlink the record(s) from the relation field (if they were present). + * + * @param {Object|Object[]} [data={}] - data object or data objects array to insert and unlink record(s). + * @returns {FieldCommand} + */ +export function insertAndUnlink(data = {}) { + return new FieldCommand('insert-and-unlink', data); +} + /** * Returns a link command to give to the model manager at create/update. * `link` command can be used for relation fields. diff --git a/addons/mail/static/src/models/composer_view.js b/addons/mail/static/src/models/composer_view.js index 11d2b0f2f3e..10b1a3459c6 100644 --- a/addons/mail/static/src/models/composer_view.js +++ b/addons/mail/static/src/models/composer_view.js @@ -560,7 +560,7 @@ registerModel({ return; } } - if (this.messaging.currentPartner) { + if (this.messaging.currentPartner) { // not supported for guest composer.thread.unregisterCurrentPartnerIsTyping({ immediateNotify: true }); } const escapedAndCompactContent = escapeAndCompactTextContent(composer.textInputContent); diff --git a/addons/mail/static/src/models/other_member_long_typing_in_thread_timer.js b/addons/mail/static/src/models/other_member_long_typing_in_thread_timer.js new file mode 100644 index 00000000000..45af67e5f39 --- /dev/null +++ b/addons/mail/static/src/models/other_member_long_typing_in_thread_timer.js @@ -0,0 +1,36 @@ +/** @odoo-module **/ + +import { registerModel } from '@mail/model/model_core'; +import { one } from '@mail/model/model_field'; +import { insertAndReplace } from '@mail/model/model_field_command'; + +registerModel({ + name: 'OtherMemberLongTypingInThreadTimer', + identifyingFields: [ + 'partner', + 'thread', + ], + recordMethods: { + onOtherMemberLongTypingTimeout() { + this.thread.unregisterOtherMemberTypingMember(this.partner); + }, + }, + fields: { + partner: one('Partner', { + inverse: 'otherMemberLongTypingInThreadTimers', + readonly: true, + required: true, + }), + thread: one('Thread', { + inverse: 'otherMembersLongTypingTimers', + readonly: true, + required: true, + }), + timer: one('Timer', { + default: insertAndReplace(), + inverse: 'otherMemberLongTypingInThreadTimerOwner', + isCausal: true, + required: true, + }), + }, +}); diff --git a/addons/mail/static/src/models/partner.js b/addons/mail/static/src/models/partner.js index d0dddff595b..2e346a094dd 100644 --- a/addons/mail/static/src/models/partner.js +++ b/addons/mail/static/src/models/partner.js @@ -456,6 +456,10 @@ registerModel({ nameOrDisplayName: attr({ compute: '_computeNameOrDisplayName', }), + otherMemberLongTypingInThreadTimers: many('OtherMemberLongTypingInThreadTimer', { + inverse: 'partner', + isCausal: true, + }), rtcSessions: many('RtcSession', { inverse: 'partner', }), diff --git a/addons/mail/static/src/models/thread.js b/addons/mail/static/src/models/thread.js index f8f98451ce1..22f009db7d9 100644 --- a/addons/mail/static/src/models/thread.js +++ b/addons/mail/static/src/models/thread.js @@ -2,10 +2,9 @@ import { registerModel } from '@mail/model/model_core'; import { attr, many, one } from '@mail/model/model_field'; -import { clear, insert, insertAndReplace, link, replace, unlink } from '@mail/model/model_field_command'; +import { clear, insert, insertAndReplace, insertAndUnlink, link, replace, unlink } from '@mail/model/model_field_command'; import { OnChange } from '@mail/model/model_onchange'; import throttle from '@mail/utils/throttle'; -import Timer from '@mail/utils/timer'; import { cleanSearchTerm } from '@mail/utils/utils'; import * as mailUtils from '@mail/js/utils'; @@ -27,12 +26,7 @@ registerModel({ identifyingFields: ['model', 'id'], lifecycleHooks: { _willDelete() { - this.currentPartnerInactiveTypingTimer.clear(); - this.currentPartnerLongTypingTimer.clear(); this.throttleNotifyCurrentPartnerTypingStatus.clear(); - for (const timer of this.otherMembersLongTypingTimers.values()) { - timer.clear(); - } if (this.isTemporary) { for (const message of this.messages) { message.delete(); @@ -888,7 +882,7 @@ registerModel({ * Refresh the typing status of the current partner. */ refreshCurrentPartnerIsTyping() { - this.currentPartnerInactiveTypingTimer.reset(); + this.update({ currentPartnerInactiveTypingTimer: [clear(), insertAndReplace()] }); }, /** * Called to refresh a registered other member partner that is typing @@ -897,7 +891,12 @@ registerModel({ * @param {Partner} partner */ refreshOtherMemberTypingMember(partner) { - this.otherMembersLongTypingTimers.get(partner).reset(); + this.update({ + otherMembersLongTypingTimers: [ + insertAndUnlink({ partner: replace(partner) }), + insert({ partner: replace(partner) }), + ], + }); }, /** * Called when current partner is inserting some input in composer. @@ -906,8 +905,10 @@ registerModel({ */ async registerCurrentPartnerIsTyping() { // Handling of typing timers. - this.currentPartnerInactiveTypingTimer.start(); - this.currentPartnerLongTypingTimer.start(); + this.update({ + currentPartnerInactiveTypingTimer: insertAndReplace(), + currentPartnerLongTypingTimer: insertAndReplace(), + }); // Manage typing member relation. const currentPartner = this.messaging.currentPartner; const newOrderedTypingMemberLocalIds = this.orderedTypingMemberLocalIds @@ -927,13 +928,7 @@ registerModel({ * @param {Partner} partner */ registerOtherMemberTypingMember(partner) { - const timer = new Timer( - this.messaging, - () => this._onOtherMemberLongTypingTimeout(partner), - 60 * 1000 - ); - this.otherMembersLongTypingTimers.set(partner, timer); - timer.start(); + this.update({ otherMembersLongTypingTimers: insert({ partner: replace(partner) }) }); const newOrderedTypingMemberLocalIds = this.orderedTypingMemberLocalIds .filter(localId => localId !== partner.localId); newOrderedTypingMemberLocalIds.push(partner.localId); @@ -999,10 +994,12 @@ registerModel({ * status of current partner is immediately notified and doesn't * consume throttling at all. */ - async unregisterCurrentPartnerIsTyping({ immediateNotify = false } = {}) { + unregisterCurrentPartnerIsTyping({ immediateNotify = false } = {}) { // Handling of typing timers. - this.currentPartnerInactiveTypingTimer.clear(); - this.currentPartnerLongTypingTimer.clear(); + this.update({ + currentPartnerInactiveTypingTimer: clear(), + currentPartnerLongTypingTimer: clear(), + }); // Manage typing member relation. const currentPartner = this.messaging.currentPartner; const newOrderedTypingMemberLocalIds = this.orderedTypingMemberLocalIds @@ -1015,7 +1012,7 @@ registerModel({ if (immediateNotify) { this.throttleNotifyCurrentPartnerTypingStatus.clear(); } - await this.throttleNotifyCurrentPartnerTypingStatus({ isTyping: false }); + this.throttleNotifyCurrentPartnerTypingStatus({ isTyping: false }); }, /** * Called to unregister an other member partner that is no longer typing @@ -1024,8 +1021,7 @@ registerModel({ * @param {Partner} partner */ unregisterOtherMemberTypingMember(partner) { - this.otherMembersLongTypingTimers.get(partner).clear(); - this.otherMembersLongTypingTimers.delete(partner); + this.update({ otherMembersLongTypingTimers: insertAndUnlink({ partner: replace(partner) }) }); const newOrderedTypingMemberLocalIds = this.orderedTypingMemberLocalIds .filter(localId => localId !== partner.localId); this.update({ @@ -1091,17 +1087,6 @@ registerModel({ } return clear(); }, - /** - * @private - * @returns {Timer} - */ - _computeCurrentPartnerLongTypingTimer() { - return new Timer( - this.messaging, - () => this._onCurrentPartnerLongTypingTimeout(), - 50 * 1000 - ); - }, /** * @private * @returns {FieldCommand} @@ -1274,21 +1259,6 @@ registerModel({ follower.partner && follower.partner === this.messaging.currentPartner ); }, - /** - * @private - * @returns {Timer} - */ - _computeCurrentPartnerInactiveTypingTimer() { - return new Timer( - this.messaging, - () => { - if (this.messaging.currentPartner) { - return this._onCurrentPartnerInactiveTypingTimeout(); - } - }, - 5 * 1000 - ); - }, /** * @private * @returns {boolean} @@ -1547,13 +1517,6 @@ registerModel({ .filter(member => !!member), ]]; }, - /** - * @private - * @returns {Map} - */ - _computeOtherMembersLongTypingTimers() { - return new Map(); - }, /** * @private * @returns {Activity[]} @@ -1680,8 +1643,8 @@ registerModel({ return; } } - if (isTyping && this.currentPartnerLongTypingTimer.isRunning) { - this.currentPartnerLongTypingTimer.reset(); + if (isTyping && this.currentPartnerLongTypingTimer) { + this.update({ currentPartnerLongTypingTimer: [clear(), insertAndReplace()] }); } } this.update({ @@ -1821,34 +1784,18 @@ registerModel({ }); this.update({ members }); }, - /** - * @private - */ - async _onCurrentPartnerInactiveTypingTimeout() { - await this.unregisterCurrentPartnerIsTyping(); + onCurrentPartnerInactiveTypingTimeout() { + this.unregisterCurrentPartnerIsTyping(); }, /** * Called when current partner has been typing for a very long time. * Immediately notify other members that he/she is still typing. - * - * @private */ - async _onCurrentPartnerLongTypingTimeout() { + async onCurrentPartnerLongTypingTimeout() { this.update({ forceNotifyNextCurrentPartnerTypingStatus: true }); this.throttleNotifyCurrentPartnerTypingStatus.clear(); await this.throttleNotifyCurrentPartnerTypingStatus({ isTyping: true }); }, - /** - * @private - * @param {Partner} partner - */ - async _onOtherMemberLongTypingTimeout(partner) { - if (!this.typingMembers.includes(partner)) { - this.otherMembersLongTypingTimers.delete(partner); - return; - } - this.unregisterOtherMemberTypingMember(partner); - }, }, fields: { /** @@ -1914,8 +1861,9 @@ registerModel({ * partner has stopped typing something, due to making no changes * on the composer for some time. */ - currentPartnerInactiveTypingTimer: attr({ - compute: '_computeCurrentPartnerInactiveTypingTimer', + currentPartnerInactiveTypingTimer: one('Timer', { + inverse: 'threadAsCurrentPartnerInactiveTypingTimerOwner', + isCausal: true, }), /** * Last 'is_typing' status of current partner that has been notified @@ -1941,8 +1889,9 @@ registerModel({ * is still typing something, so that they should not assume he/she * has stopped typing something. */ - currentPartnerLongTypingTimer: attr({ - compute: '_computeCurrentPartnerLongTypingTimer', + currentPartnerLongTypingTimer: one('Timer', { + inverse: 'threadAsCurrentPartnerLongTypingTimerOwner', + isCausal: true, }), custom_channel_name: attr(), /** @@ -2323,8 +2272,9 @@ registerModel({ * @see registerOtherMemberTypingMember * @see unregisterOtherMemberTypingMember */ - otherMembersLongTypingTimers: attr({ - compute: '_computeOtherMembersLongTypingTimers', + otherMembersLongTypingTimers: many('OtherMemberLongTypingInThreadTimer', { + inverse: 'thread', + isCausal: true, }), /** * States the `Activity` that belongs to `this` and that are diff --git a/addons/mail/static/src/models/timer.js b/addons/mail/static/src/models/timer.js new file mode 100644 index 00000000000..64c469f708a --- /dev/null +++ b/addons/mail/static/src/models/timer.js @@ -0,0 +1,109 @@ +/** @odoo-module **/ + +import { registerModel } from '@mail/model/model_core'; +import { attr, one } from '@mail/model/model_field'; +import { clear } from '@mail/model/model_field_command'; + +registerModel({ + name: 'Timer', + identifyingFields: [[ + 'otherMemberLongTypingInThreadTimerOwner', + 'threadAsCurrentPartnerInactiveTypingTimerOwner', + 'threadAsCurrentPartnerLongTypingTimerOwner', + ]], + lifecycleHooks: { + _willDelete() { + this.messaging.browser.clearTimeout(this.timeoutId); + }, + }, + recordMethods: { + /** + * @private + * @returns {integer|FieldCommand} + */ + _computeDuration() { + if (this.threadAsCurrentPartnerInactiveTypingTimerOwner) { + return 5 * 1000; + } + if (this.threadAsCurrentPartnerLongTypingTimerOwner) { + return 50 * 1000; + } + if (this.otherMemberLongTypingInThreadTimerOwner) { + return 60 * 1000; + } + return clear(); + }, + /** + * @private + * @returns {number} + */ + _computeTimeoutId() { + if (this.duration === undefined) { + return; // ensure duration is computed first + } + if (this.timeoutId) { + return; + } + return this.messaging.browser.setTimeout(this._onTimeout, this.duration); + }, + /** + * @private + */ + _onTimeout() { + this._onTimeoutOwner(); + if (this.exists()) { // owner might have deleted the timer itself + this.delete(); + } + }, + /** + * @private + */ + _onTimeoutOwner() { + if (this.threadAsCurrentPartnerInactiveTypingTimerOwner) { + this.threadAsCurrentPartnerInactiveTypingTimerOwner.onCurrentPartnerInactiveTypingTimeout(); + return; + } + if (this.threadAsCurrentPartnerLongTypingTimerOwner) { + this.threadAsCurrentPartnerLongTypingTimerOwner.onCurrentPartnerLongTypingTimeout(); + return; + } + if (this.otherMemberLongTypingInThreadTimerOwner) { + this.otherMemberLongTypingInThreadTimerOwner.onOtherMemberLongTypingTimeout(); + return; + } + }, + }, + fields: { + /** + * Duration, in milliseconds, until timer times out and calls the + * timeout function. + */ + duration: attr({ + compute: '_computeDuration', + readonly: true, + required: true, + }), + otherMemberLongTypingInThreadTimerOwner: one('OtherMemberLongTypingInThreadTimer', { + inverse: 'timer', + isCausal: true, + readonly: true, + }), + threadAsCurrentPartnerInactiveTypingTimerOwner: one('Thread', { + inverse: 'currentPartnerInactiveTypingTimer', + readonly: true, + }), + threadAsCurrentPartnerLongTypingTimerOwner: one('Thread', { + inverse: 'currentPartnerLongTypingTimer', + readonly: true, + }), + /** + * Internal reference of `setTimeout()` that is used to invoke function + * when timer times out. Useful to clear it when timer is cleared/reset. + */ + timeoutId: attr({ + compute: '_computeTimeoutId', + readonly: true, + required: true, + }), + }, +}); diff --git a/addons/mail/static/src/utils/timer.js b/addons/mail/static/src/utils/timer.js deleted file mode 100644 index ad7b36a1809..00000000000 --- a/addons/mail/static/src/utils/timer.js +++ /dev/null @@ -1,162 +0,0 @@ -/** @odoo-module **/ - -import { makeDeferred } from '@mail/utils/deferred'; - -//------------------------------------------------------------------------------ -// Errors -//------------------------------------------------------------------------------ - -/** - * List of Timer errors. - */ - - /** - * Error when timer has been cleared with `.clear()` or `.reset()`. Used to - * let know caller of timer that the countdown has been aborted, which - * means the inner function will not be called. Usually caller should just - * accept it and kindly treated this error as a polite warning. - */ - class TimerClearedError extends Error { - /** - * @override - */ - constructor(timerId, ...args) { - super(...args); - this.name = 'TimerClearedError'; - this.timerId = timerId; - } -} - -//------------------------------------------------------------------------------ -// Private -//------------------------------------------------------------------------------ - -/** - * This class creates a timer which, when times out, calls a function. - * Note that the timer is not started on initialization (@see start method). - */ -class Timer { - - /** - * @param {Messaging} messaging the messaging record. - * @param {function} onTimeout - * @param {integer} duration - * @param {Object} [param3={}] - * @param {boolean} [param3.silentCancelationErrors=true] if unset, caller - * of timer will observe some errors that come from current timer calls - * that has been cleared with `.clear()` or `.reset()`. - * @see TimerClearedError for when timer has been aborted from `.clear()` - * or `.reset()`. - */ - constructor(messaging, onTimeout, duration, { silentCancelationErrors = true } = {}) { - this.messaging = messaging; - /** - * Determine whether the timer has a pending timeout. - */ - this.isRunning = false; - /** - * Duration, in milliseconds, until timer times out and calls the - * timeout function. - */ - this._duration = duration; - /** - * Determine whether the caller of timer `.start()` and `.reset()` - * should observe cancelation errors from `.clear()` or `.reset()`. - */ - this._hasSilentCancelationErrors = silentCancelationErrors; - /** - * The function that is called when the timer times out. - */ - this._onTimeout = onTimeout; - /** - * Deferred of a currently pending invocation to inner function on - * timeout. - */ - this._timeoutDeferred = undefined; - /** - * Internal reference of `setTimeout()` that is used to invoke function - * when timer times out. Useful to clear it when timer is cleared/reset. - */ - this._timeoutId = undefined; - } - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * Clear the timer, which basically sets the state of timer as if it was - * just instantiated, without being started. This function makes sense only - * when this timer is running. - */ - clear() { - this.messaging.browser.clearTimeout(this._timeoutId); - this.isRunning = false; - if (!this._timeoutDeferred) { - return; - } - this._timeoutDeferred.reject(new TimerClearedError(this.id)); - } - - /** - * Reset the timer, i.e. the pending timeout is refreshed with initial - * duration. This function makes sense only when this timer is running. - */ - async reset() { - this.clear(); - await this.start(); - } - - /** - * Starts the timer, i.e. after a certain duration, it times out and calls - * a function back. This function makes sense only when this timer is not - * yet running. - * - * @throws {Error} in case the timer is already running. - */ - async start() { - if (this.isRunning) { - throw new Error("Cannot start a timer that is currently running."); - } - this.isRunning = true; - const timeoutDeferred = makeDeferred(); - this._timeoutDeferred = timeoutDeferred; - const timeoutId = this.messaging.browser.setTimeout( - () => { - this.isRunning = false; - timeoutDeferred.resolve(this._onTimeout()); - }, - this._duration - ); - this._timeoutId = timeoutId; - let result; - try { - result = await timeoutDeferred; - } catch (error) { - if ( - !this._hasSilentCancelationErrors || - !(error instanceof TimerClearedError) || - error.timerId !== this.id - ) { - // This branching should never happens. - // Still defined in case of programming error. - throw error; - } - } finally { - this.messaging.browser.clearTimeout(timeoutId); - this._timeoutDeferred = undefined; - this.isRunning = false; - } - return result; - } - -} - -/** - * Make external timer errors accessible from timer class. - */ -Object.assign(Timer, { - TimerClearedError, -}); - -export default Timer; diff --git a/addons/mail/static/tests/models/qunit_test.js b/addons/mail/static/tests/models/qunit_test.js index 107568cf39b..290e1ee60a6 100644 --- a/addons/mail/static/tests/models/qunit_test.js +++ b/addons/mail/static/tests/models/qunit_test.js @@ -34,5 +34,13 @@ registerModel({ inverse: 'qunitTest', isCausal: true, }), + timer1: one('Timer', { + inverse: 'qunitTestOwner1', + isCausal: true, + }), + timer2: one('Timer', { + inverse: 'qunitTestOwner2', + isCausal: true, + }), }, }); diff --git a/addons/mail/static/tests/models/timer_qunit_tests.js b/addons/mail/static/tests/models/timer_qunit_tests.js new file mode 100644 index 00000000000..2d3043d2dc6 --- /dev/null +++ b/addons/mail/static/tests/models/timer_qunit_tests.js @@ -0,0 +1,34 @@ +/** @odoo-module **/ + +import { addFields, patchIdentifyingFields, patchRecordMethods } from '@mail/model/model_core'; +import { one } from '@mail/model/model_field'; +// ensure that the model definition is loaded before the patch +import '@mail/models/timer'; + +addFields('Timer', { + qunitTestOwner1: one('QUnitTest', { + inverse: 'timer1', + readonly: true, + }), + qunitTestOwner2: one('QUnitTest', { + inverse: 'timer2', + readonly: true, + }), +}); + +patchIdentifyingFields('Timer', identifyingFields => { + identifyingFields[0].push('qunitTestOwner1'); + identifyingFields[0].push('qunitTestOwner2'); +}); + +patchRecordMethods('Timer', { + _computeDuration() { + if (this.qunitTestOwner1) { + return 0; + } + if (this.qunitTestOwner2) { + return 1000 * 1000; + } + return this._super(); + }, +}); diff --git a/addons/mail/static/tests/qunit_suite_tests/utils/timer_tests.js b/addons/mail/static/tests/qunit_suite_tests/utils/timer_tests.js index 9e389ef2802..6077ccb0161 100644 --- a/addons/mail/static/tests/qunit_suite_tests/utils/timer_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/utils/timer_tests.js @@ -1,393 +1,66 @@ /** @odoo-module **/ -import { nextTick, start } from '@mail/../tests/helpers/test_utils'; -import Timer from '@mail/utils/timer'; - -const { TimerClearedError } = Timer; +import { insertAndReplace } from '@mail/model/model_field_command'; +import { start } from '@mail/../tests/helpers/test_utils'; QUnit.module('mail', {}, function () { QUnit.module('utils', {}, function () { QUnit.module('timer', {}, function () { -QUnit.module('timer_tests.js', { - beforeEach() { - this.timers = []; - }, - afterEach() { - // Important: tests should cleanly intercept cancelation errors that - // may result from this teardown. - for (const timer of this.timers) { - timer.clear(); - } - }, -}); +QUnit.module('timer_tests.js', {}); -QUnit.test('timer does not timeout on initialization', async function (assert) { - assert.expect(3); - - const { advanceTime, messaging } = await start({ hasTimeControl: true }); - - let hasTimedOut = false; - this.timers.push( - new Timer( - messaging, - () => hasTimedOut = true, - 0 - ) - ); - - assert.notOk( - hasTimedOut, - "timer should not have timed out on immediate initialization" - ); - - await advanceTime(0); - assert.notOk( - hasTimedOut, - "timer should not have timed out from initialization after 0ms" - ); - - await advanceTime(1000 * 1000); - assert.notOk( - hasTimedOut, - "timer should not have timed out from initialization after 1000s" - ); -}); - -QUnit.test('timer start (duration: 0ms)', async function (assert) { +QUnit.test('timer insert (duration: 0ms)', async function (assert) { assert.expect(2); const { advanceTime, messaging } = await start({ hasTimeControl: true }); - - let hasTimedOut = false; - this.timers.push( - new Timer( - messaging, - () => hasTimedOut = true, - 0 - ) - ); - - this.timers[0].start(); - assert.notOk( - hasTimedOut, - "timer should not have timed out immediately after start" + const timer = messaging.models['Timer'].insert({ + qunitTestOwner1: insertAndReplace(), + }); + assert.ok( + timer.exists(), + "timer should not have timed out immediately after insert" ); await advanceTime(0); - assert.ok( - hasTimedOut, - "timer should have timed out on start after 0ms" - ); -}); - -QUnit.test('timer start observe termination (duration: 0ms)', async function (assert) { - assert.expect(6); - - const { advanceTime, messaging } = await start({ hasTimeControl: true }); - - let hasTimedOut = false; - this.timers.push( - new Timer( - messaging, - () => { - hasTimedOut = true; - return 'timeout_result'; - }, - 0 - ) - ); - - this.timers[0].start() - .then(result => { - assert.strictEqual( - result, - 'timeout_result', - "value returned by start should be value returned by function on timeout" - ); - assert.step('timeout'); - }); - await nextTick(); assert.notOk( - hasTimedOut, - "timer should not have timed out immediately after start" - ); - assert.verifySteps( - [], - "timer.start() should not have yet observed timeout" - ); - - await advanceTime(0); - assert.ok( - hasTimedOut, - "timer should have timed out on start after 0ms" - ); - assert.verifySteps( - ['timeout'], - "timer.start() should have observed timeout after 0ms" + timer.exists(), + "timer should have timed out on insert after 0ms" ); }); -QUnit.test('timer start (duration: 1000s)', async function (assert) { +QUnit.test('timer insert (duration: 1000s)', async function (assert) { assert.expect(5); const { advanceTime, messaging } = await start({ hasTimeControl: true }); - - let hasTimedOut = false; - this.timers.push( - new Timer( - messaging, - () => hasTimedOut = true, - 1000 * 1000 - ) - ); - - this.timers[0].start(); - assert.notOk( - hasTimedOut, - "timer should not have timed out immediately after start" + const timer = messaging.models['Timer'].insert({ + qunitTestOwner2: insertAndReplace(), + }); + assert.ok( + timer.exists(), + "timer should not have timed out immediately after insert" ); await advanceTime(0); - assert.notOk( - hasTimedOut, - "timer should not have timed out on start after 0ms" + assert.ok( + timer.exists(), + "timer should not have timed out on insert after 0ms" ); await advanceTime(1000); - assert.notOk( - hasTimedOut, - "timer should not have timed out on start after 1000ms" + assert.ok( + timer.exists(), + "timer should not have timed out on insert after 1000ms" ); await advanceTime(998 * 1000 + 999); - assert.notOk( - hasTimedOut, - "timer should not have timed out on start after 9999ms" - ); - - await advanceTime(1); assert.ok( - hasTimedOut, - "timer should have timed out on start after 10s" - ); -}); - -QUnit.test('[no cancelation intercept] timer start then immediate clear (duration: 0ms)', async function (assert) { - assert.expect(4); - - const { advanceTime, messaging } = await start({ hasTimeControl: true }); - - let hasTimedOut = false; - this.timers.push( - new Timer( - messaging, - () => hasTimedOut = true, - 0 - ) - ); - - this.timers[0].start(); - assert.notOk( - hasTimedOut, - "timer should not have timed out immediately after start" - ); - - this.timers[0].clear(); - assert.notOk( - hasTimedOut, - "timer should not have timed out immediately after start and clear" - ); - - await advanceTime(0); - assert.notOk( - hasTimedOut, - "timer should not have timed out after 0ms of clear" - ); - - await advanceTime(1000); - assert.notOk( - hasTimedOut, - "timer should not have timed out after 1s of clear" - ); -}); - -QUnit.test('[no cancelation intercept] timer start then clear before timeout (duration: 1000ms)', async function (assert) { - assert.expect(4); - - const { advanceTime, messaging } = await start({ hasTimeControl: true }); - - let hasTimedOut = false; - this.timers.push( - new Timer( - messaging, - () => hasTimedOut = true, - 1000 - ) - ); - - this.timers[0].start(); - assert.notOk( - hasTimedOut, - "timer should not have timed out immediately after start" - ); - - await advanceTime(999); - assert.notOk( - hasTimedOut, - "timer should not have timed out immediately after 999ms of start" - ); - - this.timers[0].clear(); - await advanceTime(1); - assert.notOk( - hasTimedOut, - "timer should not have timed out after 1ms of clear that happens 999ms after start (globally 1s await)" - ); - - await advanceTime(1000); - assert.notOk( - hasTimedOut, - "timer should not have timed out after 1001ms after clear (timer fully cleared)" - ); -}); - -QUnit.test('[no cancelation intercept] timer start then reset before timeout (duration: 1000ms)', async function (assert) { - assert.expect(5); - - const { advanceTime, messaging } = await start({ hasTimeControl: true }); - - let hasTimedOut = false; - this.timers.push( - new Timer( - messaging, - () => hasTimedOut = true, - 1000 - ) - ); - - this.timers[0].start(); - assert.notOk( - hasTimedOut, - "timer should not have timed out immediately after start" - ); - - await advanceTime(999); - assert.notOk( - hasTimedOut, - "timer should not have timed out after 999ms of start" - ); - - this.timers[0].reset(); - await advanceTime(1); - assert.notOk( - hasTimedOut, - "timer should not have timed out after 1ms of reset which happens 999ms after start" - ); - - await advanceTime(998); - assert.notOk( - hasTimedOut, - "timer should not have timed out after 999ms of reset" + timer.exists(), + "timer should not have timed out on insert after 9999ms" ); await advanceTime(1); - assert.ok( - hasTimedOut, - "timer should not have timed out after 1s of reset" - ); -}); - -QUnit.test('[with cancelation intercept] timer start then immediate clear (duration: 0ms)', async function (assert) { - assert.expect(5); - - const { messaging } = await start({ hasTimeControl: true }); - - let hasTimedOut = false; - this.timers.push( - new Timer( - messaging, - () => hasTimedOut = true, - 0, - { silentCancelationErrors: false } - ) - ); - - this.timers[0].start() - .then(() => { - throw new Error("timer.start() should not be resolved (should have been canceled by clear)"); - }) - .catch(error => { - assert.ok( - error instanceof TimerClearedError, - "Should generate a Timer cleared error (from `.clear()`)" - ); - assert.step('timer_cleared'); - }); assert.notOk( - hasTimedOut, - "timer should not have timed out immediately after start" - ); - await nextTick(); - assert.verifySteps([], "should not have observed cleared timer (timer not yet cleared)"); - - this.timers[0].clear(); - await nextTick(); - assert.verifySteps( - ['timer_cleared'], - "timer.start() should have observed it has been cleared" - ); -}); - -QUnit.test('[with cancelation intercept] timer start then immediate reset (duration: 0ms)', async function (assert) { - assert.expect(9); - - const { advanceTime, messaging } = await start({ hasTimeControl: true }); - - let hasTimedOut = false; - this.timers.push( - new Timer( - messaging, - () => hasTimedOut = true, - 0, - { silentCancelationErrors: false } - ) - ); - - this.timers[0].start() - .then(() => { - throw new Error("timer.start() should not observe a timeout"); - }) - .catch(error => { - assert.ok(error instanceof TimerClearedError, "Should generate a Timer cleared error (from `.reset()`)"); - assert.step('timer_cleared'); - }); - assert.notOk( - hasTimedOut, - "timer should not have timed out immediately after start" - ); - await nextTick(); - assert.verifySteps([], "should not have observed cleared timer (timer not yet cleared)"); - - this.timers[0].reset() - .then(() => assert.step('timer_reset_timeout')); - await nextTick(); - assert.verifySteps( - ['timer_cleared'], - "timer.start() should have observed it has been cleared" - ); - assert.notOk( - hasTimedOut, - "timer should not have timed out immediately after reset" - ); - - await advanceTime(0); - assert.ok( - hasTimedOut, - "timer should have timed out after reset timeout" - ); - assert.verifySteps( - ['timer_reset_timeout'], - "timer.reset() should have observed it has timed out" + timer.exists(), + "timer should have timed out on insert after 10s" ); });