Testing Interactions
Test buttons, select menus, and modals
Test component interactions like buttons, select menus, and modals. These follow the same pattern as commands but use different interaction types.
Examples on this page assume you have a test session set up. See Automated Testing for session setup and Interaction Type Constants for a reference of numeric type values used below.
Stage UI showing a message with interactive button components and a select menu, demonstrating component interactions in action
Button Clicks
Basic Button
import { dispatchInteraction, expectAction } from '@robojs/mock/testing'
it('handles button click', async () => {
await dispatchInteraction(session.id, {
type: 3, // MESSAGE_COMPONENT
data: {
component_type: 2, // BUTTON
custom_id: 'confirm-action'
},
channel_id: session.channels[0].id
})
await expectAction(session.id, {
description: 'Bot should acknowledge button',
type: 'interaction_response',
expected: {
response_type: 6 // DEFERRED_UPDATE_MESSAGE
}
})
})import { dispatchInteraction, expectAction } from '@robojs/mock/testing'
it('handles button click', async () => {
await dispatchInteraction(session.id, {
type: 3, // MESSAGE_COMPONENT
data: {
component_type: 2, // BUTTON
custom_id: 'confirm-action'
},
channel_id: session.channels[0].id
})
await expectAction(session.id, {
description: 'Bot should acknowledge button',
type: 'interaction_response',
expected: {
response_type: 6 // DEFERRED_UPDATE_MESSAGE
}
})
})Button with Data
Pass data through custom_id:
await dispatchInteraction(session.id, {
type: 3, // MESSAGE_COMPONENT
data: {
component_type: 2, // BUTTON
custom_id: 'delete:123:456' // action:userId:itemId
},
channel_id: session.channels[0].id
})await dispatchInteraction(session.id, {
type: 3, // MESSAGE_COMPONENT
data: {
component_type: 2, // BUTTON
custom_id: 'delete:123:456' // action:userId:itemId
},
channel_id: session.channels[0].id
})Update vs Reply
Discord supports two response types for component interactions. Update (type 7) replaces the existing message the component is attached to. Reply (type 4) sends a new message in the channel.
// Update existing message (replaces the message content in-place)
await expectAction(session.id, {
description: 'Button should update message',
type: 'interaction_response',
expected: {
response_type: 7 // UPDATE_MESSAGE
}
})
// Reply with new message (sends a new message to the channel)
await expectAction(session.id, {
description: 'Button should reply',
type: 'interaction_response',
expected: {
response_type: 4 // CHANNEL_MESSAGE_WITH_SOURCE
}
})// Update existing message (replaces the message content in-place)
await expectAction(session.id, {
description: 'Button should update message',
type: 'interaction_response',
expected: {
response_type: 7 // UPDATE_MESSAGE
}
})
// Reply with new message (sends a new message to the channel)
await expectAction(session.id, {
description: 'Button should reply',
type: 'interaction_response',
expected: {
response_type: 4 // CHANNEL_MESSAGE_WITH_SOURCE
}
})Select Menus
String Select
await dispatchInteraction(session.id, {
type: 3, // MESSAGE_COMPONENT
data: {
component_type: 3, // STRING_SELECT
custom_id: 'role-select',
values: ['admin', 'moderator'] // Selected values
},
channel_id: session.channels[0].id
})
await expectAction(session.id, {
description: 'Bot should assign selected roles',
type: 'interaction_response',
expected: {
response_data: {
content: expect.stringContaining('admin')
}
}
})await dispatchInteraction(session.id, {
type: 3, // MESSAGE_COMPONENT
data: {
component_type: 3, // STRING_SELECT
custom_id: 'role-select',
values: ['admin', 'moderator'] // Selected values
},
channel_id: session.channels[0].id
})
await expectAction(session.id, {
description: 'Bot should assign selected roles',
type: 'interaction_response',
expected: {
response_data: {
content: expect.stringContaining('admin')
}
}
})User Select
// These are mock IDs -- any snowflake-format string works for testing
const selectedUserId = '111222333444555666'
const selectedUsername = 'TestUser'
await dispatchInteraction(session.id, {
type: 3, // MESSAGE_COMPONENT
data: {
component_type: 5, // USER_SELECT
custom_id: 'user-select',
values: [selectedUserId],
resolved: {
users: {
[selectedUserId]: {
id: selectedUserId,
username: selectedUsername
}
}
}
},
channel_id: session.channels[0].id
})// These are mock IDs -- any snowflake-format string works for testing
const selectedUserId = '111222333444555666'
const selectedUsername = 'TestUser'
await dispatchInteraction(session.id, {
type: 3, // MESSAGE_COMPONENT
data: {
component_type: 5, // USER_SELECT
custom_id: 'user-select',
values: [selectedUserId],
resolved: {
users: {
[selectedUserId]: {
id: selectedUserId,
username: selectedUsername
}
}
}
},
channel_id: session.channels[0].id
})Channel Select
const selectedChannel = session.channels[0]
await dispatchInteraction(session.id, {
type: 3, // MESSAGE_COMPONENT
data: {
component_type: 8, // CHANNEL_SELECT
custom_id: 'channel-select',
values: [selectedChannel.id],
resolved: {
channels: {
[selectedChannel.id]: {
id: selectedChannel.id,
name: selectedChannel.name,
type: selectedChannel.type
}
}
}
},
channel_id: session.channels[0].id
})const selectedChannel = session.channels[0]
await dispatchInteraction(session.id, {
type: 3, // MESSAGE_COMPONENT
data: {
component_type: 8, // CHANNEL_SELECT
custom_id: 'channel-select',
values: [selectedChannel.id],
resolved: {
channels: {
[selectedChannel.id]: {
id: selectedChannel.id,
name: selectedChannel.name,
type: selectedChannel.type
}
}
}
},
channel_id: session.channels[0].id
})Role Select
// Define a mock role ID for testing
const roleId = '999888777666555444'
await dispatchInteraction(session.id, {
type: 3, // MESSAGE_COMPONENT
data: {
component_type: 6, // ROLE_SELECT
custom_id: 'role-picker',
values: [roleId],
resolved: {
roles: {
[roleId]: {
id: roleId,
name: 'Moderator',
color: 0x3498db
}
}
}
},
channel_id: session.channels[0].id
})// Define a mock role ID for testing
const roleId = '999888777666555444'
await dispatchInteraction(session.id, {
type: 3, // MESSAGE_COMPONENT
data: {
component_type: 6, // ROLE_SELECT
custom_id: 'role-picker',
values: [roleId],
resolved: {
roles: {
[roleId]: {
id: roleId,
name: 'Moderator',
color: 0x3498db
}
}
}
},
channel_id: session.channels[0].id
})Mentionable Select
Combines users and roles:
// Define mock IDs for both a user and a role
const userId = '111222333444555666'
const roleId = '999888777666555444'
await dispatchInteraction(session.id, {
type: 3, // MESSAGE_COMPONENT
data: {
component_type: 7, // MENTIONABLE_SELECT
custom_id: 'mention-select',
values: [userId, roleId],
resolved: {
users: {
[userId]: {
id: userId,
username: 'TestUser',
discriminator: '0',
avatar: null
}
},
roles: {
[roleId]: {
id: roleId,
name: 'Moderator',
color: 0x3498db
}
}
}
},
channel_id: session.channels[0].id
})// Define mock IDs for both a user and a role
const userId = '111222333444555666'
const roleId = '999888777666555444'
await dispatchInteraction(session.id, {
type: 3, // MESSAGE_COMPONENT
data: {
component_type: 7, // MENTIONABLE_SELECT
custom_id: 'mention-select',
values: [userId, roleId],
resolved: {
users: {
[userId]: {
id: userId,
username: 'TestUser',
discriminator: '0',
avatar: null
}
},
roles: {
[roleId]: {
id: roleId,
name: 'Moderator',
color: 0x3498db
}
}
}
},
channel_id: session.channels[0].id
})Modal Submissions
Basic Modal
await dispatchInteraction(session.id, {
type: 5, // MODAL_SUBMIT
data: {
custom_id: 'feedback-modal',
components: [{
type: 1, // ACTION_ROW
components: [{
type: 4, // TEXT_INPUT
custom_id: 'feedback-input',
value: 'Great service'
}]
}]
},
channel_id: session.channels[0].id
})
await expectAction(session.id, {
description: 'Bot should process feedback',
type: 'interaction_response',
expected: {
response_data: {
content: expect.stringContaining('Thank you')
}
}
})await dispatchInteraction(session.id, {
type: 5, // MODAL_SUBMIT
data: {
custom_id: 'feedback-modal',
components: [{
type: 1, // ACTION_ROW
components: [{
type: 4, // TEXT_INPUT
custom_id: 'feedback-input',
value: 'Great service'
}]
}]
},
channel_id: session.channels[0].id
})
await expectAction(session.id, {
description: 'Bot should process feedback',
type: 'interaction_response',
expected: {
response_data: {
content: expect.stringContaining('Thank you')
}
}
})Stage UI showing a modal form dialog open with text input fields, a title bar, and Submit/Cancel buttons at the bottom
Multiple Inputs
await dispatchInteraction(session.id, {
type: 5, // MODAL_SUBMIT
data: {
custom_id: 'application-form',
components: [
{
type: 1, // ACTION_ROW
components: [{
type: 4, // TEXT_INPUT
custom_id: 'name',
value: 'John Doe'
}]
},
{
type: 1, // ACTION_ROW
components: [{
type: 4, // TEXT_INPUT
custom_id: 'experience',
value: '5 years of development'
}]
},
{
type: 1, // ACTION_ROW
components: [{
type: 4, // TEXT_INPUT
custom_id: 'reason',
value: 'I want to contribute to the community'
}]
}
]
},
channel_id: session.channels[0].id
})await dispatchInteraction(session.id, {
type: 5, // MODAL_SUBMIT
data: {
custom_id: 'application-form',
components: [
{
type: 1, // ACTION_ROW
components: [{
type: 4, // TEXT_INPUT
custom_id: 'name',
value: 'John Doe'
}]
},
{
type: 1, // ACTION_ROW
components: [{
type: 4, // TEXT_INPUT
custom_id: 'experience',
value: '5 years of development'
}]
},
{
type: 1, // ACTION_ROW
components: [{
type: 4, // TEXT_INPUT
custom_id: 'reason',
value: 'I want to contribute to the community'
}]
}
]
},
channel_id: session.channels[0].id
})Paragraph Input
await dispatchInteraction(session.id, {
type: 5, // MODAL_SUBMIT
data: {
custom_id: 'report-modal',
components: [{
type: 1, // ACTION_ROW
components: [{
type: 4, // TEXT_INPUT
custom_id: 'description',
value: 'This is a detailed description\nwith multiple lines\nof text.'
}]
}]
},
channel_id: session.channels[0].id
})await dispatchInteraction(session.id, {
type: 5, // MODAL_SUBMIT
data: {
custom_id: 'report-modal',
components: [{
type: 1, // ACTION_ROW
components: [{
type: 4, // TEXT_INPUT
custom_id: 'description',
value: 'This is a detailed description\nwith multiple lines\nof text.'
}]
}]
},
channel_id: session.channels[0].id
})Component Chains
Test multi-step interaction flows:
it('completes multi-step flow', async () => {
// Step 1: Run command that shows buttons
await dispatchInteraction(session.id, {
type: 2, // APPLICATION_COMMAND
data: { name: 'setup', type: 1 }, // CHAT_INPUT
channel_id: session.channels[0].id
})
await expectAction(session.id, {
description: 'Bot shows setup options',
type: 'interaction_response',
expected: {
response_data: {
components: expect.arrayContaining([
expect.objectContaining({ type: 1 }) // ACTION_ROW
])
}
}
})
// Step 2: Click configure button
await dispatchInteraction(session.id, {
type: 3, // MESSAGE_COMPONENT
data: {
component_type: 2, // BUTTON
custom_id: 'setup:configure'
},
channel_id: session.channels[0].id
})
// Step 3: Submit modal
await dispatchInteraction(session.id, {
type: 5, // MODAL_SUBMIT
data: {
custom_id: 'setup:modal',
components: [{
type: 1, // ACTION_ROW
components: [{
type: 4, // TEXT_INPUT
custom_id: 'setting',
value: 'enabled'
}]
}]
},
channel_id: session.channels[0].id
})
await expectAction(session.id, {
description: 'Setup completes successfully',
type: 'interaction_response',
expected: {
response_data: {
content: expect.stringContaining('Setup complete')
}
}
})
})it('completes multi-step flow', async () => {
// Step 1: Run command that shows buttons
await dispatchInteraction(session.id, {
type: 2, // APPLICATION_COMMAND
data: { name: 'setup', type: 1 }, // CHAT_INPUT
channel_id: session.channels[0].id
})
await expectAction(session.id, {
description: 'Bot shows setup options',
type: 'interaction_response',
expected: {
response_data: {
components: expect.arrayContaining([
expect.objectContaining({ type: 1 }) // ACTION_ROW
])
}
}
})
// Step 2: Click configure button
await dispatchInteraction(session.id, {
type: 3, // MESSAGE_COMPONENT
data: {
component_type: 2, // BUTTON
custom_id: 'setup:configure'
},
channel_id: session.channels[0].id
})
// Step 3: Submit modal
await dispatchInteraction(session.id, {
type: 5, // MODAL_SUBMIT
data: {
custom_id: 'setup:modal',
components: [{
type: 1, // ACTION_ROW
components: [{
type: 4, // TEXT_INPUT
custom_id: 'setting',
value: 'enabled'
}]
}]
},
channel_id: session.channels[0].id
})
await expectAction(session.id, {
description: 'Setup completes successfully',
type: 'interaction_response',
expected: {
response_data: {
content: expect.stringContaining('Setup complete')
}
}
})
})Stage UI showing a multi-step interaction flow: a slash command response with buttons, followed by a button click resulting in an updated message or new response
Ephemeral Interactions
Test ephemeral message handling:
await dispatchInteraction(session.id, {
type: 3, // MESSAGE_COMPONENT
data: {
component_type: 2, // BUTTON
custom_id: 'secret-info'
},
channel_id: session.channels[0].id
})
await expectAction(session.id, {
description: 'Response should be ephemeral',
type: 'interaction_response',
expected: {
response_data: {
flags: 64 // EPHEMERAL
}
}
})await dispatchInteraction(session.id, {
type: 3, // MESSAGE_COMPONENT
data: {
component_type: 2, // BUTTON
custom_id: 'secret-info'
},
channel_id: session.channels[0].id
})
await expectAction(session.id, {
description: 'Response should be ephemeral',
type: 'interaction_response',
expected: {
response_data: {
flags: 64 // EPHEMERAL
}
}
})Interaction Context
Specify which user triggers the interaction:
await dispatchInteraction(session.id, {
type: 3, // MESSAGE_COMPONENT
data: {
component_type: 2, // BUTTON
custom_id: 'admin-action'
},
channel_id: session.channels[0].id,
user: {
id: '111', // Mock user ID
username: 'Admin'
}
})await dispatchInteraction(session.id, {
type: 3, // MESSAGE_COMPONENT
data: {
component_type: 2, // BUTTON
custom_id: 'admin-action'
},
channel_id: session.channels[0].id,
user: {
id: '111', // Mock user ID
username: 'Admin'
}
})The mock server generates a guild member for the specified user automatically. If no user is provided, the session's current user is used.
