From 6f1aaad53d538592583dc889c1926e7f57700be3 Mon Sep 17 00:00:00 2001 From: ugur-vaadin Date: Mon, 9 Mar 2026 12:35:46 +0200 Subject: [PATCH 1/2] test: add integration tests for chat history feature --- .../ai/tests/AIOrchestratorHistoryPage.java | 110 +++++++++++++++ .../ai/tests/AIOrchestratorHistoryIT.java | 126 ++++++++++++++++++ 2 files changed, 236 insertions(+) create mode 100644 vaadin-ai-components-flow-parent/vaadin-ai-components-flow-integration-tests/src/main/java/com/vaadin/flow/component/ai/tests/AIOrchestratorHistoryPage.java create mode 100644 vaadin-ai-components-flow-parent/vaadin-ai-components-flow-integration-tests/src/test/java/com/vaadin/flow/component/ai/tests/AIOrchestratorHistoryIT.java diff --git a/vaadin-ai-components-flow-parent/vaadin-ai-components-flow-integration-tests/src/main/java/com/vaadin/flow/component/ai/tests/AIOrchestratorHistoryPage.java b/vaadin-ai-components-flow-parent/vaadin-ai-components-flow-integration-tests/src/main/java/com/vaadin/flow/component/ai/tests/AIOrchestratorHistoryPage.java new file mode 100644 index 00000000000..ddc03e20e24 --- /dev/null +++ b/vaadin-ai-components-flow-parent/vaadin-ai-components-flow-integration-tests/src/main/java/com/vaadin/flow/component/ai/tests/AIOrchestratorHistoryPage.java @@ -0,0 +1,110 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.ai.tests; + +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import com.vaadin.flow.component.ai.common.AIAttachment; +import com.vaadin.flow.component.ai.common.ChatMessage; +import com.vaadin.flow.component.ai.orchestrator.AIOrchestrator; +import com.vaadin.flow.component.ai.provider.LLMProvider; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.NativeButton; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.messages.MessageInput; +import com.vaadin.flow.component.messages.MessageList; +import com.vaadin.flow.router.Route; + +import reactor.core.publisher.Flux; + +/** + * Test page for AIOrchestrator chat history features. + */ +@Route("vaadin-ai/orchestrator-history") +public class AIOrchestratorHistoryPage extends Div { + + private AIOrchestrator orchestrator; + private final Div chatContainer; + private final Span historyInfo; + + public AIOrchestratorHistoryPage() { + chatContainer = new Div(); + + historyInfo = new Span(); + historyInfo.setId("history-info"); + + buildOrchestrator(null); + + var getHistoryButton = new NativeButton("Get History", e -> { + var history = orchestrator.getHistory(); + var sb = new StringBuilder(); + sb.append("size=").append(history.size()); + for (var msg : history) { + sb.append("|").append(msg.role()).append(":") + .append(msg.content()); + } + historyInfo.setText(sb.toString()); + }); + getHistoryButton.setId("get-history"); + + var restoreHistoryButton = new NativeButton("Restore History", e -> { + var history = List.of( + new ChatMessage(ChatMessage.Role.USER, "Previous question", + "msg-1", Instant.now()), + new ChatMessage(ChatMessage.Role.ASSISTANT, + "Previous answer", null, Instant.now())); + buildOrchestrator(history); + }); + restoreHistoryButton.setId("restore-history"); + + add(getHistoryButton, restoreHistoryButton, chatContainer, historyInfo); + } + + private void buildOrchestrator(List history) { + chatContainer.removeAll(); + + var messageList = new MessageList(); + messageList.setSizeFull(); + + var messageInput = new MessageInput(); + + var builder = AIOrchestrator.builder(new EchoLLMProvider(), null) + .withMessageList(messageList).withInput(messageInput); + if (history != null) { + builder.withHistory(history, Collections.emptyMap()); + } + orchestrator = builder.build(); + + chatContainer.add(messageList, messageInput); + } + + private static class EchoLLMProvider implements LLMProvider { + @Override + public Flux stream(LLMRequest request) { + var response = "Echo: " + request.userMessage(); + return Flux.fromArray(response.split(" ")).map(word -> word + " "); + } + + @Override + public void setHistory(List history, + Map> attachmentsByMessageId) { + // No-op + } + } +} diff --git a/vaadin-ai-components-flow-parent/vaadin-ai-components-flow-integration-tests/src/test/java/com/vaadin/flow/component/ai/tests/AIOrchestratorHistoryIT.java b/vaadin-ai-components-flow-parent/vaadin-ai-components-flow-integration-tests/src/test/java/com/vaadin/flow/component/ai/tests/AIOrchestratorHistoryIT.java new file mode 100644 index 00000000000..82d30ccbfe0 --- /dev/null +++ b/vaadin-ai-components-flow-parent/vaadin-ai-components-flow-integration-tests/src/test/java/com/vaadin/flow/component/ai/tests/AIOrchestratorHistoryIT.java @@ -0,0 +1,126 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.ai.tests; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.flow.component.messages.testbench.MessageInputElement; +import com.vaadin.flow.component.messages.testbench.MessageListElement; +import com.vaadin.flow.testutil.TestPath; +import com.vaadin.testbench.TestBenchElement; +import com.vaadin.tests.AbstractComponentIT; + +/** + * Integration tests for AIOrchestrator chat history. + */ +@TestPath("vaadin-ai/orchestrator-history") +public class AIOrchestratorHistoryIT extends AbstractComponentIT { + + private MessageListElement messageList; + private MessageInputElement messageInput; + + @Before + public void init() { + open(); + messageList = $(MessageListElement.class).single(); + messageInput = $(MessageInputElement.class).single(); + } + + @Test + public void sendMessage_getHistory_containsUserAndAssistantMessages() { + var message = "Hello"; + submitMessage(message); + + clickElementWithJs("get-history"); + var historyInfo = getHistoryInfo(); + Assert.assertEquals(2, getHistorySize(historyInfo)); + Assert.assertTrue(containsUserMessage(historyInfo, message)); + Assert.assertTrue(containsAssistantMessage(historyInfo, message)); + } + + @Test + public void restoreHistory_messagesDisplayed() { + restoreHistory(); + + var messages = messageList.getMessageElements(); + Assert.assertEquals(2, messages.size()); + Assert.assertTrue( + messages.get(0).getText().contains("Previous question")); + Assert.assertTrue( + messages.get(1).getText().contains("Previous answer")); + } + + @Test + public void restoreHistory_sendMessage_historyContainsAll() { + restoreHistory(); + + var newMessage = "Follow-up"; + submitMessage(newMessage); + + clickElementWithJs("get-history"); + var historyInfo = getHistoryInfo(); + Assert.assertEquals(4, getHistorySize(historyInfo)); + Assert.assertTrue( + containsUserMessage(historyInfo, "Previous question")); + Assert.assertTrue(containsUserMessage(historyInfo, newMessage)); + } + + private void restoreHistory() { + clickElementWithJs("restore-history"); + // Re-query components since the page creates new instances + messageList = $(MessageListElement.class).single(); + messageInput = $(MessageInputElement.class).single(); + waitUntilMessagesDisplayed(2); + } + + private void waitUntilMessagesDisplayed(int expectedMessageCount) { + waitUntil(driver -> getMessageCount() == expectedMessageCount, 2); + } + + private boolean containsAssistantMessage(TestBenchElement historyInfo, + String assistantMessage) { + return historyInfo.getText() + .contains("ASSISTANT:Echo: " + assistantMessage); + } + + private boolean containsUserMessage(TestBenchElement historyInfo, + String userMessage) { + return historyInfo.getText().contains("USER:" + userMessage); + } + + private void submitMessage(String message) { + var initialMessageCount = getMessageCount(); + messageInput.submit(message); + waitUntilMessagesDisplayed(initialMessageCount + 2); + } + + private int getHistorySize(TestBenchElement historyInfo) { + return Integer.parseInt( + historyInfo.getText().split("\\|")[0].replace("size=", "")); + } + + private TestBenchElement getHistoryInfo() { + var historyInfo = $("span").id("history-info"); + waitUntil(driver -> historyInfo.getText().startsWith("size="), 2); + return historyInfo; + } + + private int getMessageCount() { + return messageList.getMessageElements().size(); + } +} From 02a598e1d491a33e598c8654f3b106838fcdf347 Mon Sep 17 00:00:00 2001 From: ugur-vaadin Date: Wed, 11 Mar 2026 18:01:11 +0200 Subject: [PATCH 2/2] test: update test to use refresh to mimic real use and move test --- .../ai/tests/AIOrchestratorHistoryPage.java | 110 --------------- .../ai/tests/AIOrchestratorPage.java | 29 +++- .../ai/tests/AIOrchestratorHistoryIT.java | 126 ------------------ .../component/ai/tests/AIOrchestratorIT.java | 17 +++ 4 files changed, 43 insertions(+), 239 deletions(-) delete mode 100644 vaadin-ai-components-flow-parent/vaadin-ai-components-flow-integration-tests/src/main/java/com/vaadin/flow/component/ai/tests/AIOrchestratorHistoryPage.java delete mode 100644 vaadin-ai-components-flow-parent/vaadin-ai-components-flow-integration-tests/src/test/java/com/vaadin/flow/component/ai/tests/AIOrchestratorHistoryIT.java diff --git a/vaadin-ai-components-flow-parent/vaadin-ai-components-flow-integration-tests/src/main/java/com/vaadin/flow/component/ai/tests/AIOrchestratorHistoryPage.java b/vaadin-ai-components-flow-parent/vaadin-ai-components-flow-integration-tests/src/main/java/com/vaadin/flow/component/ai/tests/AIOrchestratorHistoryPage.java deleted file mode 100644 index ddc03e20e24..00000000000 --- a/vaadin-ai-components-flow-parent/vaadin-ai-components-flow-integration-tests/src/main/java/com/vaadin/flow/component/ai/tests/AIOrchestratorHistoryPage.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2000-2026 Vaadin Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.vaadin.flow.component.ai.tests; - -import java.time.Instant; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import com.vaadin.flow.component.ai.common.AIAttachment; -import com.vaadin.flow.component.ai.common.ChatMessage; -import com.vaadin.flow.component.ai.orchestrator.AIOrchestrator; -import com.vaadin.flow.component.ai.provider.LLMProvider; -import com.vaadin.flow.component.html.Div; -import com.vaadin.flow.component.html.NativeButton; -import com.vaadin.flow.component.html.Span; -import com.vaadin.flow.component.messages.MessageInput; -import com.vaadin.flow.component.messages.MessageList; -import com.vaadin.flow.router.Route; - -import reactor.core.publisher.Flux; - -/** - * Test page for AIOrchestrator chat history features. - */ -@Route("vaadin-ai/orchestrator-history") -public class AIOrchestratorHistoryPage extends Div { - - private AIOrchestrator orchestrator; - private final Div chatContainer; - private final Span historyInfo; - - public AIOrchestratorHistoryPage() { - chatContainer = new Div(); - - historyInfo = new Span(); - historyInfo.setId("history-info"); - - buildOrchestrator(null); - - var getHistoryButton = new NativeButton("Get History", e -> { - var history = orchestrator.getHistory(); - var sb = new StringBuilder(); - sb.append("size=").append(history.size()); - for (var msg : history) { - sb.append("|").append(msg.role()).append(":") - .append(msg.content()); - } - historyInfo.setText(sb.toString()); - }); - getHistoryButton.setId("get-history"); - - var restoreHistoryButton = new NativeButton("Restore History", e -> { - var history = List.of( - new ChatMessage(ChatMessage.Role.USER, "Previous question", - "msg-1", Instant.now()), - new ChatMessage(ChatMessage.Role.ASSISTANT, - "Previous answer", null, Instant.now())); - buildOrchestrator(history); - }); - restoreHistoryButton.setId("restore-history"); - - add(getHistoryButton, restoreHistoryButton, chatContainer, historyInfo); - } - - private void buildOrchestrator(List history) { - chatContainer.removeAll(); - - var messageList = new MessageList(); - messageList.setSizeFull(); - - var messageInput = new MessageInput(); - - var builder = AIOrchestrator.builder(new EchoLLMProvider(), null) - .withMessageList(messageList).withInput(messageInput); - if (history != null) { - builder.withHistory(history, Collections.emptyMap()); - } - orchestrator = builder.build(); - - chatContainer.add(messageList, messageInput); - } - - private static class EchoLLMProvider implements LLMProvider { - @Override - public Flux stream(LLMRequest request) { - var response = "Echo: " + request.userMessage(); - return Flux.fromArray(response.split(" ")).map(word -> word + " "); - } - - @Override - public void setHistory(List history, - Map> attachmentsByMessageId) { - // No-op - } - } -} diff --git a/vaadin-ai-components-flow-parent/vaadin-ai-components-flow-integration-tests/src/main/java/com/vaadin/flow/component/ai/tests/AIOrchestratorPage.java b/vaadin-ai-components-flow-parent/vaadin-ai-components-flow-integration-tests/src/main/java/com/vaadin/flow/component/ai/tests/AIOrchestratorPage.java index fd2f5c15dde..d2137e9cbe7 100644 --- a/vaadin-ai-components-flow-parent/vaadin-ai-components-flow-integration-tests/src/main/java/com/vaadin/flow/component/ai/tests/AIOrchestratorPage.java +++ b/vaadin-ai-components-flow-parent/vaadin-ai-components-flow-integration-tests/src/main/java/com/vaadin/flow/component/ai/tests/AIOrchestratorPage.java @@ -15,11 +15,13 @@ */ package com.vaadin.flow.component.ai.tests; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import com.vaadin.flow.component.ai.common.AIAttachment; +import com.vaadin.flow.component.ai.common.ChatMessage; import com.vaadin.flow.component.ai.orchestrator.AIOrchestrator; import com.vaadin.flow.component.ai.provider.LLMProvider; import com.vaadin.flow.component.html.Div; @@ -34,6 +36,7 @@ import com.vaadin.flow.component.upload.UploadFileListVariant; import com.vaadin.flow.component.upload.UploadManager; import com.vaadin.flow.router.Route; +import com.vaadin.flow.server.VaadinSession; import reactor.core.publisher.Flux; @@ -45,7 +48,9 @@ @Route("vaadin-ai/orchestrator") public class AIOrchestratorPage extends UploadDropZone { - private final AIOrchestrator orchestrator; + private static final String HISTORY_SESSION_KEY = "ai-orchestrator-history"; + + private AIOrchestrator orchestrator; // Attachment storage keyed by message ID private final Map> attachmentStorage = new HashMap<>(); @@ -53,6 +58,7 @@ public class AIOrchestratorPage extends UploadDropZone { // Displays info about the last clicked attachment private final Span clickedAttachmentInfo = new Span(); + @SuppressWarnings("unchecked") public AIOrchestratorPage() { setHeightFull(); @@ -74,7 +80,7 @@ public AIOrchestratorPage() { clickedAttachmentInfo.setId("clicked-attachment-info"); - orchestrator = AIOrchestrator.builder(new EchoLLMProvider(), null) + var builder = AIOrchestrator.builder(new EchoLLMProvider(), null) .withMessageList(messageList).withInput(messageInput) .withFileReceiver(uploadManager) .withAttachmentSubmitListener(event -> { @@ -89,7 +95,18 @@ public AIOrchestratorPage() { clickedAttachmentInfo.setText(attachment.name() + " | " + attachment.mimeType()); } - }).build(); + }) + .withResponseCompleteListener(event -> VaadinSession + .getCurrent().setAttribute(HISTORY_SESSION_KEY, + orchestrator.getHistory())); + + var savedHistory = (List) VaadinSession.getCurrent() + .getAttribute(HISTORY_SESSION_KEY); + if (savedHistory != null) { + builder.withHistory(savedHistory, Collections.emptyMap()); + } + + orchestrator = builder.build(); var promptButton = new NativeButton("Send Hello", e -> orchestrator.prompt("Hello from button")); @@ -117,5 +134,11 @@ public Flux stream(LLMRequest request) { var response = "Echo: " + request.userMessage(); return Flux.fromArray(response.split(" ")).map(word -> word + " "); } + + @Override + public void setHistory(List history, + Map> attachmentsByMessageId) { + // No-op for testing + } } } diff --git a/vaadin-ai-components-flow-parent/vaadin-ai-components-flow-integration-tests/src/test/java/com/vaadin/flow/component/ai/tests/AIOrchestratorHistoryIT.java b/vaadin-ai-components-flow-parent/vaadin-ai-components-flow-integration-tests/src/test/java/com/vaadin/flow/component/ai/tests/AIOrchestratorHistoryIT.java deleted file mode 100644 index 82d30ccbfe0..00000000000 --- a/vaadin-ai-components-flow-parent/vaadin-ai-components-flow-integration-tests/src/test/java/com/vaadin/flow/component/ai/tests/AIOrchestratorHistoryIT.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright 2000-2026 Vaadin Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.vaadin.flow.component.ai.tests; - -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - -import com.vaadin.flow.component.messages.testbench.MessageInputElement; -import com.vaadin.flow.component.messages.testbench.MessageListElement; -import com.vaadin.flow.testutil.TestPath; -import com.vaadin.testbench.TestBenchElement; -import com.vaadin.tests.AbstractComponentIT; - -/** - * Integration tests for AIOrchestrator chat history. - */ -@TestPath("vaadin-ai/orchestrator-history") -public class AIOrchestratorHistoryIT extends AbstractComponentIT { - - private MessageListElement messageList; - private MessageInputElement messageInput; - - @Before - public void init() { - open(); - messageList = $(MessageListElement.class).single(); - messageInput = $(MessageInputElement.class).single(); - } - - @Test - public void sendMessage_getHistory_containsUserAndAssistantMessages() { - var message = "Hello"; - submitMessage(message); - - clickElementWithJs("get-history"); - var historyInfo = getHistoryInfo(); - Assert.assertEquals(2, getHistorySize(historyInfo)); - Assert.assertTrue(containsUserMessage(historyInfo, message)); - Assert.assertTrue(containsAssistantMessage(historyInfo, message)); - } - - @Test - public void restoreHistory_messagesDisplayed() { - restoreHistory(); - - var messages = messageList.getMessageElements(); - Assert.assertEquals(2, messages.size()); - Assert.assertTrue( - messages.get(0).getText().contains("Previous question")); - Assert.assertTrue( - messages.get(1).getText().contains("Previous answer")); - } - - @Test - public void restoreHistory_sendMessage_historyContainsAll() { - restoreHistory(); - - var newMessage = "Follow-up"; - submitMessage(newMessage); - - clickElementWithJs("get-history"); - var historyInfo = getHistoryInfo(); - Assert.assertEquals(4, getHistorySize(historyInfo)); - Assert.assertTrue( - containsUserMessage(historyInfo, "Previous question")); - Assert.assertTrue(containsUserMessage(historyInfo, newMessage)); - } - - private void restoreHistory() { - clickElementWithJs("restore-history"); - // Re-query components since the page creates new instances - messageList = $(MessageListElement.class).single(); - messageInput = $(MessageInputElement.class).single(); - waitUntilMessagesDisplayed(2); - } - - private void waitUntilMessagesDisplayed(int expectedMessageCount) { - waitUntil(driver -> getMessageCount() == expectedMessageCount, 2); - } - - private boolean containsAssistantMessage(TestBenchElement historyInfo, - String assistantMessage) { - return historyInfo.getText() - .contains("ASSISTANT:Echo: " + assistantMessage); - } - - private boolean containsUserMessage(TestBenchElement historyInfo, - String userMessage) { - return historyInfo.getText().contains("USER:" + userMessage); - } - - private void submitMessage(String message) { - var initialMessageCount = getMessageCount(); - messageInput.submit(message); - waitUntilMessagesDisplayed(initialMessageCount + 2); - } - - private int getHistorySize(TestBenchElement historyInfo) { - return Integer.parseInt( - historyInfo.getText().split("\\|")[0].replace("size=", "")); - } - - private TestBenchElement getHistoryInfo() { - var historyInfo = $("span").id("history-info"); - waitUntil(driver -> historyInfo.getText().startsWith("size="), 2); - return historyInfo; - } - - private int getMessageCount() { - return messageList.getMessageElements().size(); - } -} diff --git a/vaadin-ai-components-flow-parent/vaadin-ai-components-flow-integration-tests/src/test/java/com/vaadin/flow/component/ai/tests/AIOrchestratorIT.java b/vaadin-ai-components-flow-parent/vaadin-ai-components-flow-integration-tests/src/test/java/com/vaadin/flow/component/ai/tests/AIOrchestratorIT.java index 18dd0f7a560..f11e10a5ae1 100644 --- a/vaadin-ai-components-flow-parent/vaadin-ai-components-flow-integration-tests/src/test/java/com/vaadin/flow/component/ai/tests/AIOrchestratorIT.java +++ b/vaadin-ai-components-flow-parent/vaadin-ai-components-flow-integration-tests/src/test/java/com/vaadin/flow/component/ai/tests/AIOrchestratorIT.java @@ -79,6 +79,23 @@ public void uploadFile_submitMessage_attachmentRenderedInMessage() Assert.assertNotNull(userMessage.getAttachmentByName("test-file.txt")); } + @Test + public void submitMessage_refreshPage_historyRestored() { + messageInput.submit("Hello"); + waitUntil(driver -> getMessageCount() >= 2, 5); + Assert.assertEquals(2, getMessageCount()); + + // Refresh the page - history should be auto-restored from session + open(); + messageList = $(MessageListElement.class).single(); + + waitUntil(driver -> getMessageCount() >= 2, 5); + var messages = messageList.getMessageElements(); + Assert.assertEquals(2, messages.size()); + Assert.assertTrue(messages.get(0).getText().contains("Hello")); + Assert.assertTrue(messages.get(1).getText().contains("Echo: Hello")); + } + @Test public void uploadFile_submitMessage_clickAttachment_infoDisplayed() throws Exception {