Skip to content

Commit

Permalink
[2] simplify text handlers as well (#371)
Browse files Browse the repository at this point in the history
- Simplies `handleInput` and `initState` handler to handle just text events
    - `handleInput` is in charge of creating new (text) search session
    - `initState` takes input text from search session, no need to compose an event for it, greatly simplifies stuff
- Removes `ChatbotEvent` completely, as postback and text event handlers has their own events
    - This also removes the mock `server_choose` events and type for `event.input`, as we no longer need those.
  • Loading branch information
MrOrz authored Nov 22, 2023
2 parents 020ce97 + 1cf2ae9 commit 90358bc
Show file tree
Hide file tree
Showing 9 changed files with 293 additions and 545 deletions.
54 changes: 3 additions & 51 deletions src/types/chatbotState.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Message, MessageEvent, PostbackEvent } from '@line/bot-sdk';
import type { Message, MessageEvent } from '@line/bot-sdk';

export type ChatbotState =
| '__INIT__'
Expand All @@ -9,38 +9,6 @@ export type ChatbotState =
| 'ASKING_ARTICLE_SUBMISSION_CONSENT'
| 'Error';

/**
* Dummy event, used exclusively when calling handler from another handler
*/
type ServerChooseEvent = {
type: 'server_choose';
};

/**
* Parameters that are added by handleInput.
*
* @todo: We should consider using value from authentic event instead of manually adding fields.
*/
type ArgumentedEventParams = {
/**
* The text in text message, or value from payload in actions.
*/
input: string;
};

export type ChatbotEvent = (
| MessageEvent
| ServerChooseEvent
/**
* A special format of postback that Chatbot actually uses: postback + input (provided in `ArgumentedEventParams`)
* @FIXME Replace with original PostbackEvent and parse its action to support passing more thing than a string
*/
| {
type: 'postback';
}
) &
ArgumentedEventParams;

export type Context = {
/** Used to differientiate different search sessions (searched text or media) */
sessionId: number;
Expand All @@ -58,27 +26,11 @@ export type Context = {
}
);

export type ChatbotStateHandlerParams = {
/** Record<string, never> is for empty object and it's the default parameter in handleInput and handlePostback */
data: Context | Record<string, never>;
state: ChatbotState;
event: ChatbotEvent;
userId: string;
export type ChatbotStateHandlerReturnType = {
data: Context;
replies: Message[];
};

export type ChatbotStateHandlerReturnType = Pick<
ChatbotStateHandlerParams,
'data' | 'replies'
>;

/**
* Generic handler type for function under src/webhook/handlers
*/
export type ChatbotStateHandler = (
params: ChatbotStateHandlerParams
) => Promise<ChatbotStateHandlerReturnType>;

/**
* The data that postback action stores as JSON.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import MockDate from 'mockdate';
import initState from '../handlers/initState';
import handleInput from '../handleInput';
import originalInitState from '../handlers/initState';
import originalHandlePostback from '../handlePostback';
import { TUTORIAL_STEPS } from '../handlers/tutorial';
import handlePostback from '../handlePostback';

import { VIEW_ARTICLE_PREFIX, getArticleURL } from 'src/lib/sharedUtils';
import { MessageEvent, TextEventMessage } from '@line/bot-sdk';

jest.mock('../handlers/initState');
jest.mock('../handlePostback');

// Original session ID in context
const FIXED_DATE = 612964800000;
const initState = originalInitState as jest.MockedFunction<
typeof originalInitState
>;
const handlePostback = originalHandlePostback as jest.MockedFunction<
typeof originalHandlePostback
>;

// If session is renewed, sessionId will become this value
const NOW = 1561982400000;
Expand All @@ -25,40 +30,47 @@ afterEach(() => {
MockDate.reset();
});

it('rejects undefined input', () => {
const data = {};
const event = {};

return expect(handleInput(data, event)).rejects.toMatchInlineSnapshot(
`[Error: input undefined]`
);
});

it('shows reply list when VIEW_ARTICLE_PREFIX is sent', async () => {
const context = {
data: { sessionId: FIXED_DATE },
};
const event = {
function createTextMessageEvent(
input: string
): MessageEvent & { message: Pick<TextEventMessage, 'type' | 'text'> } {
return {
type: 'message',
input: `${VIEW_ARTICLE_PREFIX}${getArticleURL('article-id')}`,
message: {
id: '',
type: 'text',
text: input,
},
mode: 'active',
timestamp: 0,
source: {
type: 'user',
userId: '',
},
replyToken: '',
};
}

it('shows reply list when VIEW_ARTICLE_PREFIX is sent', async () => {
const event = createTextMessageEvent(
`${VIEW_ARTICLE_PREFIX}${getArticleURL('article-id')}`
);

handlePostback.mockImplementationOnce((data) => {
return Promise.resolve({
context: { data },
replies: 'Foo replies',
replies: [],
});
});

await expect(handleInput(context, event)).resolves.toMatchInlineSnapshot(`
await expect(handleInput(event, 'user-id')).resolves.toMatchInlineSnapshot(`
Object {
"context": Object {
"data": Object {
"searchedText": "",
"sessionId": 1561982400000,
},
},
"replies": "Foo replies",
"replies": Array [],
}
`);

Expand All @@ -75,37 +87,33 @@ it('shows reply list when VIEW_ARTICLE_PREFIX is sent', async () => {
"sessionId": 1561982400000,
"state": "CHOOSING_ARTICLE",
},
undefined,
"user-id",
],
]
`);
});

it('shows reply list when article URL is sent', async () => {
const context = {
data: { sessionId: FIXED_DATE },
};
const event = {
type: 'message',
input: getArticleURL('article-id') + ' \n ' /* simulate manual input */,
};
const event = createTextMessageEvent(
getArticleURL('article-id') + ' \n ' /* simulate manual input */
);

handlePostback.mockImplementationOnce((data) => {
return Promise.resolve({
context: { data },
replies: 'Foo replies',
replies: [],
});
});

await expect(handleInput(context, event)).resolves.toMatchInlineSnapshot(`
await expect(handleInput(event, 'user-id')).resolves.toMatchInlineSnapshot(`
Object {
"context": Object {
"data": Object {
"searchedText": "",
"sessionId": 1561982400000,
},
},
"replies": "Foo replies",
"replies": Array [],
}
`);

Expand All @@ -122,37 +130,32 @@ it('shows reply list when article URL is sent', async () => {
"sessionId": 1561982400000,
"state": "CHOOSING_ARTICLE",
},
undefined,
"user-id",
],
]
`);
});

it('Resets session on free-form input, triggers fast-forward', async () => {
const context = {
data: { sessionId: FIXED_DATE },
};
const event = {
type: 'message',
input: 'Newly forwarded message',
};
const input = 'Newly forwarded message';
const event = createTextMessageEvent(input);

// eslint-disable-next-line no-unused-vars
initState.mockImplementationOnce(({ data, event, userId, replies }) => {
initState.mockImplementationOnce(({ data }) => {
return Promise.resolve({
data,
replies: 'Foo replies',
replies: [],
});
});

await expect(handleInput(context, event)).resolves.toMatchInlineSnapshot(`
await expect(handleInput(event, 'user-id')).resolves.toMatchInlineSnapshot(`
Object {
"context": Object {
"data": Object {
"searchedText": "Newly forwarded message",
"sessionId": 1561982400000,
},
},
"replies": "Foo replies",
"replies": Array [],
}
`);

Expand All @@ -162,80 +165,38 @@ it('Resets session on free-form input, triggers fast-forward', async () => {
Array [
Object {
"data": Object {
"searchedText": "Newly forwarded message",
"sessionId": 1561982400000,
},
"event": Object {
"input": "Newly forwarded message",
"type": "message",
},
"replies": Array [],
"state": "__INIT__",
"userId": undefined,
"userId": "user-id",
},
],
]
`);
});

describe('defaultState', () => {
it('handles wrong event type', async () => {
const context = {
data: { sessionId: FIXED_DATE },
};
const event = {
type: 'follow',
input: '',
};

await expect(handleInput(context, event)).resolves.toMatchInlineSnapshot(`
Object {
"context": Object {
"data": Object {
"sessionId": 612964800000,
},
},
"replies": Array [
Object {
"text": "我們看不懂 QQ
大俠請重新來過。",
"type": "text",
},
],
}
`);

expect(initState).not.toHaveBeenCalled();
});
});

describe('tutorial', () => {
it('handles tutorial trigger from rich menu', async () => {
const context = {
data: { sessionId: FIXED_DATE },
};
const event = {
type: 'message',
input: TUTORIAL_STEPS['RICH_MENU'],
};
const event = createTextMessageEvent(TUTORIAL_STEPS['RICH_MENU']);

handlePostback.mockImplementationOnce((data) => {
return Promise.resolve({
context: { data },
replies: 'Foo replies',
replies: [],
});
});

await expect(handleInput(context, event)).resolves.toMatchInlineSnapshot(`
Object {
"context": Object {
"data": Object {
"searchedText": "",
"sessionId": 1561982400000,
},
},
"replies": "Foo replies",
}
`);
await expect(handleInput(event, 'user-id')).resolves.toMatchInlineSnapshot(`
Object {
"context": Object {
"data": Object {
"searchedText": "",
"sessionId": 1561982400000,
},
},
"replies": Array [],
}
`);

expect(handlePostback).toHaveBeenCalledTimes(1);
});
Expand Down
Loading

0 comments on commit 90358bc

Please sign in to comment.