How to test MessageInput send?

Hi! I’m trying to test my chat agent with KaribuTest. I don’t think I’m clicking the Send button correctly in my test in MessageSubmit.

I tried to get access to the Send button with id and text, but that didn’t work. Now I’m trying javascript. I’m not sure if it’s working or how to tell what’s happening.

Here’s the test:

class ChatAgentCreatesProjectTest extends KaribuTest {
    @Autowired
    ProjectRepository projectRepository;

    @Test
    void chatAgentCreatesProject() throws Exception {
        assertThat(projectRepository.count()).isEqualTo(0);
        loginAndNavigate(ProjectsView.class);
        MessageInput messageInput = _get(MessageInput.class, spec -> spec.withId("message-input"));
        messageInput.getElement().setText("please create a new project called Vroomba");
        messageInput.getElement().executeJs("this.shadowRoot.querySelector('textarea').dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'}));");
        Thread.sleep(2000);

        MessageList messageList = _get(MessageList.class, spec -> spec.withId("message-list"));
        assertThat(messageList.getItems()).hasSize(2);  // This is where the test fails.
        assertThat(messageList.getItems().get(0).getText()).isEqualTo("please create a new project called Vroomba");
        assertThat(messageList.getItems().get(1).getText()).isEqualTo("Success! I have created a new project called Vroomba");
    }
}

Here’s where I’m using the MessageList:

public class RightDrawer extends VerticalLayout {

    private final List<ChatMessage> messages;
    private final List<MessageListItem> items;
    private final MessageList messageList;
    private final AiGateway aiGateway;
    private final AuthenticatedUser authenticatedUser;

    public RightDrawer(AiGateway aiGateway, AuthenticatedUser authenticatedUser) {
        this.aiGateway = aiGateway;
        this.authenticatedUser = authenticatedUser;
        this.messages = new ArrayList<>();
        this.items = new ArrayList<>();
        this.messageList = new MessageList(items);
        this.messageList.setId("message-list");
        createDrawer();
    }

    private void createDrawer() {
        setId("right-drawer-layout");
        setHeight("100%");
        setWidth("500px");
        addClassNames(LumoUtility.Background.CONTRAST_10);
        Scroller messageScroller = createMessageScroller();
        MessageInput messageInput = createMessageInput(messageScroller);
        add(messageScroller, messageInput);
    }

    private Scroller createMessageScroller() {
        Scroller scroller = new Scroller(messageList);
        scroller.setSizeFull();
        return scroller;
    }

    private MessageInput createMessageInput(Scroller messageScroller) {
        MessageInput messageInput = new MessageInput();
        messageInput.setId("message-input");
        messageInput.setWidthFull();
        ComponentUtil.addListener(messageInput, TimestampedSubmitEvent.class,
                event -> handleMessageSubmit(event, messageScroller));
        return messageInput;
    }

    private void handleMessageSubmit(TimestampedSubmitEvent event, Scroller messageScroller) {
        String userMessageText = event.getValue();
        LocalDateTime creationTime = event.getLocalDateTime();
        String timestamp = event.getTimestamp();
        String timezone = event.getTimezone();
        Instant timestampInstant = timestamp != null ? Instant.parse(timestamp) : Instant.now();

        try {
            Account account = authenticatedUser.getCurrentAccount();
            String userName = account.person().name();
            String userEmail = account.username();
            String userAvatar = account.getProfilePictureUri();
            AccountId chatId = account.id();

            MessageListItem userMessage = new MessageListItem(userMessageText, timestampInstant, userName, userAvatar);

            UserMessageDto userMessageDto = new UserMessageDto(timestampInstant, creationTime, userName,
                    userMessageText, chatId, timezone, userEmail);

            appendMessageAndReply(userMessage, messageScroller, userMessageDto);
        } catch (UserNotAuthenticatedException ex) {
            UI.getCurrent().navigate(LoginView.class);
        }
    }

    private void appendMessageAndReply(MessageListItem userMessage, Scroller messageScroller, UserMessageDto userMessageDto) {
        getUI().ifPresent(ui -> ui.access(() -> {
            addMessagesToUI(userMessage);
            MessageListItem reply = new MessageListItem("", Instant.now(), "Assistant");
            appendReplyMessages(reply, messageScroller, userMessageDto);
        }));
    }

    private void addMessagesToUI(MessageListItem userMessage) {
        items.add(userMessage);
        messageList.setItems(items);
        messages.add(new UserMessage(userMessage.getText()));
    }

    private void appendReplyMessages(MessageListItem reply, Scroller messageScroller, UserMessageDto userMessageDto) {
        items.add(reply);
        messageList.setItems(items);
        Flux<String> contentStream = aiGateway.sendMessageAndReceiveReplies(userMessageDto);
        // Display error message to the user if needed
        contentStream.subscribe(
                content -> updateReplyContent(reply, content),
                Throwable::printStackTrace,
                () -> finalizeReply(reply)
        );
        scrollToBottom(messageScroller);
    }

    private void updateReplyContent(MessageListItem reply, String content) {
        getUI().ifPresent(ui -> ui.access(() -> {
            reply.setText(reply.getText() + content);
            messageList.setItems(items);
        }));
    }

    private void finalizeReply(MessageListItem reply) {
        getUI().ifPresent(ui -> ui.access(() -> {
            messages.add(new AiMessage(reply.getText()));
            messageList.setItems(items);
        }));
    }

    private void scrollToBottom(Scroller scroller) {
        scroller.getElement().executeJs("this.scrollTo(0, this.scrollHeight);");
    }
}
private void appendReplyMessages(MessageListItem reply, Scroller messageScroller, UserMessageDto userMessageDto) {
        items.add(reply);
        messageList.setItems(items);
        Flux<String> contentStream = aiGateway.sendMessageAndReceiveReplies(userMessageDto);
        // Display error message to the user if needed
        contentStream.subscribe(
                content -> updateReplyContent(reply, content),
                Throwable::printStackTrace,
                () -> finalizeReply(reply)
        );
        scrollToBottom(messageScroller);
    }

and blocking the reactive stream how does it behave ?
Is it possible to invoke the block() method instead of subscribing ?

it may be that when subscribing, the stream is not completely executed at the testing level.

I can’t replicate your case

maybe another

 assertThat(projectRepository.count()).isEqualTo(0);
        loginAndNavigate(ProjectsView.class);
        MessageInput messageInput = _get(MessageInput.class, spec -> spec.withId("message-input"));
        messageInput.getElement().setText("please create a new project called Vroomba");
        messageInput.getElement().executeJs("this.shadowRoot.querySelector('textarea').dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'}));");
        Thread.sleep(2000);

        MessageList messageList = _get(MessageList.class, spec -> spec.withId("message-list"));
        Thread.sleep(3000); //sleep the main Thread again
        assertThat(messageList.getItems()).hasSize(2);  // This is where the test fails.
        assertThat(messageList.getItems().get(0).getText()).isEqualTo("please create a new project called Vroomba");
        assertThat(messageList.getItems().get(1).getText()).isEqualTo("Success! I have created a new project called Vroomba");
1 Like

You cannot use Javascript with Karibu, since there is no browser that will execute it.
IIRC, one you get the MessageInput instance, you should be able to call messageInput._submit("some text"), that simulates the client interaction by firing the SubmitEvent on the server side.

2 Likes

Interestingly, this test passes:

public class MessageInputTest {

    private static final Logger logger = LoggerFactory.getLogger(MessageInputTest.class);
    private MessageInput messageInput;
    private MessageList messageList;
    private List<MessageListItem> items;

    @BeforeEach
    void setUp() {
        messageInput = new MessageInput();
        items = new ArrayList<>();
        messageList = new MessageList(items);

        ComponentUtil.addListener(messageInput, MessageInput.SubmitEvent.class, (ComponentEventListener<MessageInput.SubmitEvent>) event -> {
            items.add(new MessageListItem(event.getValue(), null, "User"));
            messageList.setItems(items);
        });
    }

    @Test
    void testMessageSubmission() {
        String testMessage = "please create a new project called Vroomba";
        messageInput.getElement().setText(testMessage);
        ComponentUtil.fireEvent(messageInput, new MessageInput.SubmitEvent(messageInput, false, testMessage));

        assertThat(messageList.getItems()).hasSize(1);
        assertThat(messageList.getItems().getFirst().getText()).isEqualTo(testMessage);
    }
}

But if I try to use ComponentUtil.fireEvent)) in the other test, it still fails. I guess it has something to do with the way I’m instantiating RightDrawer? Unsure.

@Test
    void chatAgentCreatesProject() throws Exception {
        assertThat(projectRepository.count()).isEqualTo(0);
        loginAndNavigate(ProjectsView.class);

        // Get the MessageInput
        MessageInput messageInput = _get(MessageInput.class, spec -> spec.withId("message-input"));
        String testMessage = "please create a new project called Vroomba";
        log.info("Setting message input value to: {}", testMessage);

        // Set the message text
        messageInput.getElement().setText(testMessage);

        // Log the current text in message input for verification
        String currentText = messageInput.getElement().getText();
        log.info("Current message input value: {}", currentText);
        assertThat(currentText).isEqualTo(testMessage);

        // Fire the submit event
        log.info("Firing submit event with text: {}", testMessage);
        ComponentUtil.fireEvent(messageInput, new MessageInput.SubmitEvent(messageInput, false, testMessage));

        // Wait for backend processing
        Thread.sleep(2000);

        // Retrieve and inspect the MessageList
        MessageList messageList = _get(MessageList.class, spec -> spec.withId("message-list"));
        log.info("Retrieved message list items: {}", messageList.getItems());
        assertThat(messageList.getItems()).hasSize(2);
        assertThat(messageList.getItems().get(0).getText()).isEqualTo(testMessage);
        assertThat(messageList.getItems().get(1).getText()).isEqualTo("Success! I have created a new project called Vroomba");
    }

Hi Rubén,

Thank you for your helpful suggestions. Here’s an update on what I’ve tried based on your recommendations:

  1. Blocking the Reactive Stream: I modified the appendReplyMessages method to use blockLast() instead of subscribing to the Flux<String>, ensuring the reactive stream completes before proceeding further. Here’s the updated method:
private void appendReplyMessages(MessageListItem reply, Scroller messageScroller, UserMessageDto userMessageDto) {
    items.add(reply);
    messageList.setItems(items);

    // Use blockLast() to ensure the stream completes before moving on
    aiGateway.sendMessageAndReceiveReplies(userMessageDto)
             .doOnNext(content -> updateReplyContent(reply, content))
             .doOnError(Throwable::printStackTrace)
             .doOnComplete(() -> finalizeReply(reply))
             .blockLast();

    scrollToBottom(messageScroller);
}
  1. Simulating User Input via Keyboard Event: I updated my test to simulate pressing the ‘Enter’ key directly on the textarea within the MessageInput using executeJs. Here’s the updated test code:
@Test
void chatAgentCreatesProject() throws Exception {
    assertThat(projectRepository.count()).isEqualTo(0);
    loginAndNavigate(ProjectsView.class);

    // Get the MessageInput
    MessageInput messageInput = _get(MessageInput.class, spec -> spec.withId("message-input"));
    String testMessage = "please create a new project called Vroomba";
    log.info("Setting message input value to: {}", testMessage);

    // Set the message text
    messageInput.getElement().setText(testMessage);

    // Log the current text in message input for verification
    String currentText = messageInput.getElement().getText();
    log.info("Current message input value: {}", currentText);
    assertThat(currentText).isEqualTo(testMessage);

    // Simulate pressing Enter to submit the message
    log.info("Simulating Enter key press on message input");
    messageInput.getElement().executeJs(
        "this.shadowRoot.querySelector('textarea').dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'}));"
    );

    // Wait for backend processing
    Thread.sleep(2000);

    // Retrieve and inspect the MessageList
    MessageList messageList = _get(MessageList.class, spec -> spec.withId("message-list"));
    log.info("Retrieved message list items: {}", messageList.getItems());
    assertThat(messageList.getItems()).hasSize(2);
    assertThat(messageList.getItems().get(0).getText()).isEqualTo(testMessage);
    assertThat(messageList.getItems().get(1).getText()).isEqualTo("Success! I have created a new project called Vroomba");
}

Despite these changes, the test still fails at the same point where it checks the message list items. It seems like the messages are not being added to the list as expected.

Do you have any additional thoughts or suggestions on what might be causing this? Any further guidance would be greatly appreciated.

Thank you for your time and assistance!

I don’t understand your view code. Why are you adding a listener with ComponentUtil? Is TimestampedSubmitEvent a custom event? Why are you not using MessageInput.addSubmitListener ?

1 Like

mmm well at this point, I wouldn’t know, I think it’s better to go for Marco’s recommendations.

Apparently it has nothing to do with the project reactor.

Adding that I was just telling you to block the stream for this test, but it is always better to subscribe.

1 Like

Thanks Marco.

Why are you adding a listener with ComponentUtil?

Yes TimestampedSubmitEvent is a custom event that allows me to get the time that the client submitted the event, not when it was received. This is important for some business logic later on. I’ll paste it below for context.

Why are you not using MessageInput.addSubmitListener ?

I can’t remember. Will that give me the client side event submit time?

public class TimestampedSubmitEvent extends SubmitEvent {
    private final String timestamp;
    private final String timezone;
    private final LocalDateTime localDateTime;

    public TimestampedSubmitEvent(MessageInput source, boolean fromClient,
                                  @EventData("event.detail.value") String value,
                                  @EventData("new Date().toISOString()") String timestamp,
                                  @EventData("Intl.DateTimeFormat().resolvedOptions().timeZone") String timezone) {
        super(source, fromClient, value);
        this.timestamp = timestamp;
        this.timezone = timezone;
        this.localDateTime = timestamp != null ? LocalDateTime.parse(timestamp, DateTimeFormatter.ISO_DATE_TIME) : LocalDateTime.now();
    }
// getters removed
}

IIRC SubmitEvent does not have any timestamp associated.

I suppose your event class is annotated with @DomEvent("submit"), to make it work.
If you are listening for a custom event, with Karibu testing you need to programmatically fire that event simulating an interaction on the browser. As previously mentioned, Javascript expressions are not executed with Karibu, since there is no browser that can handle them.

As an example, look at the implementation in Karibu for simulating submit from a MessageInput:

If you want your listener for the custom event to be triggered, you need to fire the event, e.g. ComponentUtil.fireEvent(messageInput, new TimestampedSubmitEvent(messageInput, true, testMessage, timestamp, timezone));

1 Like

Thanks so much for your help with this Marco. I never considered that the javascript event was getting in the way.

Here’s the final test that passes now:

class ChatAgentCreatesProjectTest extends KaribuTest {
    @Autowired
    AccountRepository accountRepository;
    private static final Logger log = LoggerFactory.getLogger(ChatAgentCreatesProjectTest.class);

    @Test
    void chatAgentCreatesProject() throws Exception {
        accountRepository.deleteAll();
        accountRepository.save(Account.createTestDouble());
        loginAndNavigate(ProjectsView.class);

        MessageInput messageInput = _get(MessageInput.class, spec -> spec.withId("message-input"));
        String testMessage = "please create a new project called Vroomba";
        messageInput.getElement().setText(testMessage);
        String timestamp = Instant.now().toString();
        String timezone = ZoneId.systemDefault().getId();
        ComponentUtil.fireEvent(messageInput, new TimestampedSubmitEvent(
                messageInput, true, testMessage, timestamp, timezone));

        MessageList messageList = _get(MessageList.class, spec -> spec.withId("message-list"));
        log.info("Retrieved message list items: {}", messageList.getItems());
        assertThat(messageList.getItems()).hasSize(2);
        assertThat(messageList.getItems().get(0).getText()).isEqualTo(testMessage);
        assertThat(messageList.getItems().get(1).getText()).isEqualTo("Success! I have created a new project called Vroomba");
    }
}

I suppose your event class is annotated with @DomEvent("submit") , to make it work.

I don’t think so. I don’t know what that annotation is.

I don’t think so. I don’t know what that annotation is.

So, are you firing your custom event programmatically also in the application code?
Otherwise, without the DomEvent annotation on the event class, there will nothing on the client-side capable of firing the event.

https://vaadin.com/docs/latest/flow/create-ui/creating-components/events#firing-events-from-the-client

1 Like

To be honest, I’m not entirely sure how to answer your question. I’ve shared all the relevant code I could think of unless there’s something I might have overlooked.

It seems like you’re referring to the TimestampedSubmitEvent—if so, I did share it here. If I’ve misunderstood your question, could you clarify what specifically you’d like me to address?

Maybe you’re suggesting that I update TimestampedSubmitEvent to include something like this?

@DomEvent("submit")
public class TimestampedSubmitEvent extends ComponentEvent<MessageInput> {
    // event implementation
}

Probably I did not understand correctly your goals.
What I mean is that the code you posted is all about testing without a browser, and it works fine because you are programmatically simulating your custom event being fired.

However, once you will run the application and interact with it through the browser, your custom event will not be fired, if it does not have a @DomEvent() annotation on it (unless you plan to fire it programmatically also in the application, of course).