Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[2] simplify text handlers as well #371

Merged
merged 5 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading