Skip to content

Commit 88f46fe

Browse files
fix: React two template roots regression (T1310108) (#31458)
1 parent 4f9aae9 commit 88f46fe

File tree

10 files changed

+277
-50
lines changed

10 files changed

+277
-50
lines changed

e2e/wrappers/builders/react19/src/utils/componentFinder.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ const COMPONENTS = [
1818
path: 'text-box-dynamic-styles',
1919
name: 'TextBoxDynamicStyles',
2020
component: () => import('@examples/text-box-dynamic-styles/react19/index.jsx')
21+
},
22+
{
23+
path: 'chat-template-rerender',
24+
name: 'ChatTemplateRerender',
25+
component: () => import('@examples/chat-template-rerender/react19/index.jsx')
2126
}
2227
];
2328

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export const REGENERATION_TEXT = 'Regeneration...';
2+
export const CHAT_DISABLED_CLASS = 'chat-disabled';
3+
export const user = {
4+
id: 'user',
5+
};
6+
export const assistant = {
7+
id: 'assistant',
8+
name: 'Virtual Assistant',
9+
};
10+
11+
let messageId = 0;
12+
export const getMessageId = () => messageId++;
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { useCallback, useState } from 'react';
2+
import Chat from 'devextreme-react/chat';
3+
import { loadMessages } from 'devextreme-react/common/core/localization';
4+
import { user, assistant, CHAT_DISABLED_CLASS, getMessageId } from './data';
5+
import Message from './message';
6+
import { dataSource, useApi } from './useApi';
7+
8+
loadMessages({
9+
en: {
10+
'dxChat-emptyListMessage': 'Chat is Empty',
11+
'dxChat-emptyListPrompt': 'AI Assistant is ready to answer your questions.',
12+
'dxChat-textareaPlaceholder': 'Ask AI Assistant...',
13+
},
14+
});
15+
export default function App() {
16+
const {
17+
insertMessage, fetchAIResponse, regenerateLastAIResponse,
18+
} = useApi();
19+
const [typingUsers, setTypingUsers] = useState([]);
20+
const [isProcessing, setIsProcessing] = useState(false);
21+
const processAIRequest = useCallback(
22+
async() => {
23+
setIsProcessing(true);
24+
setTypingUsers([assistant]);
25+
await fetchAIResponse();
26+
setTypingUsers([]);
27+
setIsProcessing(false);
28+
},
29+
[fetchAIResponse],
30+
);
31+
const onMessageEntered = useCallback(
32+
async({ message, event }) => {
33+
insertMessage({ id: getMessageId(), ...message });
34+
event.target.blur();
35+
await processAIRequest();
36+
event.target.focus();
37+
},
38+
[insertMessage, processAIRequest],
39+
);
40+
const onRegenerateButtonClick = useCallback(async() => {
41+
setIsProcessing(true);
42+
await regenerateLastAIResponse();
43+
setIsProcessing(false);
44+
}, [regenerateLastAIResponse]);
45+
const messageRender = useCallback(
46+
({ message }) => (
47+
<Message
48+
text={message.text}
49+
onRegenerateButtonClick={onRegenerateButtonClick}
50+
/>
51+
),
52+
[onRegenerateButtonClick],
53+
);
54+
return (
55+
<Chat
56+
className={isProcessing ? CHAT_DISABLED_CLASS : ''}
57+
dataSource={dataSource}
58+
reloadOnChange={false}
59+
showAvatar={false}
60+
showDayHeaders={false}
61+
user={user}
62+
height={710}
63+
onMessageEntered={onMessageEntered}
64+
typingUsers={typingUsers}
65+
messageRender={messageRender}
66+
/>
67+
);
68+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import React, { useCallback, useState } from 'react';
2+
import { Button } from 'devextreme-react/button';
3+
import { REGENERATION_TEXT } from './data';
4+
5+
const Message = ({ text, onRegenerateButtonClick }) => {
6+
const [icon, setIcon] = useState('copy');
7+
const onCopyButtonClick = useCallback(() => {
8+
navigator.clipboard?.writeText(text);
9+
setIcon('check');
10+
setTimeout(() => {
11+
setIcon('copy');
12+
}, 2500);
13+
}, [text]);
14+
if (text === REGENERATION_TEXT) {
15+
return <span>{REGENERATION_TEXT}</span>;
16+
}
17+
return (
18+
<React.Fragment>
19+
<div className="chat-messagebubble-text">{text}</div>
20+
<div className="bubble-button-container">
21+
<Button
22+
icon={icon}
23+
stylingMode="text"
24+
hint="Copy"
25+
onClick={onCopyButtonClick}
26+
/>
27+
<Button
28+
icon="refresh"
29+
stylingMode="text"
30+
hint="Regenerate"
31+
onClick={onRegenerateButtonClick}
32+
/>
33+
</div>
34+
</React.Fragment>
35+
);
36+
};
37+
export default Message;
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { useCallback } from 'react';
2+
import { CustomStore, DataSource } from 'devextreme-react/common/data';
3+
import {
4+
assistant, REGENERATION_TEXT, getMessageId,
5+
} from './data';
6+
7+
export function* aiResponseGen() {
8+
yield 'How can I help you?';
9+
yield 'In other words, what do you want?';
10+
}
11+
const store = [];
12+
const aiResponse = aiResponseGen();
13+
const customStore = new CustomStore({
14+
key: 'id',
15+
load: () =>
16+
new Promise((resolve) => {
17+
setTimeout(() => {
18+
resolve([...store]);
19+
}, 0);
20+
}),
21+
insert: (message) =>
22+
new Promise((resolve) => {
23+
setTimeout(() => {
24+
store.push(message);
25+
resolve(message);
26+
});
27+
}),
28+
});
29+
export const dataSource = new DataSource({
30+
store: customStore,
31+
paginate: false,
32+
});
33+
const dataItemToMessage = (item) => ({
34+
role: item.author.id,
35+
content: item.text,
36+
});
37+
const getMessageHistory = () => [...dataSource.items()].map(dataItemToMessage);
38+
export const useApi = () => {
39+
const insertMessage = useCallback((data) => {
40+
dataSource.store().push([{ type: 'insert', data }]);
41+
}, []);
42+
const updateLastMessageContent = useCallback((text) => {
43+
const lastMessage = dataSource.items().at(-1);
44+
dataSource.store().push([
45+
{
46+
type: 'update',
47+
key: lastMessage.id,
48+
data: { text },
49+
},
50+
]);
51+
}, []);
52+
const fetchAIResponse = useCallback(
53+
async() => {
54+
const response = aiResponse.next().value;
55+
await new Promise((r) => setTimeout(r, 500));
56+
insertMessage({
57+
id: getMessageId(),
58+
timestamp: new Date(),
59+
author: assistant,
60+
text: response,
61+
});
62+
},
63+
[insertMessage],
64+
);
65+
const regenerateLastAIResponse = useCallback(async() => {
66+
const messageHistory = getMessageHistory();
67+
updateLastMessageContent(REGENERATION_TEXT);
68+
try {
69+
const response = aiResponse.next().value;
70+
await new Promise((r) => setTimeout(r, 500));
71+
updateLastMessageContent(response);
72+
} catch {
73+
updateLastMessageContent(messageHistory.at(-1).content);
74+
}
75+
}, [updateLastMessageContent]);
76+
return {
77+
insertMessage,
78+
fetchAIResponse,
79+
regenerateLastAIResponse,
80+
};
81+
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Selector } from 'testcafe';
2+
import { testInFramework } from '../test-helpers';
3+
4+
if(process.env.FRAMEWORK === 'react') {
5+
testInFramework('Chat template re-rendering', 'chat-template-rerender', [
6+
'Chat should be able to re-render its messages',
7+
async (t) => {
8+
const textarea = Selector('.dx-chat-messagebox textarea');
9+
const sendButton = Selector('.dx-icon-sendfilled');
10+
const assistantBubble = Selector('.chat-messagebubble-text').nth(1);
11+
const regenerateButton = Selector('.dx-icon-refresh').nth(1);
12+
13+
await t
14+
.typeText(textarea, 'Hi there!')
15+
.click(sendButton)
16+
.expect(assistantBubble.textContent).eql('How can I help you?')
17+
.click(regenerateButton)
18+
.expect(assistantBubble.textContent).eql('In other words, what do you want?');
19+
}
20+
]);
21+
}

packages/devextreme-react/src/core/__tests__/template-manager.test.tsx

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -154,11 +154,11 @@ describe('Template Manager', () => {
154154
}));
155155

156156
expect(document.querySelector('.children-template-container')?.innerHTML)
157-
.toBe('<div class="child-template">Children template</div><div style="display: none;"></div>');
157+
.toBe('<div class="child-template">Children template</div><div style="display: none;" class="__dx_react_guard_node__"></div>');
158158
expect(document.querySelector('.render-template-container')?.innerHTML)
159-
.toBe('<div class="render-template">Render template text-2</div><div style="display: none;"></div>');
159+
.toBe('<div class="render-template">Render template text-2</div><div style="display: none;" class="__dx_react_guard_node__"></div>');
160160
expect(document.querySelector('.component-template-container')?.innerHTML)
161-
.toBe('<div class="component-template">Component template text</div><div style="display: none;"></div>');
161+
.toBe('<div class="component-template">Component template text</div><div style="display: none;" class="__dx_react_guard_node__"></div>');
162162

163163
expect(childrenTemplateRendered && renderTemplateRendered && componentTemplateRendered).toBeTruthy();
164164
});
@@ -200,7 +200,7 @@ describe('Template Manager', () => {
200200
}));
201201

202202
expect(document.querySelector('.render-template-container')?.innerHTML)
203-
.toBe('<div class="render-template">Render template text-2</div><div style="display: none;"></div>');
203+
.toBe('<div class="render-template">Render template text-2</div><div style="display: none;" class="__dx_react_guard_node__"></div>');
204204
});
205205

206206
it('does not render template instances if the template prop has been removed but instance removal is delayed (T1208518)', () => {
@@ -232,7 +232,7 @@ describe('Template Manager', () => {
232232
}));
233233

234234
expect(document.querySelector('.render-template-container')?.innerHTML)
235-
.toBe('<div class="render-template">Render template text-2</div><div style="display: none;"></div>');
235+
.toBe('<div class="render-template">Render template text-2</div><div style="display: none;" class="__dx_react_guard_node__"></div>');
236236
expect(document.querySelector('.component-template-container')?.innerHTML)
237237
.toBe('');
238238

@@ -250,7 +250,7 @@ describe('Template Manager', () => {
250250
expect(document.querySelector('.render-template-container')?.innerHTML)
251251
.toBe('');
252252
expect(document.querySelector('.component-template-container')?.innerHTML)
253-
.toBe('<div class="component-template">Component template text</div><div style="display: none;"></div>');
253+
.toBe('<div class="component-template">Component template text</div><div style="display: none;" class="__dx_react_guard_node__"></div>');
254254
});
255255

256256
it('template wrappers are memoized', () => {
@@ -345,9 +345,9 @@ describe('Template Manager', () => {
345345
}));
346346

347347
expect(document.querySelector('.render-template-container')?.innerHTML)
348-
.toBe('<div class="render-template">render-data-key</div><div style="display: none;"></div>');
348+
.toBe('<div class="render-template">render-data-key</div><div style="display: none;" class="__dx_react_guard_node__"></div>');
349349
expect(document.querySelector('.component-template-container')?.innerHTML)
350-
.toBe('<div class="component-template">component-data-key</div><div style="display: none;"></div>');
350+
.toBe('<div class="component-template">component-data-key</div><div style="display: none;" class="__dx_react_guard_node__"></div>');
351351
});
352352

353353
it('onRendered is not called if the template was rendered by a previous widget instance (StrictMode)', () => {
@@ -400,7 +400,7 @@ describe('Template Manager', () => {
400400
}));
401401

402402
expect(document.querySelector('.render-template-container')?.innerHTML)
403-
.toBe('<div class="render-template">Render template text-2</div><div style="display: none;"></div>');
403+
.toBe('<div class="render-template">Render template text-2</div><div style="display: none;" class="__dx_react_guard_node__"></div>');
404404
expect(renderTemplateRendered).toBeTruthy();
405405
});
406406

@@ -437,7 +437,7 @@ describe('Template Manager', () => {
437437
}));
438438

439439
expect(document.querySelector('.render-template-container')?.innerHTML)
440-
.toBe('<div class="render-template">Render template text-2</div><div style="display: none;"></div>');
440+
.toBe('<div class="render-template">Render template text-2</div><div style="display: none;" class="__dx_react_guard_node__"></div>');
441441

442442
const newTemplateOptions = {
443443
renderKey: {
@@ -458,7 +458,7 @@ describe('Template Manager', () => {
458458
});
459459

460460
expect(document.querySelector('.render-template-container')?.innerHTML)
461-
.toBe('<div class="new-render-template">Render template text</div><div style="display: none;"></div>');
461+
.toBe('<div class="new-render-template">Render template text</div><div style="display: none;" class="__dx_react_guard_node__"></div>');
462462
expect(callbackCalled).toBeTruthy();
463463
});
464464

@@ -493,7 +493,7 @@ describe('Template Manager', () => {
493493
}));
494494

495495
expect(document.querySelector('.render-template-container')?.innerHTML)
496-
.toBe('<div class="render-template">Render template text-1</div><div style="display: none;"></div>');
496+
.toBe('<div class="render-template">Render template text-1</div><div style="display: none;" class="__dx_react_guard_node__"></div>');
497497

498498
act(() => dxTemplates.renderKey.render({
499499
model: data1,
@@ -502,7 +502,7 @@ describe('Template Manager', () => {
502502
}));
503503

504504
expect(document.querySelector('.render-template-container')?.innerHTML)
505-
.toBe('<div class="render-template">Render template text-1</div><div style="display: none;"></div>');
505+
.toBe('<div class="render-template">Render template text-1</div><div style="display: none;" class="__dx_react_guard_node__"></div>');
506506

507507
act(() => dxTemplates.renderKey.render({
508508
model: data2,
@@ -511,8 +511,8 @@ describe('Template Manager', () => {
511511
}));
512512

513513
expect(document.querySelector('.render-template-container')?.innerHTML)
514-
.toBe(''.concat('<div class="render-template">Render template text-1</div><div style="display: none;"></div>',
515-
'<div class="render-template">Some other text-1</div><div style="display: none;"></div>'));
514+
.toBe(''.concat('<div class="render-template">Render template text-1</div><div style="display: none;" class="__dx_react_guard_node__"></div>',
515+
'<div class="render-template">Some other text-1</div><div style="display: none;" class="__dx_react_guard_node__"></div>'));
516516

517517
act(() => dxTemplates.renderKey.render({
518518
model: data2,
@@ -521,8 +521,8 @@ describe('Template Manager', () => {
521521
}));
522522

523523
expect(document.querySelector('.render-template-container')?.innerHTML)
524-
.toBe(''.concat('<div class="render-template">Render template text-1</div><div style="display: none;"></div>',
525-
'<div class="render-template">Some other text-1</div><div style="display: none;"></div>'));
524+
.toBe(''.concat('<div class="render-template">Render template text-1</div><div style="display: none;" class="__dx_react_guard_node__"></div>',
525+
'<div class="render-template">Some other text-1</div><div style="display: none;" class="__dx_react_guard_node__"></div>'));
526526

527527
act(() => dxTemplates.renderKey.render({
528528
model: data2,
@@ -531,10 +531,10 @@ describe('Template Manager', () => {
531531
}));
532532

533533
expect(document.querySelector('.render-template-container')?.innerHTML)
534-
.toBe(''.concat('<div class="render-template">Render template text-1</div><div style="display: none;"></div>',
535-
'<div class="render-template">Some other text-1</div><div style="display: none;"></div>'));
534+
.toBe(''.concat('<div class="render-template">Render template text-1</div><div style="display: none;" class="__dx_react_guard_node__"></div>',
535+
'<div class="render-template">Some other text-1</div><div style="display: none;" class="__dx_react_guard_node__"></div>'));
536536
expect(document.querySelector('.other-container')?.innerHTML)
537-
.toBe('<div class="render-template">Some other text-1</div><div style="display: none;"></div>');
537+
.toBe('<div class="render-template">Some other text-1</div><div style="display: none;" class="__dx_react_guard_node__"></div>');
538538
});
539539

540540
it('template is removed if the container was removed during render', () => {
@@ -589,7 +589,7 @@ describe('Template Manager', () => {
589589
}));
590590

591591
expect(document.querySelector('.render-template-container')?.innerHTML)
592-
.toBe('<div class="render-template">Render template text-1</div><div style="display: none;"></div>');
592+
.toBe('<div class="render-template">Render template text-1</div><div style="display: none;" class="__dx_react_guard_node__"></div>');
593593

594594
rerender(
595595
<React.Fragment>
@@ -599,6 +599,6 @@ describe('Template Manager', () => {
599599
);
600600

601601
expect(document.querySelector('.render-template-container')?.innerHTML)
602-
.toBe('<div class="render-template">Render template text-1</div><div style="display: none;"></div>');
602+
.toBe('<div class="render-template">Render template text-1</div><div style="display: none;" class="__dx_react_guard_node__"></div>');
603603
});
604-
});
604+
});

0 commit comments

Comments
 (0)