Commit 006e0b67 authored by Phil Hughes's avatar Phil Hughes

Refactors the awards block to use Vue

Converts our awards block which is handled by JS and HAML
to use Vue which allows us to use the new emoji picker.

Closes https://gitlab.com/gitlab-org/gitlab/-/issues/324666
parent 987be4a5
import Vue from 'vue';
import { mapActions, mapState } from 'vuex';
import { parseBoolean } from '~/lib/utils/common_utils';
import AwardsList from '~/vue_shared/components/awards_list.vue';
import createstore from './store';
export default (el) => {
const {
dataset: { path },
} = el;
const canAwardEmoji = parseBoolean(el.dataset.canAwardEmoji);
return new Vue({
el,
store: createstore(),
computed: {
...mapState(['currentUserId', 'canAwardEmoji', 'awards']),
},
created() {
this.setInitialData({ path, currentUserId: window.gon.current_user_id, canAwardEmoji });
},
mounted() {
this.fetchAwards();
},
methods: {
...mapActions(['setInitialData', 'fetchAwards', 'toggleAward']),
},
render(createElement) {
return createElement(AwardsList, {
props: {
awards: this.awards,
canAwardEmoji: this.canAwardEmoji,
currentUserId: this.currentUserId,
defaultAwards: ['thumbsup', 'thumbsdown'],
selectedClass: 'gl-bg-blue-50! is-active',
},
on: {
award: this.toggleAward,
},
});
},
});
};
import * as Sentry from '@sentry/browser';
import axios from '~/lib/utils/axios_utils';
import { normalizeHeaders } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import showToast from '~/vue_shared/plugins/global_toast';
import {
SET_INITIAL_DATA,
FETCH_AWARDS_SUCCESS,
ADD_NEW_AWARD,
REMOVE_AWARD,
} from './mutation_types';
export const setInitialData = ({ commit }, data) => commit(SET_INITIAL_DATA, data);
export const fetchAwards = async ({ commit, dispatch, state }, page = '1') => {
try {
const { data, headers } = await axios.get(state.path, { params: { per_page: 100, page } });
const normalizedHeaders = normalizeHeaders(headers);
const nextPage = normalizedHeaders['X-NEXT-PAGE'];
commit(FETCH_AWARDS_SUCCESS, data);
if (nextPage) {
dispatch('fetchAwards', nextPage);
}
} catch (error) {
Sentry.captureException(error);
}
};
export const toggleAward = async ({ commit, state }, name) => {
const award = state.awards.find((a) => a.name === name && a.user.id === state.currentUserId);
try {
if (award) {
await axios.delete(`${state.path}/${award.id}`);
commit(REMOVE_AWARD, award.id);
showToast(__('Award removed'));
} else {
const { data } = await axios.post(state.path, { name });
commit(ADD_NEW_AWARD, data);
showToast(__('Award added'));
}
} catch (error) {
Sentry.captureException(error);
}
};
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import mutations from './mutations';
Vue.use(Vuex);
const createState = () => ({
awards: [],
awardPath: '',
currentUserId: null,
canAwardEmoji: false,
});
export default () =>
new Vuex.Store({
state: createState(),
actions,
mutations,
});
export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
export const FETCH_AWARDS_SUCCESS = 'FETCH_AWARDS_SUCCESS';
export const ADD_NEW_AWARD = 'ADD_NEW_AWARD';
export const REMOVE_AWARD = 'REMOVE_AWARD';
import {
SET_INITIAL_DATA,
FETCH_AWARDS_SUCCESS,
ADD_NEW_AWARD,
REMOVE_AWARD,
} from './mutation_types';
export default {
[SET_INITIAL_DATA](state, { path, currentUserId, canAwardEmoji }) {
state.path = path;
state.currentUserId = currentUserId;
state.canAwardEmoji = canAwardEmoji;
},
[FETCH_AWARDS_SUCCESS](state, data) {
state.awards.push(...data);
},
[ADD_NEW_AWARD](state, data) {
state.awards.push(data);
},
[REMOVE_AWARD](state, awardId) {
state.awards = state.awards.filter(({ id }) => id !== awardId);
},
};
...@@ -82,6 +82,8 @@ export default { ...@@ -82,6 +82,8 @@ export default {
no-flip no-flip
right right
lazy lazy
@shown="$emit('shown')"
@hidden="$emit('hidden')"
> >
<template #button-content><slot name="button-content"></slot></template> <template #button-content><slot name="button-content"></slot></template>
<gl-search-box-by-type <gl-search-box-by-type
......
...@@ -46,10 +46,18 @@ export default function initShowIssue() { ...@@ -46,10 +46,18 @@ export default function initShowIssue() {
new ZenMode(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new
if (issueType !== IssuableType.TestCase) { if (issueType !== IssuableType.TestCase) {
const awardEmojiEl = document.getElementById('js-vue-awards-block');
new Issue(); // eslint-disable-line no-new new Issue(); // eslint-disable-line no-new
new ShortcutsIssuable(); // eslint-disable-line no-new new ShortcutsIssuable(); // eslint-disable-line no-new
initIssuableSidebar(); initIssuableSidebar();
loadAwardsHandler(); if (awardEmojiEl) {
import('~/emoji/awards_app')
.then((m) => m.default(awardEmojiEl))
.catch(() => {});
} else {
loadAwardsHandler();
}
initInviteMemberModal(); initInviteMemberModal();
initInviteMemberTrigger(); initInviteMemberTrigger();
} }
......
...@@ -13,13 +13,21 @@ import initSourcegraph from '~/sourcegraph'; ...@@ -13,13 +13,21 @@ import initSourcegraph from '~/sourcegraph';
import ZenMode from '~/zen_mode'; import ZenMode from '~/zen_mode';
export default function initMergeRequestShow() { export default function initMergeRequestShow() {
const awardEmojiEl = document.getElementById('js-vue-awards-block');
new ZenMode(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new
initIssuableSidebar(); initIssuableSidebar();
initPipelines(); initPipelines();
new ShortcutsIssuable(true); // eslint-disable-line no-new new ShortcutsIssuable(true); // eslint-disable-line no-new
handleLocationHash(); handleLocationHash();
initSourcegraph(); initSourcegraph();
loadAwardsHandler(); if (awardEmojiEl) {
import('~/emoji/awards_app')
.then((m) => m.default(awardEmojiEl))
.catch(() => {});
} else {
loadAwardsHandler();
}
initInviteMemberModal(); initInviteMemberModal();
initInviteMemberTrigger(); initInviteMemberTrigger();
initInviteMembersModal(); initInviteMembersModal();
......
...@@ -44,6 +44,16 @@ export default { ...@@ -44,6 +44,16 @@ export default {
required: false, required: false,
default: () => [], default: () => [],
}, },
selectedClass: {
type: String,
required: false,
default: 'selected',
},
},
data() {
return {
isMenuOpen: false,
};
}, },
computed: { computed: {
groupedDefaultAwards() { groupedDefaultAwards() {
...@@ -68,7 +78,7 @@ export default { ...@@ -68,7 +78,7 @@ export default {
methods: { methods: {
getAwardClassBindings(awardList) { getAwardClassBindings(awardList) {
return { return {
selected: this.hasReactionByCurrentUser(awardList), [this.selectedClass]: this.hasReactionByCurrentUser(awardList),
disabled: this.currentUserId === NO_USER_ID, disabled: this.currentUserId === NO_USER_ID,
}; };
}, },
...@@ -147,6 +157,11 @@ export default { ...@@ -147,6 +157,11 @@ export default {
const parsedName = /^[0-9]+$/.test(awardName) ? Number(awardName) : awardName; const parsedName = /^[0-9]+$/.test(awardName) ? Number(awardName) : awardName;
this.$emit('award', parsedName); this.$emit('award', parsedName);
if (document.activeElement) document.activeElement.blur();
},
setIsMenuOpen(menuOpen) {
this.isMenuOpen = menuOpen;
}, },
}, },
}; };
...@@ -172,8 +187,10 @@ export default { ...@@ -172,8 +187,10 @@ export default {
<div v-if="canAwardEmoji" class="award-menu-holder"> <div v-if="canAwardEmoji" class="award-menu-holder">
<emoji-picker <emoji-picker
v-if="glFeatures.improvedEmojiPicker" v-if="glFeatures.improvedEmojiPicker"
toggle-class="add-reaction-button gl-relative!" :toggle-class="['add-reaction-button gl-relative!', { 'is-active': isMenuOpen }]"
@click="handleAward" @click="handleAward"
@shown="setIsMenuOpen(true)"
@hidden="setIsMenuOpen(false)"
> >
<template #button-content> <template #button-content>
<span class="reaction-control-icon reaction-control-icon-neutral"> <span class="reaction-control-icon reaction-control-icon-neutral">
......
...@@ -362,3 +362,7 @@ ...@@ -362,3 +362,7 @@
} }
} }
} }
.awards .is-active {
box-shadow: inset 0 0 0 1px $blue-200;
}
...@@ -43,6 +43,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -43,6 +43,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:new_pipelines_table, @project, default_enabled: :yaml) push_frontend_feature_flag(:new_pipelines_table, @project, default_enabled: :yaml)
push_frontend_feature_flag(:confidential_notes, @project, default_enabled: :yaml) push_frontend_feature_flag(:confidential_notes, @project, default_enabled: :yaml)
push_frontend_feature_flag(:usage_data_i_testing_summary_widget_total, @project, default_enabled: :yaml) push_frontend_feature_flag(:usage_data_i_testing_summary_widget_total, @project, default_enabled: :yaml)
push_frontend_feature_flag(:improved_emoji_picker, project, default_enabled: :yaml)
record_experiment_user(:invite_members_version_b) record_experiment_user(:invite_members_version_b)
......
...@@ -186,6 +186,12 @@ module IssuesHelper ...@@ -186,6 +186,12 @@ module IssuesHelper
def scoped_labels_available?(parent) def scoped_labels_available?(parent)
false false
end end
def award_emoji_issue_api_path(issue)
if Feature.enabled?(:improved_emoji_picker, issue.project, default_enabled: :yaml)
api_v4_projects_issues_award_emoji_path(id: issue.project.id, issue_iid: issue.iid)
end
end
end end
IssuesHelper.prepend_if_ee('EE::IssuesHelper') IssuesHelper.prepend_if_ee('EE::IssuesHelper')
...@@ -206,6 +206,12 @@ module MergeRequestsHelper ...@@ -206,6 +206,12 @@ module MergeRequestsHelper
} }
end end
def award_emoji_merge_request_api_path(merge_request)
if Feature.enabled?(:improved_emoji_picker, merge_request.project, default_enabled: :yaml)
api_v4_projects_merge_requests_award_emoji_path(id: merge_request.project.id, merge_request_iid: merge_request.iid)
end
end
private private
def review_requested_merge_requests_count def review_requested_merge_requests_count
......
- grouped_emojis = awardable.grouped_awards(with_thumbs: inline) - api_awards_path = local_assigns.fetch(:api_awards_path, nil)
.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: toggle_award_url(awardable) } }
- awards_sort(grouped_emojis).each do |emoji, awards|
%button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button",
class: [(award_state_class(awardable, awards, current_user))],
data: { title: award_user_list(awards, current_user) } }
= emoji_icon(emoji)
%span.award-control-text.js-counter
= awards.count
- if can?(current_user, :award_emoji, awardable) - if api_awards_path
.award-menu-holder.js-award-holder .gl-display-flex.gl-flex-wrap
%button.btn.award-control.has-tooltip.js-add-award{ type: 'button', #js-vue-awards-block{ data: { path: api_awards_path, can_award_emoji: can?(current_user, :award_emoji, awardable).to_s } }
'aria-label': _('Add reaction'), = yield
data: { title: _('Add reaction') } } - else
%span{ class: "award-control-icon award-control-icon-neutral" }= sprite_icon('slight-smile') - grouped_emojis = awardable.grouped_awards(with_thumbs: inline)
%span{ class: "award-control-icon award-control-icon-positive" }= sprite_icon('smiley') .awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: toggle_award_url(awardable) } }
%span{ class: "award-control-icon award-control-icon-super-positive" }= sprite_icon('smile') - awards_sort(grouped_emojis).each do |emoji, awards|
= yield %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button",
class: [(award_state_class(awardable, awards, current_user))],
data: { title: award_user_list(awards, current_user) } }
= emoji_icon(emoji)
%span.award-control-text.js-counter
= awards.count
- if can?(current_user, :award_emoji, awardable)
.award-menu-holder.js-award-holder
%button.btn.award-control.has-tooltip.js-add-award{ type: 'button',
'aria-label': _('Add reaction'),
data: { title: _('Add reaction') } }
%span{ class: "award-control-icon award-control-icon-neutral" }= sprite_icon('slight-smile')
%span{ class: "award-control-icon award-control-icon-positive" }= sprite_icon('smiley')
%span{ class: "award-control-icon award-control-icon-super-positive" }= sprite_icon('smile')
= yield
- api_awards_path = local_assigns.fetch(:api_awards_path, nil)
- page_description issuable.description_html - page_description issuable.description_html
- page_card_attributes issuable.card_attributes - page_card_attributes issuable.card_attributes
- if issuable.relocation_target - if issuable.relocation_target
...@@ -6,4 +7,4 @@ ...@@ -6,4 +7,4 @@
= render "projects/issues/alert_moved_from_service_desk", issue: issuable = render "projects/issues/alert_moved_from_service_desk", issue: issuable
= render 'shared/issue_type/details_header', issuable: issuable = render 'shared/issue_type/details_header', issuable: issuable
= render 'shared/issue_type/details_content', issuable: issuable = render 'shared/issue_type/details_content', issuable: issuable, api_awards_path: api_awards_path
...@@ -3,5 +3,5 @@ ...@@ -3,5 +3,5 @@
- breadcrumb_title @issue.to_reference - breadcrumb_title @issue.to_reference
- page_title "#{@issue.title} (#{@issue.to_reference})", _("Issues") - page_title "#{@issue.title} (#{@issue.to_reference})", _("Issues")
= render 'projects/issuable/show', issuable: @issue = render 'projects/issuable/show', issuable: @issue, api_awards_path: award_emoji_issue_api_path(@issue)
= render 'shared/issuable/invite_members_trigger', project: @project = render 'shared/issuable/invite_members_trigger', project: @project
.content-block.content-block-small.emoji-list-container.js-noteable-awards .content-block.content-block-small.emoji-list-container.js-noteable-awards
= render 'award_emoji/awards_block', awardable: @merge_request, inline: true do = render 'award_emoji/awards_block', awardable: @merge_request, inline: true, api_awards_path: award_emoji_merge_request_api_path(@merge_request) do
.ml-auto.mt-auto.mb-auto .ml-auto.mt-auto.mb-auto
#js-vue-sort-issue-discussions #js-vue-sort-issue-discussions
= render "projects/merge_requests/discussion_filter" = render "projects/merge_requests/discussion_filter"
- related_branches_path = related_branches_project_issue_path(@project, issuable) - related_branches_path = related_branches_project_issue_path(@project, issuable)
- api_awards_path = local_assigns.fetch(:api_awards_path, nil)
.issue-details.issuable-details .issue-details.issuable-details
.detail-page-description.content-block .detail-page-description.content-block
...@@ -24,7 +25,7 @@ ...@@ -24,7 +25,7 @@
#related-branches{ data: { url: related_branches_path } } #related-branches{ data: { url: related_branches_path } }
-# This element is filled in using JavaScript. -# This element is filled in using JavaScript.
= render 'shared/issue_type/emoji_block', issuable: issuable = render 'shared/issue_type/emoji_block', issuable: issuable, api_awards_path: api_awards_path
= render 'projects/issues/discussion' = render 'projects/issues/discussion'
......
- api_awards_path = local_assigns.fetch(:api_awards_path, nil)
.content-block.emoji-block.emoji-block-sticky .content-block.emoji-block.emoji-block-sticky
.row.gl-m-0.gl-justify-content-space-between .row.gl-m-0.gl-justify-content-space-between
.js-noteable-awards .js-noteable-awards
= render 'award_emoji/awards_block', awardable: issuable, inline: true = render 'award_emoji/awards_block', awardable: issuable, inline: true, api_awards_path: api_awards_path
.new-branch-col .new-branch-col
= render_if_exists "projects/issues/timeline_toggle", issuable: issuable = render_if_exists "projects/issues/timeline_toggle", issuable: issuable
#js-vue-sort-issue-discussions #js-vue-sort-issue-discussions
......
...@@ -4624,6 +4624,12 @@ msgstr "" ...@@ -4624,6 +4624,12 @@ msgstr ""
msgid "Average per day: %{average}" msgid "Average per day: %{average}"
msgstr "" msgstr ""
msgid "Award added"
msgstr ""
msgid "Award removed"
msgstr ""
msgid "AwardEmoji|No emojis found." msgid "AwardEmoji|No emojis found."
msgstr "" msgstr ""
......
...@@ -5,6 +5,10 @@ require 'spec_helper' ...@@ -5,6 +5,10 @@ require 'spec_helper'
RSpec.describe 'User interacts with awards' do RSpec.describe 'User interacts with awards' do
let(:user) { create(:user) } let(:user) { create(:user) }
before do
stub_feature_flags(improved_emoji_picker: false)
end
describe 'User interacts with awards in an issue', :js do describe 'User interacts with awards in an issue', :js do
let(:issue) { create(:issue, project: project)} let(:issue) { create(:issue, project: project)}
let(:project) { create(:project) } let(:project) { create(:project) }
......
...@@ -17,33 +17,28 @@ RSpec.describe 'Merge request > User awards emoji', :js do ...@@ -17,33 +17,28 @@ RSpec.describe 'Merge request > User awards emoji', :js do
end end
it 'adds award to merge request' do it 'adds award to merge request' do
first('.js-emoji-btn').click first('[data-testid="award-button"]').click
expect(page).to have_selector('.js-emoji-btn.active') expect(page).to have_selector('[data-testid="award-button"].is-active')
expect(first('.js-emoji-btn')).to have_content '1' expect(first('[data-testid="award-button"]')).to have_content '1'
visit project_merge_request_path(project, merge_request) visit project_merge_request_path(project, merge_request)
expect(first('.js-emoji-btn')).to have_content '1' expect(first('[data-testid="award-button"]')).to have_content '1'
end end
it 'removes award from merge request' do it 'removes award from merge request' do
first('.js-emoji-btn').click first('[data-testid="award-button"]').click
find('.js-emoji-btn.active').click find('[data-testid="award-button"].is-active').click
expect(first('.js-emoji-btn')).to have_content '0' expect(first('[data-testid="award-button"]')).to have_content '0'
visit project_merge_request_path(project, merge_request) visit project_merge_request_path(project, merge_request)
expect(first('.js-emoji-btn')).to have_content '0' expect(first('[data-testid="award-button"]')).to have_content '0'
end
it 'has only one menu on the page' do
first('.js-add-award').click
expect(page).to have_selector('.emoji-menu')
expect(page).to have_selector('.emoji-menu', count: 1)
end end
it 'adds awards to note' do it 'adds awards to note' do
first('.js-note-emoji').click page.within('.note-actions') do
first('.emoji-menu .js-emoji-btn').click first('.note-emoji-button').click
find('gl-emoji[data-name="8ball"]').click
end
wait_for_requests wait_for_requests
......
import * as Sentry from '@sentry/browser';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/emoji/awards_app/store/actions';
import axios from '~/lib/utils/axios_utils';
jest.mock('@sentry/browser');
describe('Awards app actions', () => {
describe('setInitialData', () => {
it('commits SET_INITIAL_DATA', async () => {
await testAction(
actions.setInitialData,
{ path: 'https://gitlab.com' },
{},
[{ type: 'SET_INITIAL_DATA', payload: { path: 'https://gitlab.com' } }],
[],
);
});
});
describe('fetchAwards', () => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('success', () => {
beforeEach(() => {
mock
.onGet('/awards', { params: { per_page: 100, page: '1' } })
.reply(200, ['thumbsup'], { 'x-next-page': '2' });
mock.onGet('/awards', { params: { per_page: 100, page: '2' } }).reply(200, ['thumbsdown']);
});
it('commits FETCH_AWARDS_SUCCESS', async () => {
await testAction(
actions.fetchAwards,
'1',
{ path: '/awards' },
[{ type: 'FETCH_AWARDS_SUCCESS', payload: ['thumbsup'] }],
[{ type: 'fetchAwards', payload: '2' }],
);
});
});
describe('error', () => {
beforeEach(() => {
mock.onGet('/awards').reply(500);
});
it('calls Sentry.captureException', async () => {
await testAction(actions.fetchAwards, null, { path: '/awards' }, [], [], () => {
expect(Sentry.captureException).toHaveBeenCalled();
});
});
});
});
describe('toggleAward', () => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('adding new award', () => {
describe('success', () => {
beforeEach(() => {
mock.onPost('/awards').reply(200, { id: 1 });
});
it('commits ADD_NEW_AWARD', async () => {
testAction(actions.toggleAward, null, { path: '/awards', awards: [] }, [
{ type: 'ADD_NEW_AWARD', payload: { id: 1 } },
]);
});
});
describe('error', () => {
beforeEach(() => {
mock.onPost('/awards').reply(500);
});
it('calls Sentry.captureException', async () => {
await testAction(
actions.toggleAward,
null,
{ path: '/awards', awards: [] },
[],
[],
() => {
expect(Sentry.captureException).toHaveBeenCalled();
},
);
});
});
});
describe('removing a award', () => {
const mockData = { id: 1, name: 'thumbsup', user: { id: 1 } };
describe('success', () => {
beforeEach(() => {
mock.onDelete('/awards/1').reply(200);
});
it('commits REMOVE_AWARD', async () => {
testAction(
actions.toggleAward,
'thumbsup',
{
path: '/awards',
currentUserId: 1,
awards: [mockData],
},
[{ type: 'REMOVE_AWARD', payload: 1 }],
);
});
});
describe('error', () => {
beforeEach(() => {
mock.onDelete('/awards/1').reply(500);
});
it('calls Sentry.captureException', async () => {
await testAction(
actions.toggleAward,
'thumbsup',
{
path: '/awards',
currentUserId: 1,
awards: [mockData],
},
[],
[],
() => {
expect(Sentry.captureException).toHaveBeenCalled();
},
);
});
});
});
});
});
import {
SET_INITIAL_DATA,
FETCH_AWARDS_SUCCESS,
ADD_NEW_AWARD,
REMOVE_AWARD,
} from '~/emoji/awards_app/store/mutation_types';
import mutations from '~/emoji/awards_app/store/mutations';
describe('Awards app mutations', () => {
describe('SET_INITIAL_DATA', () => {
it('sets initial data', () => {
const state = {};
mutations[SET_INITIAL_DATA](state, {
path: 'https://gitlab.com',
currentUserId: 1,
canAwardEmoji: true,
});
expect(state).toEqual({
path: 'https://gitlab.com',
currentUserId: 1,
canAwardEmoji: true,
});
});
});
describe('FETCH_AWARDS_SUCCESS', () => {
it('sets awards', () => {
const state = { awards: [] };
mutations[FETCH_AWARDS_SUCCESS](state, ['thumbsup']);
expect(state.awards).toEqual(['thumbsup']);
});
it('does not overwrite previously set awards', () => {
const state = { awards: ['thumbsup'] };
mutations[FETCH_AWARDS_SUCCESS](state, ['thumbsdown']);
expect(state.awards).toEqual(['thumbsup', 'thumbsdown']);
});
});
describe('ADD_NEW_AWARD', () => {
it('adds new award to array', () => {
const state = { awards: ['thumbsup'] };
mutations[ADD_NEW_AWARD](state, 'thumbsdown');
expect(state.awards).toEqual(['thumbsup', 'thumbsdown']);
});
});
describe('REMOVE_AWARD', () => {
it('removes award from array', () => {
const state = { awards: [{ id: 1 }, { id: 2 }] };
mutations[REMOVE_AWARD](state, 1);
expect(state.awards).toEqual([{ id: 2 }]);
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment