[IMP] mail: convert timer utils into model

Better integration with the rest of the code.

Task-2831082

closes odoo/odoo#90726

Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
This commit is contained in:
Alexandre Kühn
2022-05-16 16:24:47 +02:00
parent 9c5bae4064
commit c53ddf00e2
10 changed files with 269 additions and 603 deletions
@@ -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.
@@ -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);
@@ -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,
}),
},
});
+4
View File
@@ -456,6 +456,10 @@ registerModel({
nameOrDisplayName: attr({
compute: '_computeNameOrDisplayName',
}),
otherMemberLongTypingInThreadTimers: many('OtherMemberLongTypingInThreadTimer', {
inverse: 'partner',
isCausal: true,
}),
rtcSessions: many('RtcSession', {
inverse: 'partner',
}),
+34 -84
View File
@@ -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
+109
View File
@@ -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,
}),
},
});
-162
View File
@@ -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;
@@ -34,5 +34,13 @@ registerModel({
inverse: 'qunitTest',
isCausal: true,
}),
timer1: one('Timer', {
inverse: 'qunitTestOwner1',
isCausal: true,
}),
timer2: one('Timer', {
inverse: 'qunitTestOwner2',
isCausal: true,
}),
},
});
@@ -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();
},
});
@@ -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"
);
});