From 54370da08fca6eb42bc12045a1ce1940f8984433 Mon Sep 17 00:00:00 2001 From: EugeniyKiyashko Date: Tue, 5 Nov 2024 16:57:17 +0400 Subject: [PATCH] Chat: partial support for dataSource operations 'update' and 'remove' --- .../devextreme/js/__internal/ui/chat/chat.ts | 14 +- .../js/__internal/ui/chat/messagebubble.ts | 5 +- .../js/__internal/ui/chat/messagegroup.ts | 8 +- .../js/__internal/ui/chat/messagelist.ts | 125 +++++- .../chatParts/chat.tests.js | 414 +++++++++++++++++- 5 files changed, 518 insertions(+), 48 deletions(-) diff --git a/packages/devextreme/js/__internal/ui/chat/chat.ts b/packages/devextreme/js/__internal/ui/chat/chat.ts index b7d0681b70e2..b42d8caeac91 100644 --- a/packages/devextreme/js/__internal/ui/chat/chat.ts +++ b/packages/devextreme/js/__internal/ui/chat/chat.ts @@ -16,7 +16,6 @@ import type { } from '@js/ui/chat'; import type { OptionChanged } from '@ts/core/widget/types'; import Widget from '@ts/core/widget/widget'; -import { applyBatch } from '@ts/data/m_array_utils'; import AlertList from './alertlist'; import ChatHeader from './header'; @@ -100,15 +99,10 @@ class Chat extends Widget { if (e?.changes) { this._messageList._modifyByChanges(e.changes); - // @ts-expect-error - const dataSource = this.getDataSource(); - // @ts-expect-error - applyBatch({ - // // @ts-expect-error - keyInfo: dataSource, - data: this.option('items'), - changes: e.changes, - }); + this._setOptionWithoutOptionChange('items', newItems.slice()); + this._messageList._setOptionWithoutOptionChange('items', newItems.slice()); + + this._messageList._renderEmptyView(); } else { this.option('items', newItems.slice()); } diff --git a/packages/devextreme/js/__internal/ui/chat/messagebubble.ts b/packages/devextreme/js/__internal/ui/chat/messagebubble.ts index fed93596be15..1d54dbd684dd 100644 --- a/packages/devextreme/js/__internal/ui/chat/messagebubble.ts +++ b/packages/devextreme/js/__internal/ui/chat/messagebubble.ts @@ -7,7 +7,7 @@ import Widget from '@ts/core/widget/widget'; import type Chat from './chat'; import type { MessageTemplate } from './messagelist'; -const CHAT_MESSAGEBUBBLE_CLASS = 'dx-chat-messagebubble'; +export const CHAT_MESSAGEBUBBLE_CLASS = 'dx-chat-messagebubble'; export interface Properties extends WidgetOptions { text?: string; @@ -44,6 +44,9 @@ class MessageBubble extends Widget { const messageTemplate = this._getTemplateByOption('template'); + // @ts-expect-error + templateData.message.text = text; + messageTemplate.render({ container: this.element(), model: templateData, diff --git a/packages/devextreme/js/__internal/ui/chat/messagegroup.ts b/packages/devextreme/js/__internal/ui/chat/messagegroup.ts index 89704fe40f88..1ed8a2ed4b83 100644 --- a/packages/devextreme/js/__internal/ui/chat/messagegroup.ts +++ b/packages/devextreme/js/__internal/ui/chat/messagegroup.ts @@ -15,7 +15,9 @@ import type Chat from './chat'; import MessageBubble from './messagebubble'; import type { MessageTemplate } from './messagelist'; -const CHAT_MESSAGEGROUP_CLASS = 'dx-chat-messagegroup'; +export const MESSAGE_DATA_KEY = 'dxMessageData'; + +export const CHAT_MESSAGEGROUP_CLASS = 'dx-chat-messagegroup'; export const CHAT_MESSAGEGROUP_ALIGNMENT_START_CLASS = 'dx-chat-messagegroup-alignment-start'; export const CHAT_MESSAGEGROUP_ALIGNMENT_END_CLASS = 'dx-chat-messagegroup-alignment-end'; const CHAT_MESSAGEGROUP_INFORMATION_CLASS = 'dx-chat-messagegroup-information'; @@ -111,7 +113,9 @@ class MessageGroup extends Widget { } _renderMessageBubble(message: Message): void { - const $bubble = $('
'); + const $bubble = $('
') + .data(MESSAGE_DATA_KEY, message); + const { messageTemplate, messageTemplateData } = this.option(); this._createComponent($bubble, MessageBubble, { diff --git a/packages/devextreme/js/__internal/ui/chat/messagelist.ts b/packages/devextreme/js/__internal/ui/chat/messagelist.ts index 01f07c390cc8..4bba1b567177 100644 --- a/packages/devextreme/js/__internal/ui/chat/messagelist.ts +++ b/packages/devextreme/js/__internal/ui/chat/messagelist.ts @@ -25,10 +25,13 @@ import { getScrollTopMax } from '@ts/ui/scroll_view/utils/get_scroll_top_max'; import { isElementVisible } from '../splitter/utils/layout'; import type Chat from './chat'; +import MessageBubble, { CHAT_MESSAGEBUBBLE_CLASS } from './messagebubble'; import type { MessageGroupAlignment } from './messagegroup'; import MessageGroup, { CHAT_MESSAGEGROUP_ALIGNMENT_END_CLASS, CHAT_MESSAGEGROUP_ALIGNMENT_START_CLASS, + CHAT_MESSAGEGROUP_CLASS, + MESSAGE_DATA_KEY, } from './messagegroup'; import TypingIndicator from './typingindicator'; @@ -52,7 +55,7 @@ export const MESSAGEGROUP_TIMEOUT = 5 * 1000 * 60; export interface Change { type: 'insert' | 'update' | 'remove'; data?: DeepPartial; - key?: unknown; + key?: string | number; index?: number; } @@ -77,8 +80,6 @@ export interface Properties extends WidgetOptions { } class MessageList extends Widget { - private _messageGroups?: MessageGroup[]; - private _lastMessageDate?: null | string | number | Date; private _containerClientHeight!: number; @@ -112,7 +113,6 @@ class MessageList extends Widget { _init(): void { super._init(); - this._messageGroups = []; this._lastMessageDate = null; } @@ -206,13 +206,6 @@ class MessageList extends Widget { }); } - _removeEmptyView(): void { - this.$element() - .removeClass(CHAT_MESSAGELIST_EMPTY_CLASS) - .removeClass(CHAT_MESSAGELIST_EMPTY_LOADING_CLASS); - this._$content.empty(); - } - _isEmpty(): boolean { const { items } = this.option(); @@ -241,7 +234,7 @@ class MessageList extends Widget { const $messageGroup = $('
').appendTo(this._$content); - const messageGroup = this._createComponent($messageGroup, MessageGroup, { + this._createComponent($messageGroup, MessageGroup, { items, alignment: this._messageGroupAlignment(userId), showAvatar, @@ -251,8 +244,6 @@ class MessageList extends Widget { messageTemplateData, messageTimestampFormat, }); - - this._messageGroups?.push(messageGroup); } _renderScrollView(): void { @@ -325,6 +316,8 @@ class MessageList extends Widget { } _renderEmptyView(): void { + this._getEmptyView().remove(); + const { isLoading } = this.option(); this.$element() @@ -400,10 +393,24 @@ class MessageList extends Widget { $lastAlignmentEndGroup.addClass(CHAT_LAST_MESSAGEGROUP_ALIGNMENT_END_CLASS); } + _getLastMessageGroup(): MessageGroup | undefined { + const $lastMessageGroup = this._$content.find(`.${CHAT_MESSAGEGROUP_CLASS}`); + + if ($lastMessageGroup.length) { + return this._getMessageGroupInstanceByElement($lastMessageGroup); + } + + return undefined; + } + + _getMessageGroupInstanceByElement($element: dxElementWrapper): MessageGroup { + return MessageGroup.getInstance($element) as MessageGroup; + } + _renderMessage(message: Message): void { const { author, timestamp } = message; - const lastMessageGroup = this._messageGroups?.at(-1); + const lastMessageGroup = this._getLastMessageGroup(); const shouldCreateDayHeader = this._shouldAddDayHeader(timestamp); if (lastMessageGroup) { @@ -431,6 +438,80 @@ class MessageList extends Widget { this._scrollDownContent(); } + _getMessageData(message: Element): Message { + // @ts-expect-error + return $(message).data(MESSAGE_DATA_KEY); + } + + _findMessageElementByKey(key: string | number): dxElementWrapper { + const $bubbles = this.$element().find(`.${CHAT_MESSAGEBUBBLE_CLASS}`); + + let result = $(); + + $bubbles.each((_, item) => { + const messageData = this._getMessageData(item); + + if (messageData.id === key) { + result = $(item); + return false; + } + + return true; + }); + + return result; + } + + _updateMessageByKey(key: string | number | undefined, data: Message): void { + if (key) { + const $targetMessage = this._findMessageElementByKey(key); + + const bubble = MessageBubble.getInstance($targetMessage); + bubble.option('text', data.text); + } + } + + _removeMessageByKey(key: string | number | undefined): void { + if (!key) { + return; + } + + const $targetMessage = this._findMessageElementByKey(key); + + if (!$targetMessage.length) { + return; + } + + const $currentMessageGroup = $targetMessage.closest(`.${CHAT_MESSAGEGROUP_CLASS}`); + + const group = this._getMessageGroupInstanceByElement($currentMessageGroup); + + const { items } = group.option(); + const newItems = items.filter((item) => item.id !== key); + + if (newItems.length === 0) { + const { showDayHeaders } = this.option(); + + if (showDayHeaders) { + const $prev = group.$element().prev(); + const $next = group.$element().next(); + + const shouldRemoveDayHeader = $prev.length + && $prev.hasClass(CHAT_MESSAGELIST_DAY_HEADER_CLASS) + && (($next.length && $next.hasClass(CHAT_MESSAGELIST_DAY_HEADER_CLASS)) || !$next.length); + + if (shouldRemoveDayHeader) { + $prev.remove(); + } + } + group.$element().remove(); + } else { + group.option('items', newItems); + } + + this._setLastMessageGroupClasses(); + } + _scrollDownContent(): void { this._scrollView.scrollTo({ top: getScrollTopMax(this._scrollableContainer()), @@ -468,9 +549,7 @@ class MessageList extends Widget { if (shouldItemsBeUpdatedCompletely) { this._invalidate(); } else { - if (!previousValue.length) { - this._removeEmptyView(); - } + this._renderEmptyView(); const newMessage = value[value.length - 1]; @@ -530,8 +609,11 @@ class MessageList extends Widget { return $(this._scrollView.content()); } + _getEmptyView(): dxElementWrapper { + return this._$content.find(`.${CHAT_MESSAGELIST_EMPTY_VIEW_CLASS}`); + } + _clean(): void { - this._messageGroups = []; this._lastMessageDate = null; super._clean(); @@ -541,14 +623,15 @@ class MessageList extends Widget { changes.forEach((change) => { switch (change.type) { case 'update': + this._updateMessageByKey(change.key, change.data ?? {}); break; case 'insert': { const { items } = this.option(); - this.option('items', [...items, change.data ?? {}]); break; } case 'remove': + this._removeMessageByKey(change.key); break; default: break; @@ -588,7 +671,7 @@ class MessageList extends Widget { getEmptyViewId(): string | null { if (this._isEmpty()) { - const $emptyView = this._$content.find(`.${CHAT_MESSAGELIST_EMPTY_VIEW_CLASS}`); + const $emptyView = this._getEmptyView(); const emptyViewId = $emptyView.attr('id') ?? null; return emptyViewId; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js index 712e0158fdc4..cf07ebd6c308 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js @@ -25,6 +25,10 @@ const CHAT_MESSAGEBOX_BUTTON_CLASS = 'dx-chat-messagebox-button'; const CHAT_MESSAGEBOX_TEXTAREA_CLASS = 'dx-chat-messagebox-textarea'; const CHAT_MESSAGELIST_EMPTY_VIEW_CLASS = 'dx-chat-messagelist-empty-view'; const SCROLLVIEW_REACHBOTTOM_INDICATOR = 'dx-scrollview-scrollbottom'; +const CHAT_MESSAGELIST_DAY_HEADER_CLASS = 'dx-chat-messagelist-day-header'; + +const CHAT_LAST_MESSAGEGROUP_ALIGNMENT_START_CLASS = 'dx-chat-last-messagegroup-alignment-start'; +const CHAT_LAST_MESSAGEGROUP_ALIGNMENT_END_CLASS = 'dx-chat-last-messagegroup-alignment-end'; const TEXTEDITOR_INPUT_CLASS = 'dx-texteditor-input'; @@ -33,6 +37,7 @@ const MOCK_CHAT_HEADER_TEXT = 'Chat title'; export const MOCK_COMPANION_USER_ID = 'COMPANION_USER_ID'; export const MOCK_CURRENT_USER_ID = 'CURRENT_USER_ID'; export const NOW = 1721747399083; +const MS_IN_DAY = 86400000; export const userFirst = { id: MOCK_COMPANION_USER_ID, @@ -80,9 +85,10 @@ const moduleConfig = { return this.$element.find(`.${CHAT_MESSAGELIST_EMPTY_VIEW_CLASS}`); }; - this.getBubbles = () => { - return this.$element.find(`.${CHAT_MESSAGEBUBBLE_CLASS}`); - }; + this.getMessageList = () => MessageList.getInstance(this.$element.find(`.${CHAT_MESSAGELIST_CLASS}`)); + this.getMessageGroups = () => this.$element.find(`.${CHAT_MESSAGEGROUP_CLASS}`); + this.getDayHeaders = () => this.$element.find(`.${CHAT_MESSAGELIST_DAY_HEADER_CLASS}`); + this.getBubbles = () => this.$element.find(`.${CHAT_MESSAGEBUBBLE_CLASS}`); init(); } @@ -128,13 +134,7 @@ QUnit.module('Chat', () => { }); }); - QUnit.module('MessageList integration', { - beforeEach: function() { - moduleConfig.beforeEach.apply(this, arguments); - - this.getMessageList = () => MessageList.getInstance(this.$element.find(`.${CHAT_MESSAGELIST_CLASS}`)); - } - }, () => { + QUnit.module('MessageList integration', moduleConfig, () => { QUnit.test('passed currentUserId should be equal generated chat.user.id', function(assert) { const messageList = this.getMessageList(); @@ -637,13 +637,11 @@ QUnit.module('Chat', () => { text: 'NEW MESSAGE', }; - const getMessageGroups = () => this.$element.find(`.${CHAT_MESSAGEGROUP_CLASS}`); - - assert.strictEqual(getMessageGroups().length, 0); + assert.strictEqual(this.getMessageGroups().length, 0); this.instance.renderMessage(newMessage); - assert.strictEqual(getMessageGroups().length, 1); + assert.strictEqual(this.getMessageGroups().length, 1); }); [ @@ -1044,6 +1042,7 @@ QUnit.module('Chat', () => { this.clock.tick(timeout * 2); assert.deepEqual(this.instance.option('items'), [...messages, newMessage], 'items option should contain all messages including the new one'); + assert.deepEqual(this.getMessageList().option('items'), [...messages, newMessage], 'messagelist items option should contain all messages including the new one'); assert.strictEqual(this.getBubbles().length, 3, 'new message should be rendered in list'); }); @@ -1091,10 +1090,397 @@ QUnit.module('Chat', () => { this.clock.tick(timeout * 2); assert.deepEqual(this.instance.option('items'), [...messages, newMessage], 'items option should contain all messages including the new one'); + assert.deepEqual(this.getMessageList().option('items'), [...messages, newMessage], 'messagelist items option should contain all messages including the new one'); assert.strictEqual(this.getEmptyView().length, 0, 'empty view is removed'); assert.strictEqual(this.getBubbles().length, 1, 'new message should be rendered in list'); }); + QUnit.test('message text should be updated when using store.push({ type: "update", key: "message_id", data: { text: "new text"} })', function(assert) { + const messages = [{ id: 1, text: 'message_1' }, { id: 2, text: 'message_2' }, { id: 3, text: 'message_3' }]; + const timeout = 100; + + const store = new CustomStore({ + key: 'id', + load: function() { + const d = $.Deferred(); + setTimeout(function() { + d.resolve([...messages]); + }, timeout); + return d.promise(); + }, + }); + + this.reinit({ + dataSource: store, + reloadOnChange: false, + }); + + this.clock.tick(timeout); + + const newBubbleText = 'updated text'; + store.push([{ type: 'update', key: 2, data: { text: 'updated text' } }]); + + this.clock.tick(timeout * 2); + + const expectedData = [{ id: 1, text: 'message_1' }, { id: 2, text: newBubbleText }, { id: 3, text: 'message_3' }]; + + assert.deepEqual(this.instance.option('items'), expectedData, 'items option should contain the same count of messages after update'); + assert.deepEqual(this.getMessageList().option('items'), expectedData, 'messagelist items option should contain the same count of messages after update'); + assert.strictEqual(this.getBubbles().length, 3, 'message bubble count'); + assert.strictEqual(this.getBubbles().eq(1).text(), newBubbleText, 'message bubble text was updated'); + + const messageData = data(this.getBubbles().eq(1).get(0), 'dxMessageData'); + + assert.deepEqual(messageData, { id: 2, text: newBubbleText }, 'message bubble data was updated'); + }); + + QUnit.test('Message should be removed along with its group when using store.push({ type: "remove", key: "message_id" }), and the message was the last one in the group', function(assert) { + const messages = [{ id: 1, text: 'message_1', author: userFirst }, { id: 2, text: 'message_2', author: userSecond }]; + const timeout = 100; + + const store = new CustomStore({ + key: 'id', + load: function() { + const d = $.Deferred(); + setTimeout(function() { + d.resolve([...messages]); + }, timeout); + return d.promise(); + }, + }); + + this.reinit({ + dataSource: store, + reloadOnChange: false, + }); + + this.clock.tick(timeout); + + assert.strictEqual(this.getMessageGroups().length, 2, 'messagegroup count after initialization'); + + store.push([{ type: 'remove', key: 2 }]); + assert.strictEqual(this.getBubbles().length, 2, 'message bubble was removed'); + + this.clock.tick(timeout * 2); + + assert.strictEqual(this.getMessageGroups().length, 1, 'messagegroup count after removing item'); + assert.deepEqual(this.instance.option('items'), [...messages.splice(0, 1)], 'items option should contain the correct messages after deletion'); + assert.deepEqual(this.getMessageList().option('items'), this.instance.option('items'), 'messagelist items option should contain the correct messages after deletion'); + }); + + QUnit.test('Message should be removed when using store.push({ type: "remove", key: "message_id" })', function(assert) { + const messages = [{ id: 1, text: 'message_1' }, { id: 2, text: 'message_2' }, { id: 3, text: 'message_3' }]; + const timeout = 100; + + const store = new CustomStore({ + key: 'id', + load: function() { + const d = $.Deferred(); + setTimeout(function() { + d.resolve([...messages]); + }, timeout); + return d.promise(); + }, + }); + + this.reinit({ + dataSource: store, + reloadOnChange: false, + }); + + this.clock.tick(timeout); + + store.push([{ type: 'remove', key: 3 }]); + + this.clock.tick(timeout * 2); + + assert.deepEqual(this.instance.option('items'), [...messages.splice(0, 2)], 'items option should contain the last messages after deletion'); + assert.deepEqual(this.getMessageList().option('items'), this.instance.option('items'), 'messagelist items option should contain the last messages after deletion'); + assert.strictEqual(this.getBubbles().length, 2, 'message bubble was removed'); + }); + + QUnit.test(`${CHAT_LAST_MESSAGEGROUP_ALIGNMENT_START_CLASS} class should be moved to a previous group after removing the last one from store`, function(assert) { + const messages = [{ + id: 1, + text: 'message_1', + author: userFirst + }, { id: 2, + text: 'message_2', + author: userSecond, + }, { + id: 3, + text: 'message_3', + author: userFirst + }, { + id: 4, + text: 'message_4', + author: userSecond, + }]; + + const timeout = 100; + + const store = new CustomStore({ + key: 'id', + load: function() { + const d = $.Deferred(); + setTimeout(function() { + d.resolve([...messages]); + }, timeout); + return d.promise(); + }, + }); + + this.reinit({ + dataSource: store, + user: { id: userFirst.id }, + reloadOnChange: false, + }); + + this.clock.tick(timeout); + store.push([{ type: 'remove', key: 4 }]); + + this.clock.tick(timeout * 2); + + const $lastMessageGroup = this.$element.find(`.${CHAT_LAST_MESSAGEGROUP_ALIGNMENT_START_CLASS}`); + + assert.strictEqual($lastMessageGroup.length, 1, 'only one message group has the corresponding class'); + assert.strictEqual($lastMessageGroup.find(`.${CHAT_MESSAGEBUBBLE_CLASS}`).text(), 'message_2', 'message group content is correct.'); + }); + + QUnit.test(`${CHAT_LAST_MESSAGEGROUP_ALIGNMENT_END_CLASS} class should move to the previous group after removing the last one from the store`, function(assert) { + const messages = [{ + id: 1, + text: 'message_1', + author: userFirst + }, { id: 2, + text: 'message_2', + author: userSecond, + }, { + id: 3, + text: 'message_3', + author: userFirst + }, { + id: 4, + text: 'message_4', + author: userSecond, + }]; + + const timeout = 100; + + const store = new CustomStore({ + key: 'id', + load: function() { + const d = $.Deferred(); + setTimeout(function() { + d.resolve([...messages]); + }, timeout); + return d.promise(); + }, + }); + + this.reinit({ + dataSource: store, + user: { id: userFirst.id }, + reloadOnChange: false, + }); + + this.clock.tick(timeout); + store.push([{ type: 'remove', key: 3 }]); + + this.clock.tick(timeout * 2); + + const $lastMessageGroup = this.$element.find(`.${CHAT_LAST_MESSAGEGROUP_ALIGNMENT_END_CLASS}`); + + assert.strictEqual($lastMessageGroup.length, 1, 'only one message group has the corresponding class'); + assert.strictEqual($lastMessageGroup.find(`.${CHAT_MESSAGEBUBBLE_CLASS}`).text(), 'message_1', 'message group content is correct.'); + }); + + QUnit.test('day header element should be removed after removing all groups for the current day', function(assert) { + const messages = [{ + id: 1, + text: 'message_1', + timestamp: new Date('2021/10/17'), + author: userFirst + }, { id: 2, + text: 'message_2', + timestamp: new Date('2021/10/24'), + author: userSecond, + }, { + id: 3, + text: 'message_3', + timestamp: new Date('2021/10/24'), + author: userFirst, + }, { + id: 4, + timestamp: new Date('2021/10/27'), + text: 'message_4', + author: userSecond, + }]; + + const timeout = 100; + + const store = new CustomStore({ + key: 'id', + load: function() { + const d = $.Deferred(); + setTimeout(function() { + d.resolve([...messages]); + }, timeout); + return d.promise(); + }, + }); + + this.reinit({ + dataSource: store, + user: { id: userFirst.id }, + reloadOnChange: false, + }); + + this.clock.tick(timeout); + + store.push([{ type: 'remove', key: 3 }]); + this.clock.tick(timeout); + + assert.strictEqual(this.getDayHeaders().length, 3, 'three day header should be present'); + + store.push([{ type: 'remove', key: 2 }]); + this.clock.tick(timeout); + + assert.strictEqual(this.getDayHeaders().length, 2, 'day header was removed'); + assert.strictEqual(this.getDayHeaders().eq(0).text(), '10/17/2021', 'day header content is correct'); + assert.strictEqual(this.getDayHeaders().eq(1).text(), '10/27/2021', 'day header content is correct'); + }); + + QUnit.test('day header should be removed after the last message is deleted', function(assert) { + const messages = [{ + id: 1, + text: 'message_1', + timestamp: new Date('2021/10/17'), + author: userFirst + }]; + + const timeout = 100; + + const store = new CustomStore({ + key: 'id', + load: function() { + const d = $.Deferred(); + setTimeout(function() { + d.resolve([...messages]); + }, timeout); + return d.promise(); + }, + }); + + this.reinit({ + dataSource: store, + user: { id: userFirst.id }, + reloadOnChange: false, + }); + + this.clock.tick(timeout); + + assert.strictEqual(this.getDayHeaders().length, 1, 'day header should be present'); + + store.push([{ type: 'remove', key: 1 }]); + this.clock.tick(timeout); + + assert.strictEqual(this.getDayHeaders().length, 0, 'day header was removed'); + }); + + QUnit.test('emptyview should be rendered after the last message is deleted from the store', function(assert) { + const messages = [{ + id: 1, + text: 'message_1', + timestamp: new Date('2021/10/17'), + author: userFirst + }]; + + const timeout = 100; + + const store = new CustomStore({ + key: 'id', + load: function() { + const d = $.Deferred(); + setTimeout(function() { + d.resolve([...messages]); + }, timeout); + return d.promise(); + }, + }); + + this.reinit({ + dataSource: store, + user: { id: userFirst.id }, + reloadOnChange: false, + }); + + this.clock.tick(timeout); + + assert.strictEqual(this.getEmptyView().length, 0, 'empty view is not rendered'); + + store.push([{ type: 'remove', key: 1 }]); + this.clock.tick(timeout * 2); + + assert.strictEqual(this.getEmptyView().length, 1, 'empty view is rendered'); + }); + + QUnit.test('emptyview should be rendered after each removal and removed after each addition from the store', function(assert) { + const messages = [{ + id: 1, + text: 'message_1', + timestamp: new Date('2021/10/17'), + author: userFirst + }]; + + const timeout = 100; + + const store = new CustomStore({ + key: 'id', + load: function() { + const d = $.Deferred(); + setTimeout(function() { + d.resolve([...messages]); + }, timeout); + return d.promise(); + }, + insert: function(message) { + const d = $.Deferred(); + + setTimeout(() => { + d.resolve(); + }, timeout); + + return d.promise(); + }, + }); + + this.reinit({ + dataSource: store, + user: { id: userFirst.id }, + reloadOnChange: false, + }); + + this.clock.tick(timeout); + + assert.strictEqual(this.getEmptyView().length, 0, 'empty view is not rendered'); + + store.push([{ type: 'remove', key: 1 }]); + this.clock.tick(timeout); + + store.push([{ type: 'insert', data: { + id: 1, + timestamp: NOW, + text: 'NEW MESSAGE', + } }]); + this.clock.tick(timeout); + + store.push([{ type: 'remove', key: 1 }]); + this.clock.tick(timeout); + + assert.strictEqual(this.getEmptyView().length, 1, 'empty view is rendered'); + }); + QUnit.test('Loading and Empty view should not be shown at the same time when the dataSource option changes', function(assert) { const messages = [{ text: 'message_1' }, { text: 'message_2' }]; const timeout = 400;