Skip to content
Snippets Groups Projects
Commit d4a449bb authored by Zack Cuddy's avatar Zack Cuddy :two: Committed by David Pisek
Browse files

Tanuki Bot Chat

This change hooks up the Tanuki
Bot Chat to the API and allows
users to communicate with the
bot in the drawer.
parent cbb1d60f
No related branches found
No related tags found
2 merge requests!122597doc/gitaly: Remove references to removed metrics,!117597Tanuki Bot Chat
Showing
with 757 additions and 4 deletions
......@@ -300,3 +300,8 @@ body.gl-dark {
// soften on darkmode
background-color: mix($gray-50, $orange-50, 75%) !important;
}
.tanuki-bot-chat-drawer .tanuki-bot-message {
// lightens chat bubble in darkmode as $gray-50 matches drawer background. See tanuki_bot_chat.scss
background-color: $gray-100;
}
<script>
import { GlDrawer, GlIcon, GlBadge } from '@gitlab/ui';
import { mapActions } from 'vuex';
import { __, s__ } from '~/locale';
import { helpCenterState } from '~/super_sidebar/constants';
import TanukiBotChat from './tanuki_bot_chat.vue';
import TanukiBotChatInput from './tanuki_bot_chat_input.vue';
export default {
name: 'TanukiBotChatApp',
......@@ -13,6 +16,8 @@ export default {
GlIcon,
GlDrawer,
GlBadge,
TanukiBotChat,
TanukiBotChatInput,
},
data() {
return {
......@@ -20,6 +25,7 @@ export default {
};
},
methods: {
...mapActions(['sendMessage']),
closeDrawer() {
this.helpCenterState.showTanukiBotChatDrawer = false;
},
......@@ -43,6 +49,12 @@ export default {
<gl-badge variant="muted">{{ $options.i18n.experiment }}</gl-badge>
</span>
</template>
<tanuki-bot-chat />
<template #footer>
<tanuki-bot-chat-input @submit="sendMessage" />
</template>
</gl-drawer>
<div
v-if="helpCenterState.showTanukiBotChatDrawer"
......
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { mapState } from 'vuex';
import TanukiBotChatMessage from './tanuki_bot_chat_message.vue';
export default {
name: 'TanukiBotChat',
components: {
TanukiBotChatMessage,
GlLoadingIcon,
},
computed: {
...mapState(['loading', 'messages']),
},
};
</script>
<template>
<section class="gl-h-full">
<transition-group
tag="div"
name="messages"
class="gl-display-flex gl-flex-direction-column gl-justify-content-end gl-h-auto tanuki-bot-chat-chat-messages"
>
<tanuki-bot-chat-message v-for="message in messages" :key="message.id" :message="message" />
</transition-group>
<gl-loading-icon v-if="loading" class="gl-display-inline-flex gl-w-full gl-mb-5" />
</section>
</template>
<script>
import { GlFormTextarea, GlLink, GlSprintf, GlButton } from '@gitlab/ui';
import { mapState } from 'vuex';
import { __, s__ } from '~/locale';
export default {
name: 'TanukiBotChatInput',
i18n: {
askAQuestion: s__('TanukiBot|Ask a question about GitLab'),
exampleQuestion: s__('TanukiBot|For example, %{linkStart}what is a fork?%{linkEnd}'),
whatIsAForkQuestion: s__('TanukiBot|What is a fork?'),
send: __('Send'),
},
components: {
GlFormTextarea,
GlLink,
GlSprintf,
GlButton,
},
data() {
return {
message: '',
};
},
computed: {
...mapState(['loading']),
},
methods: {
handleSubmit() {
if (this.loading) {
return;
}
this.$emit('submit', this.message);
this.message = '';
},
handleWhatIsAForkClick() {
if (this.loading) {
return;
}
this.$emit('submit', this.$options.i18n.whatIsAForkQuestion);
},
},
};
</script>
<template>
<div>
<gl-form-textarea
v-model="message"
:placeholder="$options.i18n.askAQuestion"
:no-resize="false"
class="tanuki-bot-chat-input-field"
autofocus
@keydown.enter.prevent="handleSubmit"
/>
<div class="gl-text-gray-500 gl-my-3">
<gl-sprintf :message="$options.i18n.exampleQuestion">
<template #link="{ content }">
<gl-link
class="gl-text-gray-500 gl-text-decoration-underline"
@click="handleWhatIsAForkClick"
>{{ content }}</gl-link
>
</template>
</gl-sprintf>
</div>
<div class="gl-display-flex gl-justify-content-end">
<gl-button
icon="paper-airplane"
variant="confirm"
:disabled="loading"
@click="handleSubmit"
>{{ $options.i18n.send }}</gl-button
>
</div>
</div>
</template>
<script>
import { GlLink, GlIcon } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { MESSAGE_TYPES, SOURCE_TYPES, TANUKI_BOT_FEEDBACK_ISSUE_URL } from '../constants';
export default {
name: 'TanukiBotChatMessage',
i18n: {
sources: s__('TanukiBot|Sources'),
giveFeedback: s__('TanukiBot|Give feedback'),
source: __('Source'),
},
components: {
GlLink,
GlIcon,
},
props: {
message: {
type: Object,
required: true,
},
},
computed: {
isUserMessage() {
return this.message.type === MESSAGE_TYPES.USER;
},
isTanukiMessage() {
return this.message.type === MESSAGE_TYPES.TANUKI;
},
hasSources() {
return this.message.sources?.length > 0;
},
},
mounted() {
this.$refs.message.scrollIntoView({ behavior: 'smooth' });
},
methods: {
getSourceIcon(sourceType) {
const currentSourceType = Object.values(SOURCE_TYPES).find(
({ value }) => value === sourceType,
);
return currentSourceType?.icon;
},
getSourceTitle({ title, source_type: sourceType, stage, group, date, author }) {
if (title) {
return title;
}
if (sourceType === SOURCE_TYPES.DOC.value) {
if (stage && group) {
return `${stage} / ${group}`;
}
}
if (sourceType === SOURCE_TYPES.BLOG.value) {
if (date && author) {
return `${date} / ${author}`;
}
}
return this.$options.i18n.source;
},
},
TANUKI_BOT_FEEDBACK_ISSUE_URL,
};
</script>
<template>
<div
ref="message"
data-testid="tanuki-bot-chat-message"
class="gl-py-3 gl-px-4 gl-mb-6 gl-rounded-lg"
:class="{
'gl-ml-auto gl-bg-blue-100 gl-text-blue-900': isUserMessage,
'tanuki-bot-message gl-text-gray-900': isTanukiMessage,
}"
>
<p class="gl-my-0">{{ message.msg }}</p>
<div v-if="isTanukiMessage" class="gl-display-flex gl-align-items-flex-end gl-mt-3">
<div
v-if="hasSources"
class="gl-mr-3 gl-text-gray-600"
data-testid="tanuki-bot-chat-message-sources"
>
<span>{{ $options.i18n.sources }}</span>
<ul class="gl-pl-5 gl-my-0">
<li v-for="(source, index) in message.sources" :key="index">
<gl-icon v-if="source.source_type" :name="getSourceIcon(source.source_type)" />
<gl-link :href="source.source_url">{{ getSourceTitle(source) }}</gl-link>
</li>
</ul>
</div>
<gl-link
class="gl-ml-auto gl-white-space-nowrap"
:href="$options.TANUKI_BOT_FEEDBACK_ISSUE_URL"
target="_blank"
><gl-icon name="comment" /> {{ $options.i18n.giveFeedback }}</gl-link
>
</div>
</div>
</template>
import { s__ } from '~/locale';
export const MESSAGE_TYPES = {
USER: 'user',
TANUKI: 'tanuki',
};
export const SOURCE_TYPES = {
HANDBOOK: {
value: 'handbook',
icon: 'book',
},
DOC: {
value: 'doc',
icon: 'documents',
},
BLOG: {
value: 'blog',
icon: 'list-bulleted',
},
};
export const ERROR_MESSAGE = s__(
'TanukiBot|There was an error communicating with Tanuki Bot. Please reach out to GitLab support for more assistance or try again later.',
);
export const TANUKI_BOT_FEEDBACK_ISSUE_URL = 'https://gitlab.com/gitlab-org/gitlab/-/issues/408527';
import Vue from 'vue';
import TanukiBotChatApp from './components/app.vue';
import store from './store';
export const initTanukiBotChatDrawer = () => {
const el = document.getElementById('js-tanuki-bot-chat-app');
......@@ -10,6 +11,7 @@ export const initTanukiBotChatDrawer = () => {
return new Vue({
el,
store,
render(createElement) {
return createElement(TanukiBotChatApp);
},
......
import Api from 'ee/api';
import * as types from './mutation_types';
export const sendMessage = async ({ commit }, msg) => {
try {
commit(types.SET_LOADING, true);
commit(types.ADD_USER_MESSAGE, msg);
const { data } = await Api.requestTanukiBotResponse(msg);
commit(types.ADD_TANUKI_MESSAGE, data);
} catch {
commit(types.ADD_ERROR_MESSAGE);
} finally {
commit(types.SET_LOADING, false);
}
};
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import mutations from './mutations';
import state from './state';
Vue.use(Vuex);
export default new Vuex.Store({
actions,
mutations,
state,
});
export const SET_LOADING = 'SET_LOADING';
export const ADD_USER_MESSAGE = 'ADD_USER_MESSAGE';
export const ADD_TANUKI_MESSAGE = 'ADD_TANUKI_MESSAGE';
export const ADD_ERROR_MESSAGE = 'ADD_ERROR_MESSAGE';
import { MESSAGE_TYPES, ERROR_MESSAGE } from '../constants';
import * as types from './mutation_types';
export default {
[types.SET_LOADING](state, loading) {
state.loading = loading;
},
[types.ADD_USER_MESSAGE](state, msg) {
state.messages.push({ id: state.messages.length, type: MESSAGE_TYPES.USER, msg });
},
[types.ADD_TANUKI_MESSAGE](state, data) {
state.messages.push({ id: state.messages.length, type: MESSAGE_TYPES.TANUKI, ...data });
},
[types.ADD_ERROR_MESSAGE](state) {
state.messages.push({
id: state.messages.length,
type: MESSAGE_TYPES.TANUKI,
msg: ERROR_MESSAGE,
});
},
};
const createState = () => ({
loading: false,
messages: [],
});
export default createState;
......@@ -41,6 +41,7 @@ export default {
aiCompletionsPath: '/api/:version/ai/experimentation/openai/completions',
aiEmbeddingsPath: '/api/:version/ai/experimentation/openai/embeddings',
aiChatPath: '/api/:version/ai/experimentation/openai/chat/completions',
tanukiBotAskPath: '-/llm/tanuki_bot/ask',
userSubscription(namespaceId) {
const url = Api.buildUrl(this.subscriptionPath).replace(':id', encodeURIComponent(namespaceId));
......@@ -325,4 +326,9 @@ export default {
const url = Api.buildUrl(this.aiChatPath);
return axios.post(url, { model, messages, rest });
},
requestTanukiBotResponse(q) {
const url = Api.buildUrl(this.tanukiBotAskPath);
return axios.post(url, { q });
},
};
......@@ -8,6 +8,48 @@
overflow-y: hidden;
max-width: $wide-drawer;
width: 100%;
.tanuki-bot-chat-input-field {
max-height: 200px;
height: 2rem;
}
.gl-drawer-body-scrim-on-footer {
&::before {
background: none;
}
}
.gl-drawer-body {
overflow-y: auto;
}
.tanuki-bot-chat-chat-messages {
min-height: 100%;
}
.tanuki-bot-message {
background-color: $gray-50; // Overridden to $gray-100 in dark_mode_overrides.scss
}
.messages-move {
transition: all 400ms ease;
}
.messages-enter-active,
.messages-leave-active {
animation: 400ms ease 0s 1 slide-in;
}
@keyframes slide-in {
0% {
transform: translateY(200%);
}
100% {
transform: translateY(0);
}
}
}
.tanuki-bot-backdrop {
......
import { GlDrawer } from '@gitlab/ui';
import { nextTick } from 'vue';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import TanukiBotChatApp from 'ee/ai/tanuki_bot/components/app.vue';
import TanukiBotChat from 'ee/ai/tanuki_bot/components/tanuki_bot_chat.vue';
import TanukiBotChatInput from 'ee/ai/tanuki_bot/components/tanuki_bot_chat_input.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { helpCenterState } from '~/super_sidebar/constants';
import { MOCK_USER_MESSAGE } from '../mock_data';
Vue.use(Vuex);
describe('TanukiBotChatApp', () => {
let wrapper;
const actionSpies = {
sendMessage: jest.fn(),
};
const createComponent = () => {
const store = new Vuex.Store({
actions: actionSpies,
});
wrapper = shallowMountExtended(TanukiBotChatApp, {
store,
stubs: {
GlDrawer,
Portal: {
template: '<div><slot></slot></div>',
},
},
});
};
const findGlDrawer = () => wrapper.findComponent(GlDrawer);
const findGlDrawerBackdrop = () => wrapper.findByTestId('tanuki-bot-chat-drawer-backdrop');
const findTanukiBotChat = () => wrapper.findComponent(TanukiBotChat);
const findTanukiBotChatInput = () => wrapper.findComponent(TanukiBotChatInput);
describe('GlDrawer interactions', () => {
beforeEach(() => {
......@@ -64,4 +78,24 @@ describe('TanukiBotChatApp', () => {
});
});
});
describe('Tanuki Chat', () => {
beforeEach(() => {
createComponent();
helpCenterState.showTanukiBotChatDrawer = true;
});
it('renders TanukiBotChat', () => {
expect(findTanukiBotChat().exists()).toBe(true);
});
it('calls sendMessage when input is submitted', () => {
findTanukiBotChatInput().vm.$emit('submit', MOCK_USER_MESSAGE.msg);
expect(actionSpies.sendMessage).toHaveBeenCalledWith(
expect.any(Object),
MOCK_USER_MESSAGE.msg,
);
});
});
});
import { GlSprintf, GlFormTextarea, GlLink, GlButton } from '@gitlab/ui';
import Vue from 'vue';
import Vuex from 'vuex';
import { ENTER_KEY } from '~/lib/utils/keys';
import TanukiBotChatInput from 'ee/ai/tanuki_bot/components/tanuki_bot_chat_input.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { MOCK_USER_MESSAGE } from '../mock_data';
Vue.use(Vuex);
describe('TanukiBotChatInput', () => {
let wrapper;
const createComponent = (initialState) => {
const store = new Vuex.Store({
state: {
loading: false,
...initialState,
},
});
wrapper = shallowMountExtended(TanukiBotChatInput, {
store,
stubs: {
GlSprintf,
},
});
};
const findGlFormTextarea = () => wrapper.findComponent(GlFormTextarea);
const findGlButton = () => wrapper.findComponent(GlButton);
const findGlLink = () => wrapper.findComponent(GlLink);
describe('when not loading', () => {
beforeEach(() => {
createComponent();
});
it('can send message via pressing Enter on the text field', () => {
findGlFormTextarea().vm.$emit('input', MOCK_USER_MESSAGE.msg);
findGlFormTextarea().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
expect(wrapper.emitted('submit')).toStrictEqual([[MOCK_USER_MESSAGE.msg]]);
});
it('can send message via pressing Send button', () => {
findGlFormTextarea().vm.$emit('input', MOCK_USER_MESSAGE.msg);
findGlButton().vm.$emit('click');
expect(wrapper.emitted('submit')).toStrictEqual([[MOCK_USER_MESSAGE.msg]]);
});
it('can send message via pressing "what is a fork?" link', () => {
findGlFormTextarea().vm.$emit('input', MOCK_USER_MESSAGE.msg);
findGlLink().vm.$emit('click');
expect(wrapper.emitted('submit')).toStrictEqual([['What is a fork?']]);
});
});
describe('when loading', () => {
beforeEach(() => {
createComponent({ loading: true });
});
it('can not send message via pressing Enter on the text field', () => {
findGlFormTextarea().vm.$emit('input', MOCK_USER_MESSAGE.msg);
findGlFormTextarea().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
expect(wrapper.emitted('submit')).toBeUndefined();
});
it('can not send message via pressing Send button', () => {
findGlFormTextarea().vm.$emit('input', MOCK_USER_MESSAGE.msg);
findGlButton().vm.$emit('click');
expect(wrapper.emitted('submit')).toBeUndefined();
});
it('can not send message via pressing "what is a fork?" link', () => {
findGlFormTextarea().vm.$emit('input', MOCK_USER_MESSAGE.msg);
findGlLink().vm.$emit('click');
expect(wrapper.emitted('submit')).toBeUndefined();
});
});
});
import { GlLink, GlIcon } from '@gitlab/ui';
import TanukiBotChatMessage from 'ee/ai/tanuki_bot/components/tanuki_bot_chat_message.vue';
import { SOURCE_TYPES, TANUKI_BOT_FEEDBACK_ISSUE_URL } from 'ee/ai/tanuki_bot/constants';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { MOCK_USER_MESSAGE, MOCK_TANUKI_MESSAGE, MOCK_SOURCE_TYPES } from '../mock_data';
describe('TanukiBotChatMessage', () => {
let wrapper;
const defaultProps = {
messages: MOCK_USER_MESSAGE,
};
const createComponent = (props) => {
wrapper = shallowMountExtended(TanukiBotChatMessage, {
propsData: {
...defaultProps,
...props,
},
});
};
const findTanukiBotChatMessage = () => wrapper.findByTestId('tanuki-bot-chat-message');
const findSendFeedbackLink = () => wrapper.findByText('Give feedback');
const findTanukiBotChatMessageSources = () =>
wrapper.findByTestId('tanuki-bot-chat-message-sources');
const findSourceLink = () => findTanukiBotChatMessageSources().findComponent(GlLink);
const findSourceIcon = () => findTanukiBotChatMessageSources().findComponent(GlIcon);
describe('when message is a User message', () => {
beforeEach(() => {
createComponent({ message: MOCK_USER_MESSAGE });
});
it('uses the correct classList', () => {
expect(findTanukiBotChatMessage().classes()).toEqual(
expect.arrayContaining(['gl-ml-auto', 'gl-bg-blue-100', 'gl-text-blue-900']),
);
});
it('does not render Share Feedback link', () => {
expect(findSendFeedbackLink().exists()).toBe(false);
});
it('does not render sources', () => {
expect(findTanukiBotChatMessageSources().exists()).toBe(false);
});
});
describe('when message is a Tanuki message', () => {
describe('default', () => {
beforeEach(() => {
createComponent({ message: MOCK_TANUKI_MESSAGE });
});
it('uses the correct classList', () => {
expect(findTanukiBotChatMessage().classes()).toEqual(
expect.arrayContaining(['tanuki-bot-message', 'gl-text-gray-900']),
);
});
it('does render Share Feedback Link', () => {
expect(findSendFeedbackLink().attributes('href')).toBe(TANUKI_BOT_FEEDBACK_ISSUE_URL);
});
});
describe('Sources', () => {
describe('when no sources available', () => {
beforeEach(() => {
createComponent({ message: { ...MOCK_TANUKI_MESSAGE, sources: [] } });
});
it('does not render sources', () => {
expect(findTanukiBotChatMessageSources().exists()).toBe(false);
});
});
describe('when sources are provided', () => {
beforeEach(() => {
createComponent({ message: MOCK_TANUKI_MESSAGE });
});
it('does render sources', () => {
expect(findTanukiBotChatMessageSources().exists()).toBe(true);
});
});
describe.each`
sourceType | mockSource | expectedText
${SOURCE_TYPES.HANDBOOK} | ${MOCK_SOURCE_TYPES.HANDBOOK} | ${MOCK_SOURCE_TYPES.HANDBOOK.title}
${SOURCE_TYPES.DOC} | ${MOCK_SOURCE_TYPES.DOC} | ${`${MOCK_SOURCE_TYPES.DOC.stage} / ${MOCK_SOURCE_TYPES.DOC.group}`}
${SOURCE_TYPES.BLOG} | ${MOCK_SOURCE_TYPES.BLOG} | ${`${MOCK_SOURCE_TYPES.BLOG.date} / ${MOCK_SOURCE_TYPES.BLOG.author}`}
`('when provided source is $sourceType.value', ({ sourceType, mockSource, expectedText }) => {
beforeEach(() => {
createComponent({ message: { ...MOCK_TANUKI_MESSAGE, sources: [mockSource] } });
});
it('renders the correct icon', () => {
expect(findSourceIcon().props('name')).toBe(sourceType.icon);
});
it('renders the correct link URL', () => {
expect(findSourceLink().attributes('href')).toBe(mockSource.source_url);
});
it('renders the correct link text', () => {
expect(findSourceLink().text()).toBe(expectedText);
});
});
});
});
});
import { GlLoadingIcon } from '@gitlab/ui';
import Vue from 'vue';
import Vuex from 'vuex';
import TanukiBotChat from 'ee/ai/tanuki_bot/components/tanuki_bot_chat.vue';
import TanukiBotChatMessages from 'ee/ai/tanuki_bot/components/tanuki_bot_chat_message.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { MOCK_USER_MESSAGE, MOCK_TANUKI_MESSAGE } from '../mock_data';
Vue.use(Vuex);
describe('TanukiBotChat', () => {
let wrapper;
const defaultState = {
loading: false,
messages: [],
};
const createComponent = (initialState) => {
const store = new Vuex.Store({
state: {
...defaultState,
...initialState,
},
});
wrapper = shallowMountExtended(TanukiBotChat, {
store,
});
};
const findTanukiBotChatMessages = () => wrapper.findAllComponents(TanukiBotChatMessages);
const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
describe('GlLoadingIcon', () => {
it.each([true, false])('when loading is "%s" it renders/does not render"', (loading) => {
createComponent({ loading });
expect(findGlLoadingIcon().exists()).toBe(loading);
});
});
describe('TanukiBotChatMessages', () => {
beforeEach(() => {
createComponent({ messages: [MOCK_USER_MESSAGE, MOCK_TANUKI_MESSAGE] });
});
it('renders for each message', () => {
expect(findTanukiBotChatMessages().length).toBe(2);
expect(findTanukiBotChatMessages().wrappers.map((w) => w.props('message'))).toStrictEqual([
MOCK_USER_MESSAGE,
MOCK_TANUKI_MESSAGE,
]);
});
});
});
import { MESSAGE_TYPES, SOURCE_TYPES } from 'ee/ai/tanuki_bot/constants';
export const MOCK_SOURCE_TYPES = {
HANDBOOK: {
title: 'GitLab Handbook',
source_type: SOURCE_TYPES.HANDBOOK.value,
source_url: 'https://about.gitlab.com/handbook/',
},
DOC: {
stage: 'Mock Stage',
group: 'Mock Group',
source_type: SOURCE_TYPES.DOC.value,
source_url: 'https://about.gitlab.com/company/team/',
},
BLOG: {
date: '2023-04-21',
author: 'Test User',
source_type: SOURCE_TYPES.BLOG.value,
source_url: 'https://about.gitlab.com/blog/',
},
};
export const MOCK_SOURCES = Object.values(MOCK_SOURCE_TYPES);
export const MOCK_TANUKI_MESSAGE = {
msg: 'Tanuki Bot message',
type: MESSAGE_TYPES.TANUKI,
sources: MOCK_SOURCES,
};
export const MOCK_USER_MESSAGE = {
msg: 'User message',
type: MESSAGE_TYPES.USER,
};
import MockAdapter from 'axios-mock-adapter';
import * as actions from 'ee/ai/tanuki_bot/store/actions';
import * as types from 'ee/ai/tanuki_bot/store/mutation_types';
import createState from 'ee/ai/tanuki_bot/store/state';
import testAction from 'helpers/vuex_action_helper';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK, HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
import { MOCK_USER_MESSAGE, MOCK_TANUKI_MESSAGE } from '../mock_data';
describe('TanukiBot Store Actions', () => {
let state;
let mock;
beforeEach(() => {
state = createState();
mock = new MockAdapter(axios);
});
afterEach(() => {
state = null;
mock.restore();
});
describe('sendMessage', () => {
describe('onSuccess', () => {
beforeEach(() => {
mock.onPost().reply(HTTP_STATUS_OK, MOCK_TANUKI_MESSAGE);
});
it(`should dispatch the correct mutations`, () => {
return testAction({
action: actions.sendMessage,
payload: MOCK_USER_MESSAGE.msg,
state,
expectedMutations: [
{ type: types.SET_LOADING, payload: true },
{ type: types.ADD_USER_MESSAGE, payload: MOCK_USER_MESSAGE.msg },
{ type: types.ADD_TANUKI_MESSAGE, payload: MOCK_TANUKI_MESSAGE },
{ type: types.SET_LOADING, payload: false },
],
});
});
});
describe('onError', () => {
beforeEach(() => {
mock.onPost().reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it(`should dispatch the correct mutations`, () => {
return testAction({
action: actions.sendMessage,
payload: MOCK_USER_MESSAGE.msg,
state,
expectedMutations: [
{ type: types.SET_LOADING, payload: true },
{ type: types.ADD_USER_MESSAGE, payload: MOCK_USER_MESSAGE.msg },
{ type: types.ADD_ERROR_MESSAGE },
{ type: types.SET_LOADING, payload: false },
],
});
});
});
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment