Commit 5d95f4d6 authored by Nathan Friend's avatar Nathan Friend Committed by Illya Klymov

Add Vue ref selector component

Adds a Vue component that allows branches, tags, or commits to be
searched and selected.
parent 1ef0df07
<script>
import { GlNewDropdownHeader, GlNewDropdownItem, GlBadge, GlIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
name: 'RefResultsSection',
components: {
GlNewDropdownHeader,
GlNewDropdownItem,
GlBadge,
GlIcon,
},
props: {
sectionTitle: {
type: String,
required: true,
},
totalCount: {
type: Number,
required: true,
},
/**
* An array of object that have the following properties:
*
* - name (String, required): The name of the ref that will be displayed
* - value (String, optional): The value that will be selected when the ref
* is selected. If not provided, `name` will be used as the value.
* For example, commits use the short SHA for `name`
* and long SHA for `value`.
* - subtitle (String, optional): Text to render underneath the name.
* For example, used to render the commit's title underneath its SHA.
* - default (Boolean, optional): Whether or not to render a "default"
* indicator next to the item. Used to indicate
* the project's default branch.
*
*/
items: {
type: Array,
required: true,
validator: items => Array.isArray(items) && items.every(item => item.name),
},
/**
* The currently selected ref.
* Used to render a check mark by the selected item.
* */
selectedRef: {
type: String,
required: false,
default: '',
},
/**
* An error object that indicates that an error
* occurred while fetching items for this section
*/
error: {
type: Error,
required: false,
default: null,
},
/** The message to display if an error occurs */
errorMessage: {
type: String,
required: false,
default: '',
},
},
computed: {
totalCountText() {
return this.totalCount > 999 ? s__('TotalRefCountIndicator|1000+') : `${this.totalCount}`;
},
},
methods: {
showCheck(item) {
return item.name === this.selectedRef || item.value === this.selectedRef;
},
},
};
</script>
<template>
<div>
<gl-new-dropdown-header>
<div class="gl-display-flex align-items-center" data-testid="section-header">
<span class="gl-mr-2 gl-mb-1">{{ sectionTitle }}</span>
<gl-badge variant="neutral">{{ totalCountText }}</gl-badge>
</div>
</gl-new-dropdown-header>
<template v-if="error">
<div class="gl-display-flex align-items-start text-danger gl-ml-4 gl-mr-4 gl-mb-3">
<gl-icon name="error" class="gl-mr-2 gl-mt-2 gl-flex-shrink-0" />
<span>{{ errorMessage }}</span>
</div>
</template>
<template v-else>
<gl-new-dropdown-item
v-for="item in items"
:key="item.name"
@click="$emit('selected', item.value || item.name)"
>
<div class="gl-display-flex align-items-start">
<gl-icon
name="mobile-issue-close"
class="gl-mr-2 gl-flex-shrink-0"
:class="{ 'gl-visibility-hidden': !showCheck(item) }"
/>
<div class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column">
<span class="gl-font-monospace">{{ item.name }}</span>
<span class="gl-text-gray-600">{{ item.subtitle }}</span>
</div>
<gl-badge v-if="item.default" size="sm" variant="info">{{
s__('DefaultBranchLabel|default')
}}</gl-badge>
</div>
</gl-new-dropdown-item>
</template>
</div>
</template>
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import {
GlNewDropdown,
GlNewDropdownDivider,
GlNewDropdownHeader,
GlSearchBoxByType,
GlSprintf,
GlIcon,
GlLoadingIcon,
} from '@gitlab/ui';
import { debounce } from 'lodash';
import createStore from '../stores';
import { SEARCH_DEBOUNCE_MS, DEFAULT_I18N } from '../constants';
import RefResultsSection from './ref_results_section.vue';
export default {
name: 'RefSelector',
store: createStore(),
components: {
GlNewDropdown,
GlNewDropdownDivider,
GlNewDropdownHeader,
GlSearchBoxByType,
GlSprintf,
GlIcon,
GlLoadingIcon,
RefResultsSection,
},
props: {
value: {
type: String,
required: false,
default: '',
},
projectId: {
type: String,
required: true,
},
translations: {
type: Object,
required: false,
default: () => ({}),
},
},
data() {
return {
query: '',
};
},
computed: {
...mapState({
matches: state => state.matches,
lastQuery: state => state.query,
selectedRef: state => state.selectedRef,
}),
...mapGetters(['isLoading', 'isQueryPossiblyASha']),
i18n() {
return {
...DEFAULT_I18N,
...this.translations,
};
},
showBranchesSection() {
return Boolean(this.matches.branches.totalCount > 0 || this.matches.branches.error);
},
showTagsSection() {
return Boolean(this.matches.tags.totalCount > 0 || this.matches.tags.error);
},
showCommitsSection() {
return Boolean(this.matches.commits.totalCount > 0 || this.matches.commits.error);
},
showNoResults() {
return !this.showBranchesSection && !this.showTagsSection && !this.showCommitsSection;
},
},
created() {
this.setProjectId(this.projectId);
this.search(this.query);
},
methods: {
...mapActions(['setProjectId', 'setSelectedRef', 'search']),
focusSearchBox() {
this.$refs.searchBox.$el.querySelector('input').focus();
},
onSearchBoxInput: debounce(function search() {
this.search(this.query);
}, SEARCH_DEBOUNCE_MS),
selectRef(ref) {
this.setSelectedRef(ref);
this.$emit('input', this.selectedRef);
},
},
};
</script>
<template>
<gl-new-dropdown class="ref-selector" @shown="focusSearchBox">
<template slot="button-content">
<span class="gl-flex-grow-1 gl-ml-2 gl-text-gray-600" data-testid="button-content">
<span v-if="selectedRef" class="gl-font-monospace">{{ selectedRef }}</span>
<span v-else>{{ i18n.noRefSelected }}</span>
</span>
<gl-icon name="chevron-down" />
</template>
<div class="gl-display-flex gl-flex-direction-column ref-selector-dropdown-content">
<gl-new-dropdown-header>
<span class="gl-text-center gl-display-block">{{ i18n.dropdownHeader }}</span>
</gl-new-dropdown-header>
<gl-new-dropdown-divider />
<gl-search-box-by-type
ref="searchBox"
v-model.trim="query"
class="gl-m-3"
:placeholder="i18n.searchPlaceholder"
@input="onSearchBoxInput"
/>
<div class="gl-flex-grow-1 gl-overflow-y-auto">
<gl-loading-icon v-if="isLoading" size="lg" class="gl-my-3" />
<div
v-else-if="showNoResults"
class="gl-text-center gl-mx-3 gl-py-3"
data-testid="no-results"
>
<gl-sprintf v-if="lastQuery" :message="i18n.noResultsWithQuery">
<template #query>
<b class="gl-word-break-all">{{ lastQuery }}</b>
</template>
</gl-sprintf>
<span v-else>{{ i18n.noResults }}</span>
</div>
<template v-else>
<template v-if="showBranchesSection">
<ref-results-section
:section-title="i18n.branches"
:total-count="matches.branches.totalCount"
:items="matches.branches.list"
:selected-ref="selectedRef"
:error="matches.branches.error"
:error-message="i18n.branchesErrorMessage"
data-testid="branches-section"
@selected="selectRef($event)"
/>
<gl-new-dropdown-divider v-if="showTagsSection || showCommitsSection" />
</template>
<template v-if="showTagsSection">
<ref-results-section
:section-title="i18n.tags"
:total-count="matches.tags.totalCount"
:items="matches.tags.list"
:selected-ref="selectedRef"
:error="matches.tags.error"
:error-message="i18n.tagsErrorMessage"
data-testid="tags-section"
@selected="selectRef($event)"
/>
<gl-new-dropdown-divider v-if="showCommitsSection" />
</template>
<template v-if="showCommitsSection">
<ref-results-section
:section-title="i18n.commits"
:total-count="matches.commits.totalCount"
:items="matches.commits.list"
:selected-ref="selectedRef"
:error="matches.commits.error"
:error-message="i18n.commitsErrorMessage"
data-testid="commits-section"
@selected="selectRef($event)"
/>
</template>
</template>
</div>
</div>
</gl-new-dropdown>
</template>
// This eslint-disable can be removed once a second
// value is added to this file.
/* eslint-disable import/prefer-default-export */
import { __ } from '~/locale';
export const X_TOTAL_HEADER = 'x-total';
export const SEARCH_DEBOUNCE_MS = 250;
export const DEFAULT_I18N = Object.freeze({
dropdownHeader: __('Select Git revision'),
searchPlaceholder: __('Search by Git revision'),
noResultsWithQuery: __('No matching results for "%{query}"'),
noResults: __('No matching results'),
branchesErrorMessage: __('An error occurred while fetching branches. Retry the search.'),
tagsErrorMessage: __('An error occurred while fetching tags. Retry the search.'),
commitsErrorMessage: __('An error occurred while fetching commits. Retry the search.'),
branches: __('Branches'),
tags: __('Tags'),
commits: __('Commits'),
noRefSelected: __('No ref selected'),
});
.ref-selector {
& &-dropdown-content {
// Setting a max height is necessary to allow the dropdown's content
// to control where and how scrollbars appear.
// This content is limited to the max-height of the dropdown
// ($dropdown-max-height-lg) minus the additional padding
// on the top and bottom (2 * $gl-padding-8)
max-height: $dropdown-max-height-lg - 2 * $gl-padding-8;
}
.dropdown-menu.show {
// Make the dropdown a little wider and longer than usual
// since it contains quite a bit of content.
width: 20rem;
max-height: $dropdown-max-height-lg;
}
}
......@@ -2475,6 +2475,12 @@ msgstr ""
msgid "An error occurred while enabling Service Desk."
msgstr ""
msgid "An error occurred while fetching branches. Retry the search."
msgstr ""
msgid "An error occurred while fetching commits. Retry the search."
msgstr ""
msgid "An error occurred while fetching coverage reports."
msgstr ""
......@@ -2505,6 +2511,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
msgid "An error occurred while fetching tags. Retry the search."
msgstr ""
msgid "An error occurred while fetching terraform reports."
msgstr ""
......@@ -7441,6 +7450,9 @@ msgstr ""
msgid "Default: Map a FogBugz account ID to a full name"
msgstr ""
msgid "DefaultBranchLabel|default"
msgstr ""
msgid "Define a custom pattern with cron syntax"
msgstr ""
......@@ -15598,6 +15610,9 @@ msgstr ""
msgid "No matching results"
msgstr ""
msgid "No matching results for \"%{query}\""
msgstr ""
msgid "No merge requests found"
msgstr ""
......@@ -15634,6 +15649,9 @@ msgstr ""
msgid "No public groups"
msgstr ""
msgid "No ref selected"
msgstr ""
msgid "No related merge requests found."
msgstr ""
......@@ -20253,6 +20271,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
msgid "Search by Git revision"
msgstr ""
msgid "Search by author"
msgstr ""
......@@ -24632,6 +24653,9 @@ msgstr ""
msgid "Total: %{total}"
msgstr ""
msgid "TotalRefCountIndicator|1000+"
msgstr ""
msgid "Trace"
msgstr ""
......
import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { GlLoadingIcon, GlSearchBoxByType, GlNewDropdownItem, GlIcon } from '@gitlab/ui';
import { trimText } from 'helpers/text_helper';
import { sprintf } from '~/locale';
import RefSelector from '~/ref/components/ref_selector.vue';
import { X_TOTAL_HEADER, DEFAULT_I18N } from '~/ref/constants';
import createStore from '~/ref/stores/';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Ref selector component', () => {
const fixtures = {
branches: getJSONFixture('api/branches/branches.json'),
tags: getJSONFixture('api/tags/tags.json'),
commit: getJSONFixture('api/commits/commit.json'),
};
const projectId = '8';
let wrapper;
let branchesApiCallSpy;
let tagsApiCallSpy;
let commitApiCallSpy;
const createComponent = () => {
wrapper = mount(RefSelector, {
propsData: {
projectId,
value: '',
},
listeners: {
// simulate a parent component v-model binding
input: selectedRef => {
wrapper.setProps({ value: selectedRef });
},
},
stubs: {
GlSearchBoxByType: true,
},
localVue,
store: createStore(),
});
};
beforeEach(() => {
const mock = new MockAdapter(axios);
gon.api_version = 'v4';
branchesApiCallSpy = jest
.fn()
.mockReturnValue([200, fixtures.branches, { [X_TOTAL_HEADER]: '123' }]);
tagsApiCallSpy = jest.fn().mockReturnValue([200, fixtures.tags, { [X_TOTAL_HEADER]: '456' }]);
commitApiCallSpy = jest.fn().mockReturnValue([200, fixtures.commit]);
mock
.onGet(`/api/v4/projects/${projectId}/repository/branches`)
.reply(config => branchesApiCallSpy(config));
mock
.onGet(`/api/v4/projects/${projectId}/repository/tags`)
.reply(config => tagsApiCallSpy(config));
mock
.onGet(new RegExp(`/api/v4/projects/${projectId}/repository/commits/.*`))
.reply(config => commitApiCallSpy(config));
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
//
// Finders
//
const findButtonContent = () => wrapper.find('[data-testid="button-content"]');
const findNoResults = () => wrapper.find('[data-testid="no-results"]');
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findBranchesSection = () => wrapper.find('[data-testid="branches-section"]');
const findBranchDropdownItems = () => findBranchesSection().findAll(GlNewDropdownItem);
const findFirstBranchDropdownItem = () => findBranchDropdownItems().at(0);
const findTagsSection = () => wrapper.find('[data-testid="tags-section"]');
const findTagDropdownItems = () => findTagsSection().findAll(GlNewDropdownItem);
const findFirstTagDropdownItem = () => findTagDropdownItems().at(0);
const findCommitsSection = () => wrapper.find('[data-testid="commits-section"]');
const findCommitDropdownItems = () => findCommitsSection().findAll(GlNewDropdownItem);
const findFirstCommitDropdownItem = () => findCommitDropdownItems().at(0);
//
// Expecters
//
const branchesSectionContainsErrorMessage = () => {
const branchesSection = findBranchesSection();
return branchesSection.text().includes(DEFAULT_I18N.branchesErrorMessage);
};
const tagsSectionContainsErrorMessage = () => {
const tagsSection = findTagsSection();
return tagsSection.text().includes(DEFAULT_I18N.tagsErrorMessage);
};
const commitsSectionContainsErrorMessage = () => {
const commitsSection = findCommitsSection();
return commitsSection.text().includes(DEFAULT_I18N.commitsErrorMessage);
};
//
// Convenience methods
//
const updateQuery = newQuery => {
wrapper.find(GlSearchBoxByType).vm.$emit('input', newQuery);
};
const selectFirstBranch = () => {
findFirstBranchDropdownItem().vm.$emit('click');
};
const selectFirstTag = () => {
findFirstTagDropdownItem().vm.$emit('click');
};
const selectFirstCommit = () => {
findFirstCommitDropdownItem().vm.$emit('click');
};
const waitForRequests = ({ andClearMocks } = { andClearMocks: false }) =>
axios.waitForAll().then(() => {
if (andClearMocks) {
branchesApiCallSpy.mockClear();
tagsApiCallSpy.mockClear();
commitApiCallSpy.mockClear();
}
});
describe('initialization behavior', () => {
beforeEach(createComponent);
it('initializes the dropdown with branches and tags when mounted', () => {
return waitForRequests().then(() => {
expect(branchesApiCallSpy).toHaveBeenCalledTimes(1);
expect(tagsApiCallSpy).toHaveBeenCalledTimes(1);
expect(commitApiCallSpy).not.toHaveBeenCalled();
});
});
it('shows a spinner while network requests are in progress', () => {
expect(findLoadingIcon().exists()).toBe(true);
return waitForRequests().then(() => {
expect(findLoadingIcon().exists()).toBe(false);
});
});
});
describe('post-initialization behavior', () => {
describe('when the search query is updated', () => {
beforeEach(() => {
createComponent();
return waitForRequests({ andClearMocks: true });
});
it('requeries the endpoints when the search query is updated', () => {
updateQuery('v1.2.3');
return waitForRequests().then(() => {
expect(branchesApiCallSpy).toHaveBeenCalledTimes(1);
expect(tagsApiCallSpy).toHaveBeenCalledTimes(1);
});
});
it("does not make a call to the commit endpoint if the query doesn't look like a SHA", () => {
updateQuery('not a sha');
return waitForRequests().then(() => {
expect(commitApiCallSpy).not.toHaveBeenCalled();
});
});
it('searches for a commit if the query could potentially be a SHA', () => {
updateQuery('abcdef');
return waitForRequests().then(() => {
expect(commitApiCallSpy).toHaveBeenCalled();
});
});
});
describe('when no results are found', () => {
beforeEach(() => {
branchesApiCallSpy = jest.fn().mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
tagsApiCallSpy = jest.fn().mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
commitApiCallSpy = jest.fn().mockReturnValue([404]);
createComponent();
return waitForRequests();
});
describe('when the search query is empty', () => {
it('renders a "no results" message', () => {
expect(findNoResults().text()).toBe(DEFAULT_I18N.noResults);
});
});
describe('when the search query is not empty', () => {
const query = 'hello';
beforeEach(() => {
updateQuery(query);
return waitForRequests();
});
it('renders a "no results" message that includes the search query', () => {
expect(findNoResults().text()).toBe(sprintf(DEFAULT_I18N.noResultsWithQuery, { query }));
});
});
});
describe('branches', () => {
describe('when the branches search returns results', () => {
beforeEach(() => {
createComponent();
return waitForRequests();
});
it('renders the branches section in the dropdown', () => {
expect(findBranchesSection().exists()).toBe(true);
});
it('renders the "Branches" heading with a total number indicator', () => {
expect(
findBranchesSection()
.find('[data-testid="section-header"]')
.text(),
).toBe('Branches 123');
});
it("does not render an error message in the branches section's body", () => {
expect(branchesSectionContainsErrorMessage()).toBe(false);
});
it('renders each non-default branch as a selectable item', () => {
const dropdownItems = findBranchDropdownItems();
fixtures.branches.forEach((b, i) => {
if (!b.default) {
expect(dropdownItems.at(i).text()).toBe(b.name);
}
});
});
it('renders the default branch as a selectable item with a "default" badge', () => {
const dropdownItems = findBranchDropdownItems();
const defaultBranch = fixtures.branches.find(b => b.default);
const defaultBranchIndex = fixtures.branches.indexOf(defaultBranch);
expect(trimText(dropdownItems.at(defaultBranchIndex).text())).toBe(
`${defaultBranch.name} default`,
);
});
});
describe('when the branches search returns no results', () => {
beforeEach(() => {
branchesApiCallSpy = jest.fn().mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
createComponent();
return waitForRequests();
});
it('does not render the branches section in the dropdown', () => {
expect(findBranchesSection().exists()).toBe(false);
});
});
describe('when the branches search returns an error', () => {
beforeEach(() => {
branchesApiCallSpy = jest.fn().mockReturnValue([500]);
createComponent();
return waitForRequests();
});
it('renders the branches section in the dropdown', () => {
expect(findBranchesSection().exists()).toBe(true);
});
it("renders an error message in the branches section's body", () => {
expect(branchesSectionContainsErrorMessage()).toBe(true);
});
});
});
describe('tags', () => {
describe('when the tags search returns results', () => {
beforeEach(() => {
createComponent();
return waitForRequests();
});
it('renders the tags section in the dropdown', () => {
expect(findTagsSection().exists()).toBe(true);
});
it('renders the "Tags" heading with a total number indicator', () => {
expect(
findTagsSection()
.find('[data-testid="section-header"]')
.text(),
).toBe('Tags 456');
});
it("does not render an error message in the tags section's body", () => {
expect(tagsSectionContainsErrorMessage()).toBe(false);
});
it('renders each tag as a selectable item', () => {
const dropdownItems = findTagDropdownItems();
fixtures.tags.forEach((t, i) => {
expect(dropdownItems.at(i).text()).toBe(t.name);
});
});
});
describe('when the tags search returns no results', () => {
beforeEach(() => {
tagsApiCallSpy = jest.fn().mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
createComponent();
return waitForRequests();
});
it('does not render the tags section in the dropdown', () => {
expect(findTagsSection().exists()).toBe(false);
});
});
describe('when the tags search returns an error', () => {
beforeEach(() => {
tagsApiCallSpy = jest.fn().mockReturnValue([500]);
createComponent();
return waitForRequests();
});
it('renders the tags section in the dropdown', () => {
expect(findTagsSection().exists()).toBe(true);
});
it("renders an error message in the tags section's body", () => {
expect(tagsSectionContainsErrorMessage()).toBe(true);
});
});
});
describe('commits', () => {
describe('when the commit search returns results', () => {
beforeEach(() => {
createComponent();
updateQuery('abcd1234');
return waitForRequests();
});
it('renders the commit section in the dropdown', () => {
expect(findCommitsSection().exists()).toBe(true);
});
it('renders the "Commits" heading with a total number indicator', () => {
expect(
findCommitsSection()
.find('[data-testid="section-header"]')
.text(),
).toBe('Commits 1');
});
it("does not render an error message in the comits section's body", () => {
expect(commitsSectionContainsErrorMessage()).toBe(false);
});
it('renders each commit as a selectable item with the short SHA and commit title', () => {
const dropdownItems = findCommitDropdownItems();
const { commit } = fixtures;
expect(dropdownItems.at(0).text()).toBe(`${commit.short_id} ${commit.title}`);
});
});
describe('when the commit search returns no results (i.e. a 404)', () => {
beforeEach(() => {
commitApiCallSpy = jest.fn().mockReturnValue([404]);
createComponent();
updateQuery('abcd1234');
return waitForRequests();
});
it('does not render the commits section in the dropdown', () => {
expect(findCommitsSection().exists()).toBe(false);
});
});
describe('when the commit search returns an error (other than a 404)', () => {
beforeEach(() => {
commitApiCallSpy = jest.fn().mockReturnValue([500]);
createComponent();
updateQuery('abcd1234');
return waitForRequests();
});
it('renders the commits section in the dropdown', () => {
expect(findCommitsSection().exists()).toBe(true);
});
it("renders an error message in the commits section's body", () => {
expect(commitsSectionContainsErrorMessage()).toBe(true);
});
});
});
describe('selection', () => {
beforeEach(() => {
createComponent();
updateQuery(fixtures.commit.short_id);
return waitForRequests();
});
it('renders a checkmark by the selected item', () => {
expect(findFirstBranchDropdownItem().find(GlIcon).element).toHaveClass(
'gl-visibility-hidden',
);
selectFirstBranch();
return localVue.nextTick().then(() => {
expect(findFirstBranchDropdownItem().find(GlIcon).element).not.toHaveClass(
'gl-visibility-hidden',
);
});
});
describe('when a branch is seleceted', () => {
it("displays the branch name in the dropdown's button", () => {
expect(findButtonContent().text()).toBe(DEFAULT_I18N.noRefSelected);
selectFirstBranch();
return localVue.nextTick().then(() => {
expect(findButtonContent().text()).toBe(fixtures.branches[0].name);
});
});
it("updates the v-model binding with the branch's name", () => {
expect(wrapper.vm.value).toEqual('');
selectFirstBranch();
expect(wrapper.vm.value).toEqual(fixtures.branches[0].name);
});
});
describe('when a tag is seleceted', () => {
it("displays the tag name in the dropdown's button", () => {
expect(findButtonContent().text()).toBe(DEFAULT_I18N.noRefSelected);
selectFirstTag();
return localVue.nextTick().then(() => {
expect(findButtonContent().text()).toBe(fixtures.tags[0].name);
});
});
it("updates the v-model binding with the tag's name", () => {
expect(wrapper.vm.value).toEqual('');
selectFirstTag();
expect(wrapper.vm.value).toEqual(fixtures.tags[0].name);
});
});
describe('when a commit is selected', () => {
it("displays the full SHA in the dropdown's button", () => {
expect(findButtonContent().text()).toBe(DEFAULT_I18N.noRefSelected);
selectFirstCommit();
return localVue.nextTick().then(() => {
expect(findButtonContent().text()).toBe(fixtures.commit.id);
});
});
it("updates the v-model binding with the commit's full SHA", () => {
expect(wrapper.vm.value).toEqual('');
selectFirstCommit();
expect(wrapper.vm.value).toEqual(fixtures.commit.id);
});
});
});
});
});
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