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

[1] Simplify postback handler signatures #372

Merged
merged 25 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
cbd4064
refactor(context): typing postback action
MrOrz Oct 16, 2023
fb8968e
fix: simple handler type
MrOrz Oct 29, 2023
b071d64
fix: remove input from postback handler
MrOrz Oct 30, 2023
65f406d
fix: narrow down choosingArticle input source
MrOrz Oct 30, 2023
a72821d
refactor(types): define postback handler type
MrOrz Nov 5, 2023
d6718d6
refactor(webhook): type defaultState
MrOrz Nov 5, 2023
6c1af7a
refactor(webhook): type tutorial
MrOrz Nov 5, 2023
2433f47
refactor(webhook): apply simpler postback handler signatures
MrOrz Nov 5, 2023
7b41e99
refactor(webhook): apply ChatbotPostbackHandler to askingArticleSourc…
MrOrz Nov 6, 2023
42ec52b
docs(chatbotState): document fields in new postback handlers
MrOrz Nov 6, 2023
9d74cea
fix(webhook): fix handlePostback invocations
MrOrz Nov 6, 2023
5b57717
fix(webhook): fix askingArticleSource test
MrOrz Nov 6, 2023
771293b
refactor(webhook): remove unused imports from tutorial
MrOrz Nov 6, 2023
6d9d49e
fix(webhook): allow tutorials to open new sessions and call as postba…
MrOrz Nov 6, 2023
fa5de52
fix(types): restore ChatbotEvent to original type
MrOrz Nov 6, 2023
a3b6a12
fix(webhook): fix processMedia signature and invocation of choosingAr…
MrOrz Nov 6, 2023
7b83d4d
fix(webhook): type for initState
MrOrz Nov 6, 2023
dcb8e30
fix(handlers): fix test for askingArticleSubmissionConsent
MrOrz Nov 7, 2023
8f910d3
refactor: expose MockedGql type
MrOrz Nov 12, 2023
7c9578b
refactor(webhook): convert choosingArticle test to ts
MrOrz Nov 12, 2023
cab75e4
refactor(webhook): convert choosingReply test to TS
MrOrz Nov 12, 2023
bd28cbe
refactor(webhook): rewrite tutorial webhook to TS
MrOrz Nov 12, 2023
43739ba
refactor(webhook): update tests and snapshot for postback handlers
MrOrz Nov 12, 2023
47f5b5e
fix(handlers): fix how processMedia is called in unit tests
MrOrz Nov 12, 2023
fef2cd7
refactor(webhook): remove unused import
MrOrz Nov 12, 2023
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
1 change: 1 addition & 0 deletions src/lib/__mocks__/ga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ const ga = Object.assign(
}
);

export type MockedGa = typeof ga;
export default ga;
1 change: 1 addition & 0 deletions src/lib/__mocks__/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@ gqlMock.__finished = function () {
};

export default gqlMock;
export type MockedGql = typeof gqlMock;
45 changes: 31 additions & 14 deletions src/types/chatbotState.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Message, MessageEvent } from '@line/bot-sdk';
import type { Message, MessageEvent, PostbackEvent } from '@line/bot-sdk';

Check warning on line 1 in src/types/chatbotState.ts

View workflow job for this annotation

GitHub Actions / install-and-test

'PostbackEvent' is defined but never used

Check warning on line 1 in src/types/chatbotState.ts

View workflow job for this annotation

GitHub Actions / install-and-test

'PostbackEvent' is defined but never used

Check warning on line 1 in src/types/chatbotState.ts

View workflow job for this annotation

GitHub Actions / install-and-test

'PostbackEvent' is defined but never used

export type ChatbotState =
| '__INIT__'
Expand Down Expand Up @@ -44,17 +44,19 @@
export type Context = {
/** Used to differientiate different search sessions (searched text or media) */
sessionId: number;

/** Searched text that started this search session */
searchedText?: string;

/** Searched multi-media message that started this search session */
messageId: MessageEvent['message']['id'];
messageType: MessageEvent['message']['type'];

/** User selected article in DB */
selectedArticleId?: string;
};
} & (
| {
/** Searched multi-media message that started this search session */
messageId: MessageEvent['message']['id'];
messageType: MessageEvent['message']['type'];
}
| {
/** Searched text that started this search session */
searchedText: string;
}
);

export type ChatbotStateHandlerParams = {
/** Record<string, never> is for empty object and it's the default parameter in handleInput and handlePostback */
Expand All @@ -65,9 +67,9 @@
replies: Message[];
};

export type ChatbotStateHandlerReturnType = Omit<
export type ChatbotStateHandlerReturnType = Pick<
ChatbotStateHandlerParams,
/** The state is determined by payloads in actions. No need to return state. */ 'state'
'data' | 'replies'
>;

/**
Expand All @@ -82,8 +84,23 @@
*
* @FIXME Replace input: string with something that is more structured
*/
export type PostbackActionData = {
input: string;
export type PostbackActionData<T> = {
input: T;
sessionId: number;
state: ChatbotState;
};

export type ChatbotPostbackHandlerParams<T = unknown> = {
/** Data stored in Chatbot context */
data: Context;
/** Data in postback payload */
postbackData: PostbackActionData<T>;
userId: string;
};

/**
* For chatbot postback event handers
*/
export type ChatbotPostbackHandler<T = unknown> = (
params: ChatbotPostbackHandlerParams<T>
) => Promise<ChatbotStateHandlerReturnType>;
94 changes: 42 additions & 52 deletions src/webhook/__tests__/handleInput.test.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import MockDate from 'mockdate';
import initState from '../handlers/initState';
import handleInput from '../handleInput';
import tutorial, { TUTORIAL_STEPS } from '../handlers/tutorial';
import { TUTORIAL_STEPS } from '../handlers/tutorial';
import handlePostback from '../handlePostback';

import { VIEW_ARTICLE_PREFIX, getArticleURL } from 'src/lib/sharedUtils';

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

// Original session ID in context
Expand All @@ -19,7 +18,6 @@ const NOW = 1561982400000;
beforeEach(() => {
initState.mockClear();
handlePostback.mockClear();
tutorial.mockClear();
MockDate.set(NOW);
});

Expand All @@ -45,10 +43,9 @@ it('shows reply list when VIEW_ARTICLE_PREFIX is sent', async () => {
input: `${VIEW_ARTICLE_PREFIX}${getArticleURL('article-id')}`,
};

// eslint-disable-next-line no-unused-vars
handlePostback.mockImplementationOnce((context, state, event, userid) => {
handlePostback.mockImplementationOnce((data) => {
return Promise.resolve({
context,
context: { data },
replies: 'Foo replies',
});
});
Expand All @@ -70,15 +67,13 @@ it('shows reply list when VIEW_ARTICLE_PREFIX is sent', async () => {
Array [
Array [
Object {
"data": Object {
"searchedText": "",
"sessionId": 1561982400000,
},
"searchedText": "",
"sessionId": 1561982400000,
},
"CHOOSING_ARTICLE",
Object {
"input": "article-id",
"type": "postback",
"sessionId": 1561982400000,
"state": "CHOOSING_ARTICLE",
},
undefined,
],
Expand All @@ -95,10 +90,9 @@ it('shows reply list when article URL is sent', async () => {
input: getArticleURL('article-id') + ' \n ' /* simulate manual input */,
};

// eslint-disable-next-line no-unused-vars
handlePostback.mockImplementationOnce((context, state, event, userid) => {
handlePostback.mockImplementationOnce((data) => {
return Promise.resolve({
context,
context: { data },
replies: 'Foo replies',
});
});
Expand All @@ -120,15 +114,13 @@ it('shows reply list when article URL is sent', async () => {
Array [
Array [
Object {
"data": Object {
"searchedText": "",
"sessionId": 1561982400000,
},
"searchedText": "",
"sessionId": 1561982400000,
},
"CHOOSING_ARTICLE",
Object {
"input": "article-id",
"type": "postback",
"sessionId": 1561982400000,
"state": "CHOOSING_ARTICLE",
},
undefined,
],
Expand Down Expand Up @@ -196,21 +188,21 @@ describe('defaultState', () => {
};

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

expect(initState).not.toHaveBeenCalled();
});
Expand All @@ -226,27 +218,25 @@ describe('tutorial', () => {
input: TUTORIAL_STEPS['RICH_MENU'],
};

tutorial.mockImplementationOnce((params) => {
// it doesn't return `state`, discard it
// eslint-disable-next-line no-unused-vars
const { state, ...restParams } = params;
return {
...restParams,
handlePostback.mockImplementationOnce((data) => {
return Promise.resolve({
context: { data },
replies: 'Foo replies',
};
});
});

await expect(handleInput(context, event)).resolves.toMatchInlineSnapshot(`
Object {
"context": Object {
"data": Object {
"sessionId": 612964800000,
},
},
"replies": "Foo replies",
}
`);
Object {
"context": Object {
"data": Object {
"searchedText": "",
"sessionId": 1561982400000,
},
},
"replies": "Foo replies",
}
`);

expect(tutorial).toHaveBeenCalledTimes(1);
expect(handlePostback).toHaveBeenCalledTimes(1);
});
});
Loading
Loading