Skip to content

Sessions & State

Session counter with mockSession

mockSession injects a mutable session object into ctx.session. You can pre-seed it and read/write it directly in your tests.

Bot:

ts
// bot.ts
import { Bot, session } from 'grammy';
import type { SessionContext } from 'grammy-testing';

export interface CounterSession {
  count: number;
}
export type CounterContext = SessionContext<CounterSession>;

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

  bot.use(session({ initial: (): CounterSession => ({ count: 0 }) }));

  bot.command('count', async (ctx) => {
    ctx.session.count += 1;
    await ctx.reply(`Count: ${ctx.session.count}`);
  });

  bot.command('reset', async (ctx) => {
    ctx.session.count = 0;
    await ctx.reply('Counter reset.');
  });

  return bot;
}

Test with mockSession:

ts
import { mockSession, prepareBot } from 'grammy-testing';
import { Bot } from 'grammy';
import { describe, expect, it } from 'vitest';
import type { CounterContext, CounterSession } from './bot';

describe('session counter', () => {
  it('increments on each /count', async () => {
    const { session, mockSessionMiddleware } = mockSession<CounterSession, CounterContext>({ count: 0 });

    const bot = new Bot<CounterContext>('token');
    bot.use(mockSessionMiddleware); // inject before handlers

    bot.command('count', async (ctx) => {
      ctx.session.count += 1;
      await ctx.reply(`Count: ${ctx.session.count}`);
    });

    const { chats } = await prepareBot(bot);
    const user = chats.newUser();

    await user.sendCommand('/count');
    await user.sendCommand('/count');
    await user.sendCommand('/count');

    expect(user.replies.lastOrThrow().text).toBe('Count: 3');
    expect(session.count).toBe(3); // direct read
  });

  it('starts from a pre-set value', async () => {
    const { mockSessionMiddleware } = mockSession<CounterSession, CounterContext>({ count: 9 });
    const bot = new Bot<CounterContext>('token');
    bot.use(mockSessionMiddleware);
    bot.command('count', async (ctx) => {
      ctx.session.count += 1;
      await ctx.reply(`Count: ${ctx.session.count}`);
    });

    const { chats } = await prepareBot(bot);
    const user = chats.newUser();

    await user.sendCommand('/count');

    expect(user.replies.lastOrThrow().text).toBe('Count: 10');
  });
});

Per-chat session with mockChatSession

ts
import { mockChatSession } from 'grammy-testing';

interface ChatSettings {
  language: string;
}

const { chatSession, mockChatSessionMiddleware } = mockChatSession<ChatSettings, MyContext>({ language: 'uk' });

bot.use(mockChatSessionMiddleware);

ctx.state via prepareComposer

Use state in prepareComposer options to pre-populate ctx.state for every update:

ts
import { prepareComposer } from 'grammy-testing';
import { Composer } from 'grammy';

interface MyState {
  isAdmin: boolean;
}

const composer = new Composer<MyContext>();

composer.command('admin-action', async (ctx) => {
  if (!ctx.state.isAdmin) {
    await ctx.reply('Forbidden.');
    return;
  }
  await ctx.reply('Done!');
});

const { chats } = await prepareComposer(composer, {
  state: { isAdmin: true },
});

const user = chats.newUser();
await user.sendCommand('/admin-action');
expect(user.replies.lastOrThrow().text).toBe('Done!');

Released under the MIT License.