Skip to content

Keyboards & Buttons

Inline keyboard

Bot:

ts
import { Bot, InlineKeyboard } from 'grammy';

export function createBot() {
  const bot = new Bot('token');

  bot.command('menu', async (ctx) => {
    const keyboard = new InlineKeyboard().text('Yes', 'answer:yes').text('No', 'answer:no');

    await ctx.reply('Do you like grammY?', { reply_markup: keyboard });
  });

  bot.callbackQuery('answer:yes', async (ctx) => {
    await ctx.editMessageText('Great choice!');
    await ctx.answerCallbackQuery();
  });

  bot.callbackQuery('answer:no', async (ctx) => {
    await ctx.editMessageText('Give it a try!');
    await ctx.answerCallbackQuery();
  });

  return bot;
}

Test:

ts
import { prepareBot } from 'grammy-testing';
import { describe, expect, it } from 'vitest';

describe('inline keyboard', () => {
  it('sends a keyboard with two buttons', async () => {
    const { chats } = await prepareBot(createBot());
    const user = chats.newUser();

    await user.sendCommand('/menu');

    const reply = user.replies.lastOrThrow();

    expect(reply.text).toBe('Do you like grammY?');
    expect(reply.buttons).toHaveLength(2);
    expect(reply.buttons[0].text).toBe('Yes');
    expect(reply.buttons[1].text).toBe('No');
  });

  it('edits the message when Yes is clicked', async () => {
    const { chats } = await prepareBot(createBot());
    const user = chats.newUser();

    await user.sendCommand('/menu');
    const reply = user.replies.lastOrThrow();

    await reply.clickButton('Yes');

    expect(chats.editsFor(user).lastOrThrow().text).toBe('Great choice!');
  });

  it('each button has callback_data', async () => {
    const { chats } = await prepareBot(createBot());
    const user = chats.newUser();

    await user.sendCommand('/menu');
    const reply = user.replies.lastOrThrow();

    expect(reply.buttons[0].callbackData).toBe('answer:yes');
    expect(reply.buttons[1].callbackData).toBe('answer:no');
  });
});

clickButton by callback data

ts
// By visible text (default, recommended):
await reply.clickButton('Yes');

// By callback_data explicitly:
await reply.clickButton({ callbackData: 'answer:yes' });

Asserting on button structure

ts
const reply = user.replies.lastOrThrow();

// All buttons
console.log(reply.buttons.map((b) => b.text)); // ['Yes', 'No']

// Check URL buttons
const urlButton = reply.buttons.find((b) => b.url);
expect(urlButton?.url).toBe('https://grammy.dev');

// Access raw InlineKeyboardButton for unusual fields
expect(reply.buttons[0].raw.switch_inline_query).toBeUndefined();

Released under the MIT License.