Repository: Yoctol/bottender
Pull request #468 (2019-10-07 06:47:16)

feat(LineContext): improve types for LineContext
And also clean up some necessary tests.


Patch:
diff --git a/packages/bottender/src/context/LineContext.ts b/packages/bottender/src/context/LineContext.ts
index b7b76334..3ca5eacf 100644
--- a/packages/bottender/src/context/LineContext.ts
+++ b/packages/bottender/src/context/LineContext.ts
@@ -4,7 +4,7 @@ import chunk from 'lodash/chunk';
 import invariant from 'invariant';
 import sleep from 'delay';
 import warning from 'warning';
-import { Line, LineClient } from 'messaging-api-line';
+import { Line, LineClient, LineTypes } from 'messaging-api-line';
 
 import Session from '../session/Session';
 
@@ -37,9 +37,9 @@ class LineContext extends Context implements PlatformContext {
 
   _shouldBatch: boolean;
 
-  _replyMessages = [];
+  _replyMessages: LineTypes.Message[] = [];
 
-  _pushMessages = [];
+  _pushMessages: LineTypes.Message[] = [];
 
   _sendMethod: string;
 
@@ -57,7 +57,7 @@ class LineContext extends Context implements PlatformContext {
     super({ client, event, session, initialState, requestContext, emitter });
     this._customAccessToken = customAccessToken || null;
     this._shouldBatch = shouldBatch || false;
-    this._sendMethod = sendMethod || 'push';
+    this._sendMethod = sendMethod || 'reply';
   }
 
   /**
@@ -148,28 +148,6 @@ class LineContext extends Context implements PlatformContext {
     }
   }
 
-  /**
-   * Send text to the owner of the session.
-   *
-   */
-  async sendText(text: string, options?: Record<string, any>): Promise<any> {
-    if (this._sendMethod === 'reply') {
-      return (this as any).replyText(text, {
-        ...(this._customAccessToken
-          ? { accessToken: this._customAccessToken }
-          : undefined),
-        ...options,
-      });
-    }
-
-    return (this as any).pushText(text, {
-      ...(this._customAccessToken
-        ? { accessToken: this._customAccessToken }
-        : undefined),
-      ...options,
-    });
-  }
-
   /**
    * Leave room or group depending on type of the session.
    *
@@ -445,114 +423,486 @@ class LineContext extends Context implements PlatformContext {
       'issueLinkToken: should not be called in context without session user'
     );
   }
-}
 
-const types: { name: string; arity: number; aliases?: string[] }[] = [
-  { name: '', arity: 3 },
-  { name: 'Text', arity: 3 },
-  { name: 'Image', arity: 3 },
-  { name: 'Video', arity: 3 },
-  { name: 'Audio', arity: 3 },
-  { name: 'Location', arity: 3 },
-  { name: 'Sticker', arity: 3 },
-  { name: 'Imagemap', arity: 4 },
-  { name: 'Flex', arity: 4 },
-  { name: 'Template', arity: 4 },
-  { name: 'ButtonTemplate', aliases: ['ButtonsTemplate'], arity: 4 },
-  { name: 'ConfirmTemplate', arity: 4 },
-  { name: 'CarouselTemplate', arity: 4 },
-  { name: 'ImageCarouselTemplate', arity: 4 },
-];
-
-types.forEach(({ name, arity, aliases }) => {
-  [name].concat(aliases || []).forEach(type => {
-    Object.defineProperty(LineContext.prototype, `reply${type}`, {
-      enumerable: false,
-      configurable: true,
-      writable: true,
-      async value(...args: any[]) {
-        invariant(!this._isReplied, 'Can not reply event multiple times');
+  reply(messages: LineTypes.Message[], options: LineTypes.MessageOptions = {}) {
+    invariant(!this._isReplied, 'Can not reply event multiple times');
 
-        this._isHandled = true;
+    this._isHandled = true;
 
-        if (this._shouldBatch) {
-          if (name === '') {
-            this._replyMessages.push(...args[0]);
-          } else {
-            this._replyMessages.push((Line as any)[`create${name}`](...args));
-          }
-          return;
-        }
+    if (this._shouldBatch) {
+      this._replyMessages.push(...messages);
 
-        this._isReplied = true;
+      return;
+    }
 
-        const options = args[arity - 2];
-        args[arity - 2] = {
-          ...(this._customAccessToken
-            ? { accessToken: this._customAccessToken }
-            : undefined),
-          ...options,
-        };
+    this._isReplied = true;
 
-        return this._client[`reply${name}`](this._event.replyToken, ...args);
-      },
+    // FIXME: throw or warn for no replyToken
+    return this._client.reply(this._event.replyToken as string, messages, {
+      ...(this._customAccessToken
+        ? { accessToken: this._customAccessToken }
+        : undefined),
+      ...options,
     });
+  }
 
-    Object.defineProperty(LineContext.prototype, `push${type}`, {
-      enumerable: false,
-      configurable: true,
-      writable: true,
-      async value(...args: any[]) {
-        if (!this._session) {
-          warning(
-            false,
-            `push${type}: should not be called in context without session`
-          );
-          return;
-        }
+  replyText(text: string, options?: LineTypes.MessageOptions) {
+    return this.reply([Line.createText(text, options)], options);
+  }
 
-        this._isHandled = true;
+  replyImage(
+    image: {
+      originalContentUrl: string;
+      previewImageUrl?: string;
+    },
+    options?: LineTypes.MessageOptions
+  ) {
+    return this.reply([Line.createImage(image, options)], options);
+  }
 
-        if (this._shouldBatch) {
-          if (name === '') {
-            this._pushMessages.push(...args[0]);
-          } else {
-            this._pushMessages.push((Line as any)[`create${name}`](...args));
-          }
-          return;
-        }
+  replyVideo(
+    video: {
+      originalContentUrl: string;
+      previewImageUrl: string;
+    },
+    options?: LineTypes.MessageOptions
+  ) {
+    return this.reply([Line.createVideo(video, options)], options);
+  }
 
-        const options = args[arity - 2];
-        args[arity - 2] = {
-          ...(this._customAccessToken
-            ? { accessToken: this._customAccessToken }
-            : undefined),
+  replyAudio(
+    audio: {
+      originalContentUrl: string;
+      duration: number;
+    },
+    options?: LineTypes.MessageOptions
+  ) {
+    return this.reply([Line.createAudio(audio, options)], options);
+  }
+
+  replyLocation(
+    location: LineTypes.Location,
+    options?: LineTypes.MessageOptions
+  ) {
+    return this.reply([Line.createLocation(location, options)], options);
+  }
+
+  replySticker(
+    sticker: Omit<LineTypes.StickerMessage, 'type'>,
+    options?: LineTypes.MessageOptions
+  ) {
+    return this.reply([Line.createSticker(sticker, options)], options);
+  }
+
+  replyImagemap(
+    altText: string,
+    imagemap: Omit<LineTypes.ImagemapMessage, 'type' | 'altText'>,
+    options?: LineTypes.MessageOptions
+  ) {
+    return this.reply(
+      [Line.createImagemap(altText, imagemap, options)],
+      options
+    );
+  }
+
+  replyFlex(
+    altText: string,
+    flex: LineTypes.FlexContainer,
+    options?: LineTypes.MessageOptions
+  ) {
+    return this.reply([Line.createFlex(altText, flex, options)], options);
+  }
+
+  replyTemplate(
+    altText: string,
+    template: LineTypes.Template,
+    options?: LineTypes.MessageOptions
+  ) {
+    return this.reply(
+      [Line.createTemplate(altText, template, options)],
+      options
+    );
+  }
+
+  replyButtonTemplate(
+    altText: string,
+    buttonTemplate: Omit<LineTypes.ButtonsTemplate, 'type'>,
+    options?: LineTypes.MessageOptions
+  ) {
+    return this.reply(
+      [Line.createButtonTemplate(altText, buttonTemplate, options)],
+      options
+    );
+  }
+
+  replyButtonsTemplate(
+    altText: string,
+    buttonTemplate: Omit<LineTypes.ButtonsTemplate, 'type'>,
+    options?: LineTypes.MessageOptions
+  ) {
+    return this.replyButtonTemplate(altText, buttonTemplate, options);
+  }
+
+  replyConfirmTemplate(
+    altText: string,
+    confirmTemplate: Omit<LineTypes.ConfirmTemplate, 'type'>,
+    options: LineTypes.MessageOptions
+  ) {
+    return this.reply(
+      [Line.createConfirmTemplate(altText, confirmTemplate, options)],
+      options
+    );
+  }
+
+  replyCarouselTemplate(
+    altText: string,
+    columns: LineTypes.ColumnObject[],
+    {
+      imageAspectRatio,
+      imageSize,
+      ...options
+    }: {
+      imageAspectRatio?: 'rectangle' | 'square';
+      imageSize?: 'cover' | 'contain';
+    } & LineTypes.MessageOptions = {}
+  ) {
+    return this.reply(
+      [
+        Line.createCarouselTemplate(altText, columns, {
+          imageAspectRatio,
+          imageSize,
           ...options,
-        };
+        }),
+      ],
+      options
+    );
+  }
 
-        const sessionType = this._session.type;
-        return this._client[`push${name}`](
-          this._session[sessionType].id,
-          ...args
-        );
-      },
-    });
-  });
-});
-
-types
-  .filter(({ name }) => name !== 'Text')
-  .forEach(({ name, aliases }) => {
-    [name].concat(aliases || []).forEach(type => {
-      Object.defineProperty(LineContext.prototype, `send${type}`, {
-        enumerable: false,
-        configurable: true,
-        writable: true,
-        async value(...args: any[]) {
-          return this[`${this._sendMethod}${type}`](...args);
-        },
-      });
+  replyImageCarouselTemplate(
+    altText: string,
+    columns: LineTypes.ImageCarouselColumnObject[],
+    options: LineTypes.MessageOptions
+  ) {
+    return this.reply(
+      [Line.createImageCarouselTemplate(altText, columns, options)],
+      options
+    );
+  }
+
+  push(messages: LineTypes.Message[], options: LineTypes.MessageOptions = {}) {
+    if (!this._session) {
+      warning(false, `push: should not be called in context without session`);
+      return;
+    }
+
+    this._isHandled = true;
+
+    if (this._shouldBatch) {
+      this._pushMessages.push(...messages);
+      return;
+    }
+
+    const sessionType = this._session.type;
+    return this._client.push(this._session[sessionType].id, messages, {
+      ...(this._customAccessToken
+        ? { accessToken: this._customAccessToken }
+        : undefined),
+      ...options,
     });
-  });
+  }
+
+  pushText(text: string, options?: LineTypes.MessageOptions) {
+    return this.push([Line.createText(text, options)], options);
+  }
+
+  pushImage(
+    image: {
+      originalContentUrl: string;
+      previewImageUrl?: string;
+    },
+    options?: LineTypes.MessageOptions
+  ) {
+    return this.push([Line.createImage(image, options)], options);
+  }
+
+  pushVideo(
+    video: {
+      originalContentUrl: string;
+      previewImageUrl: string;
+    },
+    options?: LineTypes.MessageOptions
+  ) {
+    return this.push([Line.createVideo(video, options)], options);
+  }
+
+  pushAudio(
+    audio: {
+      originalContentUrl: string;
+      duration: number;
+    },
+    options?: LineTypes.MessageOptions
+  ) {
+    return this.push([Line.createAudio(audio, options)], options);
+  }
+
+  pushLocation(
+    location: LineTypes.Location,
+    options?: LineTypes.MessageOptions
+  ) {
+    return this.push([Line.createLocation(location, options)], options);
+  }
+
+  pushSticker(
+    sticker: Omit<LineTypes.StickerMessage, 'type'>,
+    options?: LineTypes.MessageOptions
+  ) {
+    return this.push([Line.createSticker(sticker, options)], options);
+  }
+
+  pushImagemap(
+    altText: string,
+    imagemap: Omit<LineTypes.ImagemapMessage, 'type' | 'altText'>,
+    options?: LineTypes.MessageOptions
+  ) {
+    return this.push(
+      [Line.createImagemap(altText, imagemap, options)],
+      options
+    );
+  }
+
+  pushFlex(
+    altText: string,
+    flex: LineTypes.FlexContainer,
+    options?: LineTypes.MessageOptions
+  ) {
+    return this.push([Line.createFlex(altText, flex, options)], options);
+  }
+
+  pushTemplate(
+    altText: string,
+    template: LineTypes.Template,
+    options?: LineTypes.MessageOptions
+  ) {
+    return this.push(
+      [Line.createTemplate(altText, template, options)],
+      options
+    );
+  }
+
+  pushButtonTemplate(
+    altText: string,
+    buttonTemplate: Omit<LineTypes.ButtonsTemplate, 'type'>,
+    options?: LineTypes.MessageOptions
+  ) {
+    return this.push(
+      [Line.createButtonTemplate(altText, buttonTemplate, options)],
+      options
+    );
+  }
+
+  pushButtonsTemplate(
+    altText: string,
+    buttonTemplate: Omit<LineTypes.ButtonsTemplate, 'type'>,
+    options?: LineTypes.MessageOptions
+  ) {
+    return this.pushButtonTemplate(altText, buttonTemplate, options);
+  }
+
+  pushConfirmTemplate(
+    altText: string,
+    confirmTemplate: Omit<LineTypes.ConfirmTemplate, 'type'>,
+    options: LineTypes.MessageOptions
+  ) {
+    return this.push(
+      [Line.createConfirmTemplate(altText, confirmTemplate, options)],
+      options
+    );
+  }
+
+  pushCarouselTemplate(
+    altText: string,
+    columns: LineTypes.ColumnObject[],
+    {
+      imageAspectRatio,
+      imageSize,
+      ...options
+    }: {
+      imageAspectRatio?: 'rectangle' | 'square';
+      imageSize?: 'cover' | 'contain';
+    } & LineTypes.MessageOptions = {}
+  ) {
+    return this.push(
+      [
+        Line.createCarouselTemplate(altText, columns, {
+          imageAspectRatio,
+          imageSize,
+          ...options,
+        }),
+      ],
+      options
+    );
+  }
+
+  pushImageCarouselTemplate(
+    altText: string,
+    columns: LineTypes.ImageCarouselColumnObject[],
+    options: LineTypes.MessageOptions
+  ) {
+    return this.push(
+      [Line.createImageCarouselTemplate(altText, columns, options)],
+      options
+    );
+  }
+
+  send(messages: LineTypes.Message[], options: LineTypes.MessageOptions = {}) {
+    if (this._sendMethod === 'push') {
+      return this.push(messages, options);
+    }
+    return this.reply(messages, options);
+  }
+
+  sendText(text: string, options?: LineTypes.MessageOptions) {
+    return this.send([Line.createText(text, options)], options);
+  }
+
+  sendImage(
+    image: {
+      originalContentUrl: string;
+      previewImageUrl?: string;
+    },
+    options?: LineTypes.MessageOptions
+  ) {
+    return this.send([Line.createImage(image, options)], options);
+  }
+
+  sendVideo(
+    video: {
+      originalContentUrl: string;
+      previewImageUrl: string;
+    },
+    options?: LineTypes.MessageOptions
+  ) {
+    return this.send([Line.createVideo(video, options)], options);
+  }
+
+  sendAudio(
+    audio: {
+      originalContentUrl: string;
+      duration: number;
+    },
+    options?: LineTypes.MessageOptions
+  ) {
+    return this.send([Line.createAudio(audio, options)], options);
+  }
+
+  sendLocation(
+    location: LineTypes.Location,
+    options?: LineTypes.MessageOptions
+  ) {
+    return this.send([Line.createLocation(location, options)], options);
+  }
+
+  sendSticker(
+    sticker: Omit<LineTypes.StickerMessage, 'type'>,
+    options?: LineTypes.MessageOptions
+  ) {
+    return this.send([Line.createSticker(sticker, options)], options);
+  }
+
+  sendImagemap(
+    altText: string,
+    imagemap: Omit<LineTypes.ImagemapMessage, 'type' | 'altText'>,
+    options?: LineTypes.MessageOptions
+  ) {
+    return this.send(
+      [Line.createImagemap(altText, imagemap, options)],
+      options
+    );
+  }
+
+  sendFlex(
+    altText: string,
+    flex: LineTypes.FlexContainer,
+    options?: LineTypes.MessageOptions
+  ) {
+    return this.send([Line.createFlex(altText, flex, options)], options);
+  }
+
+  sendTemplate(
+    altText: string,
+    template: LineTypes.Template,
+    options?: LineTypes.MessageOptions
+  ) {
+    return this.send(
+      [Line.createTemplate(altText, template, options)],
+      options
+    );
+  }
+
+  sendButtonTemplate(
+    altText: string,
+    buttonTemplate: Omit<LineTypes.ButtonsTemplate, 'type'>,
+    options?: LineTypes.MessageOptions
+  ) {
+    return this.send(
+      [Line.createButtonTemplate(altText, buttonTemplate, options)],
+      options
+    );
+  }
+
+  sendButtonsTemplate(
+    altText: string,
+    buttonTemplate: Omit<LineTypes.ButtonsTemplate, 'type'>,
+    options?: LineTypes.MessageOptions
+  ) {
+    return this.sendButtonTemplate(altText, buttonTemplate, options);
+  }
+
+  sendConfirmTemplate(
+    altText: string,
+    confirmTemplate: Omit<LineTypes.ConfirmTemplate, 'type'>,
+    options: LineTypes.MessageOptions
+  ) {
+    return this.send(
+      [Line.createConfirmTemplate(altText, confirmTemplate, options)],
+      options
+    );
+  }
+
+  sendCarouselTemplate(
+    altText: string,
+    columns: LineTypes.ColumnObject[],
+    {
+      imageAspectRatio,
+      imageSize,
+      ...options
+    }: {
+      imageAspectRatio?: 'rectangle' | 'square';
+      imageSize?: 'cover' | 'contain';
+    } & LineTypes.MessageOptions = {}
+  ) {
+    return this.send(
+      [
+        Line.createCarouselTemplate(altText, columns, {
+          imageAspectRatio,
+          imageSize,
+          ...options,
+        }),
+      ],
+      options
+    );
+  }
+
+  sendImageCarouselTemplate(
+    altText: string,
+    columns: LineTypes.ImageCarouselColumnObject[],
+    options: LineTypes.MessageOptions
+  ) {
+    return this.send(
+      [Line.createImageCarouselTemplate(altText, columns, options)],
+      options
+    );
+  }
+}
 
 export default LineContext;
diff --git a/packages/bottender/src/context/PlatformContext.ts b/packages/bottender/src/context/PlatformContext.ts
index 863daa00..d4c1acfb 100644
--- a/packages/bottender/src/context/PlatformContext.ts
+++ b/packages/bottender/src/context/PlatformContext.ts
@@ -1,5 +1,5 @@
 export interface PlatformContext {
   readonly platform: string;
 
-  sendText: (text: string) => Promise<any>;
+  sendText: (text: string) => any | Promise<any>;
 }


Test patch:
diff --git a/packages/bottender/src/context/__tests__/LineContext.spec.ts b/packages/bottender/src/context/__tests__/LineContext.spec.ts
index 9528e951..74310281 100644
--- a/packages/bottender/src/context/__tests__/LineContext.spec.ts
+++ b/packages/bottender/src/context/__tests__/LineContext.spec.ts
@@ -203,17 +203,13 @@ describe('#reply', () => {
   it('should work with quickReply', async () => {
     const { context, client } = setup();
 
-    await context.reply(
-      [
-        {
-          type: 'text',
-          text: 'xxx.com',
-        },
-      ],
+    await context.reply([
       {
+        type: 'text',
+        text: 'xxx.com',
         quickReply,
-      }
-    );
+      },
+    ]);
 
     expect(client.reply).toBeCalledWith(
       rawEvent.replyToken,
@@ -221,11 +217,10 @@ describe('#reply', () => {
         {
           type: 'text',
           text: 'xxx.com',
+          quickReply,
         },
       ],
-      {
-        quickReply,
-      }
+      {}
     );
   });
 });
@@ -236,7 +231,11 @@ describe('#replyText', () => {
 
     await context.replyText('xxx.com');
 
-    expect(client.replyText).toBeCalledWith(rawEvent.replyToken, 'xxx.com', {});
+    expect(client.reply).toBeCalledWith(
+      rawEvent.replyToken,
+      [{ text: 'xxx.com', type: 'text' }],
+      {}
+    );
   });
 
   it('should work with quickReply', async () => {
@@ -246,19 +245,35 @@ describe('#replyText', () => {
       quickReply,
     });
 
-    expect(client.replyText).toBeCalledWith(rawEvent.replyToken, 'xxx.com', {
-      quickReply,
-    });
+    expect(client.reply).toBeCalledWith(
+      rawEvent.replyToken,
+      [
+        {
+          type: 'text',
+          text: 'xxx.com',
+          quickReply,
+        },
+      ],
+      {
+        quickReply,
+      }
+    );
   });
 
   it('should throw when reply multiple times', async () => {
     const { context } = setup();
 
-    const error = new Error('Can not reply event multiple times');
-    error.name = 'Invariant Violation';
-
     await context.replyText('xxx.com');
-    await expect(context.replyText('xxx.com')).rejects.toEqual(error);
+
+    let error;
+    try {
+      await context.replyText('xxx.com');
+    } catch (err) {
+      error = err;
+    }
+
+    expect(error).toBeDefined();
+    expect(error.message).toEqual('Can not reply event multiple times');
   });
 
   it('should mark context as handled', async () => {
@@ -276,9 +291,13 @@ describe('#replyText', () => {
 
     await context.replyText('xxx.com');
 
-    expect(client.replyText).toBeCalledWith(rawEvent.replyToken, 'xxx.com', {
-      accessToken: 'anyToken',
-    });
+    expect(client.reply).toBeCalledWith(
+      rawEvent.replyToken,
+      [{ text: 'xxx.com', type: 'text' }],
+      {
+        accessToken: 'anyToken',
+      }
+    );
   });
 });
 
@@ -308,17 +327,13 @@ describe('#push', () => {
   it('should work with quickReply', async () => {
     const { context, client, session } = setup();
 
-    await context.push(
-      [
-        {
-          type: 'text',
-          text: 'xxx.com',
-        },
-      ],
+    await context.push([
       {
+        type: 'text',
+        text: 'xxx.com',
         quickReply,
-      }
-    );
+      },
+    ]);
 
     expect(client.push).toBeCalledWith(
       session.user.id,
@@ -326,11 +341,10 @@ describe('#push', () => {
         {
           type: 'text',
           text: 'xxx.com',
+          quickReply,
         },
       ],
-      {
-        quickReply,
-      }
+      {}
     );
   });
 });
@@ -341,7 +355,11 @@ describe('#pushText', () => {
 
     await context.pushText('xxx.com');
 
-    expect(client.pushText).toBeCalledWith(session.user.id, 'xxx.com', {});
+    expect(client.push).toBeCalledWith(
+      session.user.id,
+      [{ text: 'xxx.com', type: 'text' }],
+      {}
+    );
   });
 
   it('should work with quickReply', async () => {
@@ -351,9 +369,19 @@ describe('#pushText', () => {
       quickReply,
     });
 
-    expect(client.pushText).toBeCalledWith(session.user.id, 'xxx.com', {
-      quickReply,
-    });
+    expect(client.push).toBeCalledWith(
+      session.user.id,
+      [
+        {
+          text: 'xxx.com',
+          type: 'text',
+          quickReply,
+        },
+      ],
+      {
+        quickReply,
+      }
+    );
   });
 
   it('should work with room session', async () => {
@@ -363,7 +391,11 @@ describe('#pushText', () => {
 
     await context.pushText('xxx.com');
 
-    expect(client.pushText).toBeCalledWith(session.room.id, 'xxx.com', {});
+    expect(client.push).toBeCalledWith(
+      session.room.id,
+      [{ text: 'xxx.com', type: 'text' }],
+      {}
+    );
   });
 
   it('should work with group session', async () => {
@@ -373,7 +405,11 @@ describe('#pushText', () => {
 
     await context.pushText('xxx.com');
 
-    expect(client.pushText).toBeCalledWith(session.group.id, 'xxx.com', {});
+    expect(client.push).toBeCalledWith(
+      session.group.id,
+      [{ text: 'xxx.com', type: 'text' }],
+      {}
+    );
   });
 
   it('should mark context as handled', async () => {
@@ -390,7 +426,7 @@ describe('#pushText', () => {
     await context.pushText('xxx.com');
 
     expect(warning).toBeCalled();
-    expect(client.pushText).not.toBeCalled();
+    expect(client.push).not.toBeCalled();
   });
 
   it('should support custom token', async () => {
@@ -400,50 +436,35 @@ describe('#pushText', () => {
 
     await context.pushText('xxx.com');
 
-    expect(client.pushText).toBeCalledWith(session.user.id, 'xxx.com', {
-      accessToken: 'anyToken',
-    });
+    expect(client.push).toBeCalledWith(
+      session.user.id,
+      [{ text: 'xxx.com', type: 'text' }],
+      {
+        accessToken: 'anyToken',
+      }
+    );
   });
 });
 
 describe('send APIs', () => {
   describe('#send', () => {
-    it('should call client.push', async () => {
-      const { context, client, session } = setup();
-
-      await context.send([Line.createText('2'), Line.createText('3')]);
-
-      expect(client.push).toBeCalledWith(
-        session.user.id,
-        [Line.createText('2'), Line.createText('3')],
-        {}
-      );
-    });
-
-    it('should work with room session', async () => {
-      const { context, client, session } = setup({
-        session: roomSession,
-      });
-
-      await context.send([Line.createText('2'), Line.createText('3')]);
-
-      expect(client.push).toBeCalledWith(
-        session.room.id,
-        [Line.createText('2'), Line.createText('3')],
-        {}
-      );
-    });
-
-    it('should work with group session', async () => {
-      const { context, client, session } = setup({
-        session: groupSession,
-      });
+    it('should call client.reply', async () => {
+      const { context, client } = setup();
 
       await context.send([Line.createText('2'), Line.createText('3')]);
 
-      expect(client.push).toBeCalledWith(
-        session.group.id,
-        [Line.createText('2'), Line.createText('3')],
+      expect(client.reply).toBeCalledWith(
+        rawEvent.replyToken,
+        [
+          {
+            type: 'text',
+            text: '2',
+          },
+          {
+            type: 'text',
+            text: '3',
+          },
+        ],
         {}
       );
     });
@@ -458,76 +479,70 @@ describe('send APIs', () => {
   });
 
   describe('#sendText', () => {
-    it('should call client.pushText', async () => {
-      const { context, client, session } = setup();
+    it('should call client.replyText', async () => {
+      const { context, client } = setup();
 
       await context.sendText('xxx.com');
 
-      expect(client.pushText).toBeCalledWith(session.user.id, 'xxx.com', {});
+      expect(client.reply).toBeCalledWith(
+        rawEvent.replyToken,
+        [
+          {
+            type: 'text',
+            text: 'xxx.com',
+          },
+        ],
+        {}
+      );
     });
 
     it('should work with quickReply', async () => {
-      const { context, client, session } = setup();
+      const { context, client } = setup();
 
       await context.sendText('xxx.com', {
         quickReply,
       });
 
-      expect(client.pushText).toBeCalledWith(session.user.id, 'xxx.com', {
-        quickReply,
-      });
-    });
-
-    it('should work with room session', async () => {
-      const { context, client, session } = setup({
-        session: roomSession,
-      });
-
-      await context.sendText('xxx.com');
-
-      expect(client.pushText).toBeCalledWith(session.room.id, 'xxx.com', {});
-    });
-
-    it('should work with group session', async () => {
-      const { context, client, session } = setup({
-        session: groupSession,
-      });
-
-      await context.sendText('xxx.com');
-
-      expect(client.pushText).toBeCalledWith(session.group.id, 'xxx.com', {});
-    });
-
-    it('should mark context as handled', async () => {
-      const { context } = setup();
-
-      await context.sendText('xxx.com');
-
-      expect(context.isHandled).toBe(true);
+      expect(client.reply).toBeCalledWith(
+        rawEvent.replyToken,
+        [
+          {
+            type: 'text',
+            text: 'xxx.com',
+            quickReply,
+          },
+        ],
+        {
+          quickReply,
+        }
+      );
     });
   });
 
   describe('#sendImage', () => {
-    it('should call client.pushImage', async () => {
-      const { context, client, session } = setup();
+    it('should call client.replyImage', async () => {
+      const { context, client } = setup();
 
       await context.sendImage({
         originalContentUrl: 'xxx.jpg',
         previewImageUrl: 'yyy.jpg',
       });
 
-      expect(client.pushImage).toBeCalledWith(
-        session.user.id,
-        {
-          originalContentUrl: 'xxx.jpg',
-          previewImageUrl: 'yyy.jpg',
-        },
+      expect(client.reply).toBeCalledWith(
+        rawEvent.replyToken,
+        [
+          {
+            type: 'image',
+            originalContentUrl: 'xxx.jpg',
+            previewImageUrl: 'yyy.jpg',
+          },
+        ],
         { accessToken: undefined }
       );
     });
 
     it('should work with quickReply', async () => {
-      const { context, client, session } = setup();
+      const { context, client } = setup();
 
       await context.sendImage(
         {
@@ -539,103 +554,47 @@ describe('send APIs', () => {
         }
       );
 
-      expect(client.pushImage).toBeCalledWith(
-        session.user.id,
-        {
-          originalContentUrl: 'xxx.jpg',
-          previewImageUrl: 'yyy.jpg',
-        },
+      expect(client.reply).toBeCalledWith(
+        rawEvent.replyToken,
+        [
+          {
+            type: 'image',
+            originalContentUrl: 'xxx.jpg',
+            previewImageUrl: 'yyy.jpg',
+            quickReply,
+          },
+        ],
         {
           quickReply,
         }
       );
     });
-
-    it('should work with room session', async () => {
-      const { context, client, session } = setup({
-        session: roomSession,
-      });
-
-      await context.sendImage({
-        originalContentUrl: 'xxx.jpg',
-        previewImageUrl: 'yyy.jpg',
-      });
-
-      expect(client.pushImage).toBeCalledWith(
-        session.room.id,
-        {
-          originalContentUrl: 'xxx.jpg',
-          previewImageUrl: 'yyy.jpg',
-        },
-        { accessToken: undefined }
-      );
-    });
-
-    it('should work with group session', async () => {
-      const { context, client, session } = setup({
-        session: groupSession,
-      });
-
-      await context.sendImage({
-        originalContentUrl: 'xxx.jpg',
-        previewImageUrl: 'yyy.jpg',
-      });
-
-      expect(client.pushImage).toBeCalledWith(
-        session.group.id,
-        {
-          originalContentUrl: 'xxx.jpg',
-          previewImageUrl: 'yyy.jpg',
-        },
-        { accessToken: undefined }
-      );
-    });
-
-    it('should mark context as handled', async () => {
-      const { context } = setup();
-
-      await context.sendImage({
-        originalContentUrl: 'xxx.jpg',
-        previewImageUrl: 'yyy.jpg',
-      });
-
-      expect(context.isHandled).toBe(true);
-    });
-
-    it('should call warning and not to send if dont have session', async () => {
-      const { context, client } = setup({ session: false });
-
-      await context.sendImage({
-        originalContentUrl: 'xxx.jpg',
-        previewImageUrl: 'yyy.jpg',
-      });
-
-      expect(warning).toBeCalled();
-      expect(client.pushImage).not.toBeCalled();
-    });
   });
 
   describe('#sendAudio', () => {
-    it('should call client.pushAudio', async () => {
-      const { context, client, session } = setup();
+    it('should call client.replyAudio', async () => {
+      const { context, client } = setup();
 
       await context.sendAudio({
         originalContentUrl: 'xxx.m4a',
         duration: 240000,
       });
 
-      expect(client.pushAudio).toBeCalledWith(
-        session.user.id,
-        {
-          originalContentUrl: 'xxx.m4a',
-          duration: 240000,
-        },
+      expect(client.reply).toBeCalledWith(
+        rawEvent.replyToken,
+        [
+          {
+            type: 'audio',
+            originalContentUrl: 'xxx.m4a',
+            duration: 240000,
+          },
+        ],
         { accessToken: undefined }
       );
     });
 
     it('should work with quickReply', async () => {
-      const { context, client, session } = setup();
+      const { context, client } = setup();
 
       await context.sendAudio(
         {
@@ -647,91 +606,47 @@ describe('send APIs', () => {
         }
       );
 
-      expect(client.pushAudio).toBeCalledWith(
-        session.user.id,
-        {
-          originalContentUrl: 'xxx.m4a',
-          duration: 240000,
-        },
+      expect(client.reply).toBeCalledWith(
+        rawEvent.replyToken,
+        [
+          {
+            type: 'audio',
+            originalContentUrl: 'xxx.m4a',
+            duration: 240000,
+            quickReply,
+          },
+        ],
         {
           quickReply,
         }
       );
     });
+  });
 
-    it('should work with room session', async () => {
-      const { context, client, session } = setup({
-        session: roomSession,
-      });
+  describe('#sendVideo', () => {
+    it('should call client.replyVideo', async () => {
+      const { context, client } = setup();
 
-      await context.sendAudio({
-        originalContentUrl: 'xxx.m4a',
-        duration: 240000,
+      await context.sendVideo({
+        originalContentUrl: 'xxx.mp4',
+        previewImageUrl: 'yyy.jpg',
       });
 
-      expect(client.pushAudio).toBeCalledWith(
-        session.room.id,
-        {
-          originalContentUrl: 'xxx.m4a',
-          duration: 240000,
-        },
-        { accessToken: undefined }
-      );
-    });
-
-    it('should work with group session', async () => {
-      const { context, client, session } = setup({
-        session: groupSession,
-      });
-
-      await context.sendAudio({
-        originalContentUrl: 'xxx.m4a',
-        duration: 240000,
-      });
-
-      expect(client.pushAudio).toBeCalledWith(
-        session.group.id,
-        {
-          originalContentUrl: 'xxx.m4a',
-          duration: 240000,
-        },
-        { accessToken: undefined }
-      );
-    });
-
-    it('should mark context as handled', async () => {
-      const { context } = setup();
-
-      await context.sendAudio({
-        originalContentUrl: 'xxx.m4a',
-        duration: 240000,
-      });
-
-      expect(context.isHandled).toBe(true);
-    });
-  });
-
-  describe('#sendVideo', () => {
-    it('should call client.pushVideo', async () => {
-      const { context, client, session } = setup();
-
-      await context.sendVideo({
-        originalContentUrl: 'xxx.mp4',
-        previewImageUrl: 'yyy.jpg',
-      });
-
-      expect(client.pushVideo).toBeCalledWith(
-        session.user.id,
-        {
-          originalContentUrl: 'xxx.mp4',
-          previewImageUrl: 'yyy.jpg',
-        },
+      expect(client.reply).toBeCalledWith(
+        rawEvent.replyToken,
+        [
+          {
+            type: 'video',
+            originalContentUrl: 'xxx.mp4',
+            previewImageUrl: 'yyy.jpg',
+          },
+        ],
         { accessToken: undefined }
       );
     });
 
     it('should work with quickReply', async () => {
-      const { context, client, session } = setup();
+      const { context, client } = setup();
 
       await context.sendVideo(
         {
@@ -743,73 +658,26 @@ describe('send APIs', () => {
         }
       );
 
-      expect(client.pushVideo).toBeCalledWith(
-        session.user.id,
-        {
-          originalContentUrl: 'xxx.mp4',
-          previewImageUrl: 'yyy.jpg',
-        },
+      expect(client.reply).toBeCalledWith(
+        rawEvent.replyToken,
+        [
+          {
+            type: 'video',
+            originalContentUrl: 'xxx.mp4',
+            previewImageUrl: 'yyy.jpg',
+            quickReply,
+          },
+        ],
         {
           quickReply,
         }
       );
     });
-
-    it('should work with room session', async () => {
-      const { context, client, session } = setup({
-        session: roomSession,
-      });
-
-      await context.sendVideo({
-        originalContentUrl: 'xxx.mp4',
-        previewImageUrl: 'yyy.jpg',
-      });
-
-      expect(client.pushVideo).toBeCalledWith(
-        session.room.id,
-        {
-          originalContentUrl: 'xxx.mp4',
-          previewImageUrl: 'yyy.jpg',
-        },
-        { accessToken: undefined }
-      );
-    });
-
-    it('should work with group session', async () => {
-      const { context, client, session } = setup({
-        session: groupSession,
-      });
-
-      await context.sendVideo({
-        originalContentUrl: 'xxx.mp4',
-        previewImageUrl: 'yyy.jpg',
-      });
-
-      expect(client.pushVideo).toBeCalledWith(
-        session.group.id,
-        {
-          originalContentUrl: 'xxx.mp4',
-          previewImageUrl: 'yyy.jpg',
-        },
-        { accessToken: undefined }
-      );
-    });
-
-    it('should mark context as handled', async () => {
-      const { context } = setup();
-
-      await context.sendVideo({
-        originalContentUrl: 'xxx.mp4',
-        previewImageUrl: 'yyy.jpg',
-      });
-
-      expect(context.isHandled).toBe(true);
-    });
   });
 
   describe('#sendLocation', () => {
-    it('should call client.pushLocation', async () => {
-      const { context, client, session } = setup();
+    it('should call client.replyLocation', async () => {
+      const { context, client } = setup();
 
       await context.sendLocation({
         title: 'my location',
@@ -818,20 +686,23 @@ describe('send APIs', () => {
         longitude: 139.70372892916203,
       });
 
-      expect(client.pushLocation).toBeCalledWith(
-        session.user.id,
-        {
-          title: 'my location',
-          address: '〒150-0002 東京都渋谷区渋谷２丁目２１−１',
-          latitude: 35.65910807942215,
-          longitude: 139.70372892916203,
-        },
+      expect(client.reply).toBeCalledWith(
+        rawEvent.replyToken,
+        [
+          {
+            type: 'location',
+            title: 'my location',
+            address: '〒150-0002 東京都渋谷区渋谷２丁目２１−１',
+            latitude: 35.65910807942215,
+            longitude: 139.70372892916203,
+          },
+        ],
         { accessToken: undefined }
       );
     });
 
     it('should work with quickReply', async () => {
-      const { context, client, session } = setup();
+      const { context, client } = setup();
 
       await context.sendLocation(
         {
@@ -845,103 +716,49 @@ describe('send APIs', () => {
         }
       );
 
-      expect(client.pushLocation).toBeCalledWith(
-        session.user.id,
-        {
-          title: 'my location',
-          address: '〒150-0002 東京都渋谷区渋谷２丁目２１−１',
-          latitude: 35.65910807942215,
-          longitude: 139.70372892916203,
-        },
+      expect(client.reply).toBeCalledWith(
+        rawEvent.replyToken,
+        [
+          {
+            type: 'location',
+            title: 'my location',
+            address: '〒150-0002 東京都渋谷区渋谷２丁目２１−１',
+            latitude: 35.65910807942215,
+            longitude: 139.70372892916203,
+            quickReply,
+          },
+        ],
         {
           quickReply,
         }
       );
     });
-
-    it('should work with room session', async () => {
-      const { context, client, session } = setup({
-        session: roomSession,
-      });
-
-      await context.sendLocation({
-        title: 'my location',
-        address: '〒150-0002 東京都渋谷区渋谷２丁目２１−１',
-        latitude: 35.65910807942215,
-        longitude: 139.70372892916203,
-      });
-
-      expect(client.pushLocation).toBeCalledWith(
-        session.room.id,
-        {
-          title: 'my location',
-          address: '〒150-0002 東京都渋谷区渋谷２丁目２１−１',
-          latitude: 35.65910807942215,
-          longitude: 139.70372892916203,
-        },
-        { accessToken: undefined }
-      );
-    });
-
-    it('should work with group session', async () => {
-      const { context, client, session } = setup({
-        session: groupSession,
-      });
-
-      await context.sendLocation({
-        title: 'my location',
-        address: '〒150-0002 東京都渋谷区渋谷２丁目２１−１',
-        latitude: 35.65910807942215,
-        longitude: 139.70372892916203,
-      });
-
-      expect(client.pushLocation).toBeCalledWith(
-        session.group.id,
-        {
-          title: 'my location',
-          address: '〒150-0002 東京都渋谷区渋谷２丁目２１−１',
-          latitude: 35.65910807942215,
-          longitude: 139.70372892916203,
-        },
-        { accessToken: undefined }
-      );
-    });
-
-    it('should mark context as handled', async () => {
-      const { context } = setup();
-
-      await context.sendLocation({
-        title: 'my location',
-        address: '〒150-0002 東京都渋谷区渋谷２丁目２１−１',
-        latitude: 35.65910807942215,
-        longitude: 139.70372892916203,
-      });
-
-      expect(context.isHandled).toBe(true);
-    });
   });
 
   describe('#sendSticker', () => {
-    it('should call client.pushSticker', async () => {
-      const { context, client, session } = setup();
+    it('should call client.replySticker', async () => {
+      const { context, client } = setup();
 
       await context.sendSticker({
         packageId: '1',
         stickerId: '1',
       });
 
-      expect(client.pushSticker).toBeCalledWith(
-        session.user.id,
-        {
-          packageId: '1',
-          stickerId: '1',
-        },
+      expect(client.reply).toBeCalledWith(
+        rawEvent.replyToken,
+        [
+          {
+            type: 'sticker',
+            packageId: '1',
+            stickerId: '1',
+          },
+        ],
         { accessToken: undefined }
       );
     });
 
     it('should work with quickReply', async () => {
-      const { context, client, session } = setup();
+      const { context, client } = setup();
 
       await context.sendSticker(
         {
@@ -953,75 +770,30 @@ describe('send APIs', () => {
         }
       );
 
-      expect(client.pushSticker).toBeCalledWith(
-        session.user.id,
-        {
-          packageId: '1',
-          stickerId: '1',
-        },
+      expect(client.reply).toBeCalledWith(
+        rawEvent.replyToken,
+        [
+          {
+            type: 'sticker',
+            packageId: '1',
+            stickerId: '1',
+            quickReply,
+          },
+        ],
         {
           quickReply,
         }
       );
     });
-
-    it('should work with room session', async () => {
-      const { context, client, session } = setup({
-        session: roomSession,
-      });
-
-      await context.sendSticker({
-        packageId: '1',
-        stickerId: '1',
-      });
-
-      expect(client.pushSticker).toBeCalledWith(
-        session.room.id,
-        {
-          packageId: '1',
-          stickerId: '1',
-        },
-        { accessToken: undefined }
-      );
-    });
-
-    it('should work with group session', async () => {
-      const { context, client, session } = setup({
-        session: groupSession,
-      });
-
-      await context.sendSticker({
-        packageId: '1',
-        stickerId: '1',
-      });
-
-      expect(client.pushSticker).toBeCalledWith(
-        session.group.id,
-        {
-          packageId: '1',
-          stickerId: '1',
-        },
-        { accessToken: undefined }
-      );
-    });
-
-    it('should mark context as handled', async () => {
-      const { context } = setup();
-
-      await context.sendSticker({
-        packageId: '1',
-        stickerId: '1',
-      });
-
-      expect(context.isHandled).toBe(true);
-    });
   });
 
   describe('#sendImagemap', () => {
     const template = {
       baseUrl: 'https://example.com/bot/images/rm001',
-      baseHeight: 1040,
-      baseWidth: 1040,
+      baseSize: {
+        height: 1040,
+        width: 1040,
+      },
       actions: [
         {
           type: 'uri',
@@ -1046,73 +818,46 @@ describe('send APIs', () => {
       ],
     };
 
-    it('should call client.pushImagemap', async () => {
-      const { context, client, session } = setup();
+    it('should call client.replyImagemap', async () => {
+      const { context, client } = setup();
 
       await context.sendImagemap('this is an imagemap', template);
 
-      expect(client.pushImagemap).toBeCalledWith(
-        session.user.id,
-        'this is an imagemap',
-        template,
-        { accessToken: undefined }
+      expect(client.reply).toBeCalledWith(
+        rawEvent.replyToken,
+        [
+          {
+            type: 'imagemap',
+            altText: 'this is an imagemap',
+            ...template,
+          },
+        ],
+        {}
       );
     });
 
     it('should work with quickReply', async () => {
-      const { context, client, session } = setup();
+      const { context, client } = setup();
 
       await context.sendImagemap('this is an imagemap', template, {
         quickReply,
       });
 
-      expect(client.pushImagemap).toBeCalledWith(
-        session.user.id,
-        'this is an imagemap',
-        template,
+      expect(client.reply).toBeCalledWith(
+        rawEvent.replyToken,
+        [
+          {
+            type: 'imagemap',
+            altText: 'this is an imagemap',
+            ...template,
+            quickReply,
+          },
+        ],
         {
           quickReply,
         }
       );
     });
-
-    it('should work with room session', async () => {
-      const { context, client, session } = setup({
-        session: roomSession,
-      });
-
-      await context.sendImagemap('this is an imagemap', template);
-
-      expect(client.pushImagemap).toBeCalledWith(
-        session.room.id,
-        'this is an imagemap',
-        template,
-        { accessToken: undefined }
-      );
-    });
-
-    it('should work with group session', async () => {
-      const { context, client, session } = setup({
-        session: groupSession,
-      });
-
-      await context.sendImagemap('this is an imagemap', template);
-
-      expect(client.pushImagemap).toBeCalledWith(
-        session.group.id,
-        'this is an imagemap',
-        template,
-        { accessToken: undefined }
-      );
-    });
-
-    it('should mark context as handled', async () => {
-      const { context } = setup();
-
-      await context.sendImagemap('this is an imagemap', template);
-
-      expect(context.isHandled).toBe(true);
-    });
   });
 
   describe('#sendFlex', () => {
@@ -1157,73 +902,46 @@ describe('send APIs', () => {
       },
     };
 
-    it('should call client.pushFlex', async () => {
-      const { context, client, session } = setup();
-
-      await context.sendFlex('this is a flex', contents);
-
-      expect(client.pushFlex).toBeCalledWith(
-        session.user.id,
-        'this is a flex',
-        contents,
-        { accessToken: undefined }
-      );
-    });
-
-    it('should work with quickReply', async () => {
-      const { context, client, session } = setup();
-
-      await context.sendFlex('this is a flex', contents, {
-        quickReply,
-      });
-
-      expect(client.pushFlex).toBeCalledWith(
-        session.user.id,
-        'this is a flex',
-        contents,
-        {
-          quickReply,
-        }
-      );
-    });
-
-    it('should work with room session', async () => {
-      const { context, client, session } = setup({
-        session: roomSession,
-      });
+    it('should call client.replyFlex', async () => {
+      const { context, client } = setup();
 
       await context.sendFlex('this is a flex', contents);
 
-      expect(client.pushFlex).toBeCalledWith(
-        session.room.id,
-        'this is a flex',
-        contents,
-        { accessToken: undefined }
+      expect(client.reply).toBeCalledWith(
+        rawEvent.replyToken,
+        [
+          {
+            type: 'flex',
+            altText: 'this is a flex',
+            contents,
+          },
+        ],
+        {}
       );
     });
 
-    it('should work with group session', async () => {
-      const { context, client, session } = setup({
-        session: groupSession,
-      });
+    it('should work with quickReply', async () => {
+      const { context, client } = setup();
 
-      await context.sendFlex('this is a flex', contents);
+      await context.sendFlex('this is a flex', contents, {
+        quickReply,
+      });
 
-      expect(client.pushFlex).toBeCalledWith(
-        session.group.id,
-        'this is a flex',
-        contents,
-        { accessToken: undefined }
+      expect(client.reply).toBeCalledWith(
+        rawEvent.replyToken,
+        [
+          {
+            type: 'flex',
+            altText: 'this is a flex',
+            contents,
+            quickReply,
+          },
+        ],
+        {
+          quickReply,
+        }
       );
     });
-
-    it('should mark context as handled', async () => {
-      const { context } = setup();
-
-      await context.sendFlex('this is a flex', contents);
-
-      expect(context.isHandled).toBe(true);
-    });
   });
 
   describe('#sendTemplate', () => {
@@ -1251,73 +969,46 @@ describe('send APIs', () => {
       ],
     };
 
-    it('should call client.pushTemplate', async () => {
-      const { context, client, session } = setup();
+    it('should call client.replyTemplate', async () => {
+      const { context, client } = setup();
 
       await context.sendTemplate('this is a template', template);
 
-      expect(client.pushTemplate).toBeCalledWith(
-        session.user.id,
-        'this is a template',
-        template,
-        { accessToken: undefined }
+      expect(client.reply).toBeCalledWith(
+        rawEvent.replyToken,
+        [
+          {
+            type: 'template',
+            altText: 'this is a template',
+            template,
+          },
+        ],
+        {}
       );
     });
 
     it('should work with quickReply', async () => {
-      const { context, client, session } = setup();
+      const { context, client } = setup();
 
       await context.sendTemplate('this is a template', template, {
         quickReply,
       });
 
-      expect(client.pushTemplate).toBeCalledWith(
-        session.user.id,
-        'this is a template',
-        template,
+      expect(client.reply).toBeCalledWith(
+        rawEvent.replyToken,
+        [
+          {
+            type: 'template',
+            altText: 'this is a template',
+            template,
+            quickReply,
+          },
+        ],
         {
           quickReply,
         }
       );
     });
-
-    it('should work with room session', async () => {
-      const { context, client, session } = setup({
-        session: roomSession,
-      });
-
-      await context.sendTemplate('this is a template', template);
-
-      expect(client.pushTemplate).toBeCalledWith(
-        session.room.id,
-        'this is a template',
-        template,
-        { accessToken: undefined }
-      );
-    });
-
-    it('should work with group session', async () => {
-      const { context, client, session } = setup({
-        session: groupSession,
-      });
-
-      await context.sendTemplate('this is a template', template);
-
-      expect(client.pushTemplate).toBeCalledWith(
-        session.group.id,
-        'this is a template',
-        template,
-        { accessToken: undefined }
-      );
-    });
-
-    it('should mark context as handled', async () => {
-      const { context } = setup();
-
-      await context.sendTemplate('this is a template', template);
-
-      expect(context.isHandled).toBe(true);
-    });
   });
 
   describe('#sendButtonTemplate', () => {
@@ -1344,84 +1035,71 @@ describe('send APIs', () => {
       ],
     };
 
-    it('should call client.pushButtonTemplate', async () => {
-      const { context, client, session } = setup();
+    it('should call client.replyButtonTemplate', async () => {
+      const { context, client } = setup();
 
       await context.sendButtonTemplate('this is a button template', template);
 
-      expect(client.pushButtonTemplate).toBeCalledWith(
-        session.user.id,
-        'this is a button template',
-        template,
+      expect(client.reply).toBeCalledWith(
+        rawEvent.replyToken,
+        [
+          {
+            type: 'template',
+            altText: 'this is a button template',
+            template: {
+              type: 'buttons',
+              ...template,
+            },
+          },
+        ],
         { accessToken: undefined }
       );
     });
 
     it('should work with quickReply', async () => {
-      const { context, client, session } = setup();
+      const { context, client } = setup();
 
       await context.sendButtonTemplate('this is a button template', template, {
         quickReply,
       });
 
-      expect(client.pushButtonTemplate).toBeCalledWith(
-        session.user.id,
-        'this is a button template',
-        template,
+      expect(client.reply).toBeCalledWith(
+        rawEvent.replyToken,
+        [
+          {
+            type: 'template',
+            altText: 'this is a button template',
+            template: {
+              type: 'buttons',
+              ...template,
+            },
+            quickReply,
+          },
+        ],
         {
           quickReply,
         }
       );
     });
 
-    it('should work with room session', async () => {
-      const { context, client, session } = setup({
-        session: roomSession,
-      });
-
-      await context.sendButtonTemplate('this is a button template', template);
-
-      expect(client.pushButtonTemplate).toBeCalledWith(
-        session.room.id,
-        'this is a button template',
-        template,
-        { accessToken: undefined }
-      );
-    });
-
-    it('should work with group session', async () => {
-      const { context, client, session } = setup({
-        session: groupSession,
-      });
-
-      await context.sendButtonTemplate('this is a button template', template);
-
-      expect(client.pushButtonTemplate).toBeCalledWith(
-        session.group.id,
-        'this is a button template',
-        template,
-        { accessToken: undefined }
-      );
-    });
-
-    it('should mark context as handled', async () => {
-      const { context } = setup();
-
-      await context.sendButtonTemplate('this is a button template', template);
-
-      expect(context.isHandled).toBe(true);
-    });
-
     it('should support sendButtonsTemplate alias', async () => {
-      const { context, client, session } = setup();
+      const { context, client } = setup();
 
       await context.sendButtonsTemplate('this is a button template', template);
 
-      expect(client.pushButtonTemplate).toBeCalledWith(
-        session.user.id,
-        'this is a button template',
-        template,
-        { accessToken: undefined }
+      expect(client.reply).toBeCalledWith(
+        rawEvent.replyToken,
+        [
+          {
+            type: 'template',
+            altText: 'this is a button template',
+            template: {
+              type: 'buttons',
+              ...template,
+            },
+          },
+        ],
+        {}
       );
     });
   });
@@ -1443,21 +1121,29 @@ describe('send APIs', () => {
       ],
     };
 
-    it('should call client.pushConfirmTemplate', async () => {
-      const { context, client, session } = setup();
+    it('should call client.replyConfirmTemplate', async () => {
+      const { context, client } = setup();
 
       await context.sendConfirmTemplate('this is a confirm template', template);
 
-      expect(client.pushConfirmTemplate).toBeCalledWith(
-        session.user.id,
-        'this is a confirm template',
-        template,
+      expect(client.reply).toBeCalledWith(
+        rawEvent.replyToken,
+        [
+          {
+            type: 'template',
+            altText: 'this is a confirm template',
+            template: {
+              type: 'confirm',
+              ...template,
+            },
+          },
+        ],
         { accessToken: undefined }
       );
     });
 
     it('should work with quickReply', async () => {
-      const { context, client, session } = setup();
+      const { context, client } = setup();
 
       await context.sendConfirmTemplate(
         'this is a confirm template',
@@ -1465,51 +1151,22 @@ describe('send APIs', () => {
         { quickReply }
       );
 
-      expect(client.pushConfirmTemplate).toBeCalledWith(
-        session.user.id,
-        'this is a confirm template',
-        template,
+      expect(client.reply).toBeCalledWith(
+        rawEvent.replyToken,
+        [
+          {
+            type: 'template',
+            altText: 'this is a confirm template',
+            template: {
+              type: 'confirm',
+              ...template,
+            },
+            quickReply,
+          },
+        ],
         { quickReply }
       );
     });
-
-    it('should work with room session', async () => {
-      const { context, client, session } = setup({
-        session: roomSession,
-      });
-
-      await context.sendConfirmTemplate('this is a confirm template', template);
-
-      expect(client.pushConfirmTemplate).toBeCalledWith(
-        session.room.id,
-        'this is a confirm template',
-        template,
-        { accessToken: undefined }
-      );
-    });
-
-    it('should work with group session', async () => {
-      const { context, client, session } = setup({
-        session: groupSession,
-      });
-
-      await context.sendConfirmTemplate('this is a confirm template', template);
-
-      expect(client.pushConfirmTemplate).toBeCalledWith(
-        session.group.id,
-        'this is a confirm template',
-        template,
-        { accessToken: undefined }
-      );
-    });
-
-    it('should mark context as handled', async () => {
-      const { context } = setup();
-
-      await context.sendConfirmTemplate('this is a confirm template', template);
-
-      expect(context.isHandled).toBe(true);
-    });
   });
 
   describe('#sendCarouselTemplate', () => {
@@ -1560,24 +1217,34 @@ describe('send APIs', () => {
       },
     ];
 
-    it('should call client.pushCarouselTemplate', async () => {
-      const { context, client, session } = setup();
+    it('should call client.replyCarouselTemplate', async () => {
+      const { context, client } = setup();
 
       await context.sendCarouselTemplate(
         'this is a carousel template',
         template
       );
 
-      expect(client.pushCarouselTemplate).toBeCalledWith(
-        session.user.id,
-        'this is a carousel template',
-        template,
+      expect(client.reply).toBeCalledWith(
+        rawEvent.replyToken,
+        [
+          {
+            type: 'template',
+            altText: 'this is a carousel template',
+            template: {
+              type: 'carousel',
+              imageAspectRatio: undefined,
+              imageSize: undefined,
+              columns: template,
+            },
+          },
+        ],
         { accessToken: undefined }
       );
     });
 
     it('should work with quickReply', async () => {
-      const { context, client, session } = setup();
+      const { context, client } = setup();
 
       await context.sendCarouselTemplate(
         'this is a carousel template',
@@ -1585,60 +1252,24 @@ describe('send APIs', () => {
         { quickReply }
       );
 
-      expect(client.pushCarouselTemplate).toBeCalledWith(
-        session.user.id,
-        'this is a carousel template',
-        template,
+      expect(client.reply).toBeCalledWith(
+        rawEvent.replyToken,
+        [
+          {
+            type: 'template',
+            altText: 'this is a carousel template',
+            template: {
+              type: 'carousel',
+              imageAspectRatio: undefined,
+              imageSize: undefined,
+              columns: template,
+            },
+            quickReply,
+          },
+        ],
         { quickReply }
       );
     });
-
-    it('should work with room session', async () => {
-      const { context, client, session } = setup({
-        session: roomSession,
-      });
-
-      await context.sendConfirmTemplate(
-        'this is a carousel template',
-        template
-      );
-
-      expect(client.pushConfirmTemplate).toBeCalledWith(
-        session.room.id,
-        'this is a carousel template',
-        template,
-        { accessToken: undefined }
-      );
-    });
-
-    it('should work with group session', async () => {
-      const { context, client, session } = setup({
-        session: groupSession,
-      });
-
-      await context.sendConfirmTemplate(
-        'this is a carousel template',
-        template
-      );
-
-      expect(client.pushConfirmTemplate).toBeCalledWith(
-        session.group.id,
-        'this is a carousel template',
-        template,
-        { accessToken: undefined }
-      );
-    });
-
-    it('should mark context as handled', async () => {
-      const { context } = setup();
-
-      await context.sendCarouselTemplate(
-        'this is a carousel template',
-        template
-      );
-
-      expect(context.isHandled).toBe(true);
-    });
   });
 });
 
@@ -1960,7 +1591,7 @@ describe('#typing', () => {
 
 describe('batch', () => {
   it('should not batch when shouldBatch: false', async () => {
-    const { client, context } = setup({ shouldBatch: false });
+    const { client, context, session } = setup({ shouldBatch: false });
 
     await context.replyText('1');
     await context.pushText('2');
@@ -1968,8 +1599,36 @@ describe('batch', () => {
 
     await context.handlerDidEnd();
 
-    expect(client.reply).not.toBeCalled();
-    expect(client.push).not.toBeCalled();
+    expect(client.reply).toBeCalledWith(
+      rawEvent.replyToken,
+      [
+        {
+          type: 'text',
+          text: '1',
+        },
+      ],
+      { accessToken: undefined }
+    );
+    expect(client.push).toBeCalledWith(
+      session.user.id,
+      [
+        {
+          type: 'text',
+          text: '2',
+        },
+      ],
+      { accessToken: undefined }
+    );
+    expect(client.push).toBeCalledWith(
+      session.user.id,
+      [
+        {
+          type: 'text',
+          text: '3',
+        },
+      ],
+      { accessToken: undefined }
+    );
   });
 
   it('should batch reply', async () => {
@@ -2184,24 +1843,32 @@ describe('batch', () => {
 });
 
 describe('sendMethod', () => {
-  it('should use push in send as default', async () => {
-    const { context, client, session } = setup();
+  it('should use reply in send as default', async () => {
+    const { context, client } = setup();
 
     await context.sendText('hello');
 
-    expect(client.pushText).toBeCalledWith(session.user.id, 'hello', {});
-    expect(client.replyText).not.toBeCalled();
+    expect(client.reply).toBeCalledWith(
+      rawEvent.replyToken,
+      [{ text: 'hello', type: 'text' }],
+      {}
+    );
+    expect(client.push).not.toBeCalled();
   });
 
-  it('should use reply in send when sendMethod: reply', async () => {
-    const { context, client } = setup({
-      sendMethod: 'reply',
+  it('should use push in send when sendMethod: push', async () => {
+    const { context, client, session } = setup({
+      sendMethod: 'push',
     });
 
     await context.sendText('hello');
 
-    expect(client.replyText).toBeCalledWith(rawEvent.replyToken, 'hello', {});
-    expect(client.pushText).not.toBeCalled();
+    expect(client.push).toBeCalledWith(
+      session.user.id,
+      [{ text: 'hello', type: 'text' }],
+      {}
+    );
+    expect(client.reply).not.toBeCalled();
   });
 });
 
@@ -2213,8 +1880,12 @@ describe('#useAccessToken', () => {
 
     await context.replyText('xxx.com');
 
-    expect(client.replyText).toBeCalledWith(rawEvent.replyToken, 'xxx.com', {
-      accessToken: 'anyToken',
-    });
+    expect(client.reply).toBeCalledWith(
+      rawEvent.replyToken,
+      [{ text: 'xxx.com', type: 'text' }],
+      {
+        accessToken: 'anyToken',
+      }
+    );
   });
 });