diff --git a/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue b/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue index 98193de4a12f7adc81ad16c98dd491031aa5ae3a..fc37e4139613ebff77fc976def86b081c0fa5ec6 100644 --- a/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue +++ b/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue @@ -1,14 +1,5 @@ <script> -import { - GlIcon, - GlLoadingIcon, - GlAvatar, - GlDropdown, - GlDropdownSectionHeader, - GlDropdownItem, - GlSearchBoxByType, - GlTruncate, -} from '@gitlab/ui'; +import { GlButton, GlIcon, GlAvatar, GlCollapsibleListbox, GlTruncate } from '@gitlab/ui'; import { debounce } from 'lodash'; import { filterBySearchTerm } from '~/analytics/shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; @@ -18,17 +9,15 @@ import { n__, s__, __ } from '~/locale'; import getProjects from '../graphql/projects.query.graphql'; const sortByProjectName = (projects = []) => projects.sort((a, b) => a.name.localeCompare(b.name)); +const mapItemToListboxFormat = (item) => ({ ...item, value: item.id, text: item.name }); export default { name: 'ProjectsDropdownFilter', components: { + GlButton, GlIcon, - GlLoadingIcon, GlAvatar, - GlDropdown, - GlDropdownSectionHeader, - GlDropdownItem, - GlSearchBoxByType, + GlCollapsibleListbox, GlTruncate, }, props: { @@ -94,6 +83,9 @@ export default { selectedProjectIds() { return this.selectedProjects.map((p) => p.id); }, + selectedListBoxItems() { + return this.multiSelect ? this.selectedProjectIds : this.selectedProjectIds[0]; + }, hasSelectedProjects() { return Boolean(this.selectedProjects.length); }, @@ -110,6 +102,28 @@ export default { unselectedItems() { return this.availableProjects.filter(({ id }) => !this.selectedProjectIds.includes(id)); }, + selectedGroupOptions() { + return this.selectedItems.map(mapItemToListboxFormat); + }, + unSelectedGroupOptions() { + return this.unselectedItems.map(mapItemToListboxFormat); + }, + listBoxItems() { + if (this.selectedGroupOptions.length === 0) { + return this.unSelectedGroupOptions; + } + + return [ + { + text: __('Selected'), + options: this.selectedGroupOptions, + }, + { + text: __('Unselected'), + options: this.unSelectedGroupOptions, + }, + ]; + }, }, watch: { searchTerm() { @@ -129,32 +143,29 @@ export default { search: debounce(function debouncedSearch() { this.fetchData(); }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS), - getSelectedProjects(selectedProject, isSelected) { - return isSelected - ? this.selectedProjects.concat([selectedProject]) - : this.selectedProjects.filter((project) => project.id !== selectedProject.id); - }, singleSelectedProject(selectedObj, isMarking) { return isMarking ? [selectedObj] : []; }, - setSelectedProjects(project) { + setSelectedProjects(payload) { this.selectedProjects = this.multiSelect - ? this.getSelectedProjects(project, !this.isProjectSelected(project)) - : this.singleSelectedProject(project, !this.isProjectSelected(project)); + ? payload + : this.singleSelectedProject(payload, !this.isProjectSelected(payload)); }, - onClick(project) { + onClick(projectId) { + const project = this.availableProjects.find(({ id }) => id === projectId); this.setSelectedProjects(project); this.handleUpdatedSelectedProjects(); }, - onMultiSelectClick(project) { - this.setSelectedProjects(project); + onMultiSelectClick(projectIds) { + const projects = this.availableProjects.filter(({ id }) => projectIds.includes(id)); + this.setSelectedProjects(projects); this.isDirty = true; }, - onSelected(project) { + onSelected(payload) { if (this.multiSelect) { - this.onMultiSelectClick(project); + this.onMultiSelectClick(payload); } else { - this.onClick(project); + this.onClick(payload); } }, onHide() { @@ -201,97 +212,65 @@ export default { getEntityId(project) { return getIdFromGraphQLId(project.id); }, + setSearchTerm(val) { + this.searchTerm = val; + }, }, AVATAR_SHAPE_OPTION_RECT, }; </script> <template> - <gl-dropdown + <gl-collapsible-listbox ref="projectsDropdown" - class="dropdown dropdown-projects" toggle-class="gl-shadow-none gl-mb-0" + :header-text="__('Projects')" + :items="listBoxItems" + :reset-button-label="__('Clear All')" :loading="loadingDefaultProjects" - :show-clear-all="hasSelectedProjects" - show-highlighted-items-title - highlighted-items-title-class="gl-p-3" - block - @clear-all.stop="onClearAll" - @hide="onHide" + :multiple="multiSelect" + :no-results-text="__('No matching results')" + :selected="selectedListBoxItems" + :searching="loading" + searchable + @hidden="onHide" + @reset="onClearAll" + @search="setSearchTerm" + @select="onSelected" > - <template #button-content> - <gl-loading-icon v-if="loadingDefaultProjects" class="gl-mr-2 gl-flex-shrink-0" /> - <gl-avatar - v-if="isOnlyOneProjectSelected" - :src="selectedProjects[0].avatarUrl" - :entity-id="getEntityId(selectedProjects[0])" - :entity-name="selectedProjects[0].name" - :size="16" - :shape="$options.AVATAR_SHAPE_OPTION_RECT" - :alt="selectedProjects[0].name" - class="gl-display-inline-flex gl-vertical-align-middle gl-mr-2 gl-flex-shrink-0" - /> - <gl-truncate :text="selectedProjectsLabel" class="gl-min-w-0 gl-flex-grow-1" /> - <gl-icon class="gl-ml-2 gl-flex-shrink-0" name="chevron-down" /> - </template> - <template #header> - <gl-dropdown-section-header>{{ __('Projects') }}</gl-dropdown-section-header> - <gl-search-box-by-type v-model.trim="searchTerm" :placeholder="__('Search')" /> - </template> - <template #highlighted-items> - <gl-dropdown-item - v-for="project in selectedItems" - :key="project.id" - is-check-item - :is-checked="isProjectSelected(project)" - @click.native.capture.stop="onSelected(project)" - > - <div class="gl-display-flex"> - <gl-avatar - class="gl-mr-2 gl-vertical-align-middle" - :alt="project.name" - :size="16" - :entity-id="getEntityId(project)" - :entity-name="project.name" - :src="project.avatarUrl" - :shape="$options.AVATAR_SHAPE_OPTION_RECT" - /> - <div> - <div data-testid="project-name">{{ project.name }}</div> - <div class="gl-text-gray-500" data-testid="project-full-path"> - {{ project.fullPath }} - </div> - </div> - </div> - </gl-dropdown-item> + <template #toggle> + <gl-button class="dropdown-projects"> + <gl-avatar + v-if="isOnlyOneProjectSelected" + :src="selectedProjects[0].avatarUrl" + :entity-id="getEntityId(selectedProjects[0])" + :entity-name="selectedProjects[0].name" + :size="16" + :shape="$options.AVATAR_SHAPE_OPTION_RECT" + :alt="selectedProjects[0].name" + class="gl-display-inline-flex gl-vertical-align-middle gl-mr-2 gl-flex-shrink-0" + /> + <gl-truncate :text="selectedProjectsLabel" class="gl-min-w-0 gl-flex-grow-1" /> + <gl-icon class="gl-ml-2 gl-flex-shrink-0" name="chevron-down" /> + </gl-button> </template> - <gl-dropdown-item - v-for="project in unselectedItems" - :key="project.id" - @click.native.capture.stop="onSelected(project)" - > + <template #list-item="{ item }"> <div class="gl-display-flex"> <gl-avatar - class="gl-mr-2 vertical-align-middle" - :alt="project.name" + class="gl-mr-2 gl-vertical-align-middle" + :alt="item.name" :size="16" - :entity-id="getEntityId(project)" - :entity-name="project.name" - :src="project.avatarUrl" + :entity-id="getEntityId(item)" + :entity-name="item.name" + :src="item.avatarUrl" :shape="$options.AVATAR_SHAPE_OPTION_RECT" /> <div> - <div data-testid="project-name" data-qa-selector="project_name">{{ project.name }}</div> + <div data-testid="project-name" data-qa-selector="project_name">{{ item.name }}</div> <div class="gl-text-gray-500" data-testid="project-full-path"> - {{ project.fullPath }} + {{ item.fullPath }} </div> </div> </div> - </gl-dropdown-item> - <gl-dropdown-item v-show="noResultsAvailable" class="gl-pointer-events-none text-secondary">{{ - __('No matching results') - }}</gl-dropdown-item> - <gl-dropdown-item v-if="loading"> - <gl-loading-icon size="lg" /> - </gl-dropdown-item> - </gl-dropdown> + </template> + </gl-collapsible-listbox> </template> diff --git a/ee/spec/features/groups/security/compliance_dashboards_spec.rb b/ee/spec/features/groups/security/compliance_dashboards_spec.rb index 54a192cef535c4ac25f3c057158c2bffe5a4ae2a..a99338178054c5e4d1962ca52b5de8c75ef09c34 100644 --- a/ee/spec/features/groups/security/compliance_dashboards_spec.rb +++ b/ee/spec/features/groups/security/compliance_dashboards_spec.rb @@ -150,13 +150,13 @@ def set_date_range(start_date, end_date) def filter_by_project(project) page.within('[data-testid="violations-project-dropdown"]') do - find('.dropdown-toggle').click + find('.dropdown-projects').click find('input[aria-label="Search"]').set(project.name) wait_for_requests - find('.dropdown-item').click - find('.dropdown-toggle').click + find('.gl-new-dropdown-item[role="option"]').click + find('.dropdown-projects').click end page.find('body').click diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 4f0d8ba9d27ffceaed82ac89cc009ab1a19df909..03d6062830530287a9ab2668a5cba15bc4429b80 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -9684,6 +9684,9 @@ msgid_plural "Clear %{count} images from cache?" msgstr[0] "" msgstr[1] "" +msgid "Clear All" +msgstr "" + msgid "Clear all repository checks" msgstr "" @@ -41512,6 +41515,9 @@ msgstr "" msgid "Select type" msgstr "" +msgid "Selected" +msgstr "" + msgid "Selected commits" msgstr "" @@ -48066,6 +48072,9 @@ msgstr "" msgid "Unselect all" msgstr "" +msgid "Unselected" +msgstr "" + msgid "Unstar" msgstr "" diff --git a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js index 33801fb8552ed43ddcd7dac77db12742412f1545..364f0a2e372e256923f5fff4d01ebb21ff6dad0e 100644 --- a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js +++ b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js @@ -1,7 +1,6 @@ -import { GlDropdown, GlDropdownItem, GlTruncate, GlSearchBoxByType } from '@gitlab/ui'; +import { GlButton, GlTruncate, GlCollapsibleListbox, GlListboxItem, GlAvatar } from '@gitlab/ui'; import { nextTick } from 'vue'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; -import { stubComponent } from 'helpers/stub_component'; +import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { TEST_HOST } from 'helpers/test_constants'; import waitForPromises from 'helpers/wait_for_promises'; import ProjectsDropdownFilter from '~/analytics/shared/components/projects_dropdown_filter.vue'; @@ -28,18 +27,6 @@ const projects = [ }, ]; -const MockGlDropdown = stubComponent(GlDropdown, { - template: ` - <div> - <slot name="header"></slot> - <div data-testid="vsa-highlighted-items"> - <slot name="highlighted-items"></slot> - </div> - <div data-testid="vsa-default-items"><slot></slot></div> - </div> - `, -}); - const defaultMocks = { $apollo: { query: jest.fn().mockResolvedValue({ @@ -53,42 +40,36 @@ let spyQuery; describe('ProjectsDropdownFilter component', () => { let wrapper; - const createComponent = (props = {}, stubs = {}) => { + const createComponent = ({ mountFn = shallowMountExtended, props = {}, stubs = {} } = {}) => { spyQuery = defaultMocks.$apollo.query; - wrapper = mountExtended(ProjectsDropdownFilter, { + wrapper = mountFn(ProjectsDropdownFilter, { mocks: { ...defaultMocks }, propsData: { groupId: 1, groupNamespace: 'gitlab-org', ...props, }, - stubs, + stubs: { + GlButton, + GlCollapsibleListbox, + ...stubs, + }, }); }; - const createWithMockDropdown = (props) => { - createComponent(props, { GlDropdown: MockGlDropdown }); - return waitForPromises(); - }; - - const findHighlightedItems = () => wrapper.findByTestId('vsa-highlighted-items'); - const findUnhighlightedItems = () => wrapper.findByTestId('vsa-default-items'); - const findClearAllButton = () => wrapper.findByText('Clear all'); + const findClearAllButton = () => wrapper.findByTestId('listbox-reset-button'); const findSelectedProjectsLabel = () => wrapper.findComponent(GlTruncate); - const findDropdown = () => wrapper.findComponent(GlDropdown); + const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox); - const findDropdownItems = () => - findDropdown() - .findAllComponents(GlDropdownItem) - .filter((w) => w.text() !== 'No matching results'); + const findDropdownItems = () => findDropdown().findAllComponents(GlListboxItem); const findDropdownAtIndex = (index) => findDropdownItems().at(index); - const findDropdownButton = () => findDropdown().find('.dropdown-toggle'); + const findDropdownButton = () => findDropdown().findComponent(GlButton); const findDropdownButtonAvatar = () => findDropdown().find('.gl-avatar'); const findDropdownButtonAvatarAtIndex = (index) => - findDropdownAtIndex(index).find('img.gl-avatar'); + findDropdownAtIndex(index).findComponent(GlAvatar); const findDropdownButtonIdentIconAtIndex = (index) => findDropdownAtIndex(index).find('div.gl-avatar-identicon'); @@ -97,13 +78,15 @@ describe('ProjectsDropdownFilter component', () => { const findDropdownFullPathAtIndex = (index) => findDropdownAtIndex(index).find('[data-testid="project-full-path"]'); - const selectDropdownItemAtIndex = async (index) => { - findDropdownAtIndex(index).find('button').trigger('click'); + const selectDropdownItemAtIndex = async (indexes, multi = true) => { + const payload = indexes.map((index) => projects[index]?.id).filter(Boolean); + findDropdown().vm.$emit('select', multi ? payload : payload[0]); await nextTick(); }; // NOTE: Selected items are now visually separated from unselected items - const findSelectedDropdownItems = () => findHighlightedItems().findAllComponents(GlDropdownItem); + const findSelectedDropdownItems = () => + findDropdownItems().filter((component) => component.props('isSelected') === true); const findSelectedDropdownAtIndex = (index) => findSelectedDropdownItems().at(index); const findSelectedButtonIdentIconAtIndex = (index) => @@ -111,22 +94,20 @@ describe('ProjectsDropdownFilter component', () => { const findSelectedButtonAvatarItemAtIndex = (index) => findSelectedDropdownAtIndex(index).find('img.gl-avatar'); - const selectedIds = () => wrapper.vm.selectedProjects.map(({ id }) => id); - - const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType); - describe('queryParams are applied when fetching data', () => { beforeEach(() => { createComponent({ - queryParams: { - first: 50, - includeSubgroups: true, + props: { + queryParams: { + first: 50, + includeSubgroups: true, + }, }, }); }); it('applies the correct queryParams when making an api call', async () => { - findSearchBoxByType().vm.$emit('input', 'gitlab'); + findDropdown().vm.$emit('search', 'gitlab'); expect(spyQuery).toHaveBeenCalledTimes(1); @@ -147,17 +128,19 @@ describe('ProjectsDropdownFilter component', () => { const blockDefaultProps = { multiSelect: true }; beforeEach(() => { - createComponent(blockDefaultProps); + createComponent({ + props: blockDefaultProps, + }); }); describe('with no project selected', () => { - it('does not render the highlighted items', async () => { - await createWithMockDropdown(blockDefaultProps); - - expect(findSelectedDropdownItems().length).toBe(0); + it('does not render the highlighted items', () => { + expect(findSelectedDropdownItems()).toHaveLength(0); }); it('renders the default project label text', () => { + createComponent({ mountFn: mountExtended, props: blockDefaultProps }); + expect(findSelectedProjectsLabel().text()).toBe('Select projects'); }); @@ -167,18 +150,21 @@ describe('ProjectsDropdownFilter component', () => { }); describe('with a selected project', () => { - beforeEach(async () => { - await selectDropdownItemAtIndex(0); + beforeEach(() => { + createComponent({ + mountFn: mountExtended, + }); }); it('renders the highlighted items', async () => { - await createWithMockDropdown(blockDefaultProps); - await selectDropdownItemAtIndex(0); + await selectDropdownItemAtIndex([0], false); - expect(findSelectedDropdownItems().length).toBe(1); + expect(findSelectedDropdownItems()).toHaveLength(1); }); - it('renders the highlighted items title', () => { + it('renders the highlighted items title', async () => { + await selectDropdownItemAtIndex([0], false); + expect(findSelectedProjectsLabel().text()).toBe(projects[0].name); }); @@ -187,11 +173,17 @@ describe('ProjectsDropdownFilter component', () => { }); it('clears all selected items when the clear all button is clicked', async () => { - await selectDropdownItemAtIndex(1); + createComponent({ + mountFn: mountExtended, + props: { multiSelect: true }, + }); + await waitForPromises(); + + await selectDropdownItemAtIndex([0, 1]); expect(findSelectedProjectsLabel().text()).toBe('2 projects selected'); - await findClearAllButton().trigger('click'); + await findClearAllButton().vm.$emit('click'); expect(findSelectedProjectsLabel().text()).toBe('Select projects'); }); @@ -200,27 +192,35 @@ describe('ProjectsDropdownFilter component', () => { describe('with a selected project and search term', () => { beforeEach(async () => { - await createWithMockDropdown({ multiSelect: true }); + createComponent({ + props: { multiSelect: true }, + }); + await waitForPromises(); - selectDropdownItemAtIndex(0); - findSearchBoxByType().vm.$emit('input', 'this is a very long search string'); + await selectDropdownItemAtIndex([0]); + + findDropdown().vm.$emit('search', 'this is a very long search string'); }); it('renders the highlighted items', () => { - expect(findUnhighlightedItems().findAll('li').length).toBe(1); + expect(findSelectedDropdownItems()).toHaveLength(1); }); it('hides the unhighlighted items that do not match the string', () => { - expect(findUnhighlightedItems().findAll('li').length).toBe(1); - expect(findUnhighlightedItems().text()).toContain('No matching results'); + expect(wrapper.find(`[name="Selected"]`).findAllComponents(GlListboxItem).length).toBe(1); + expect(wrapper.find(`[name="Unselected"]`).findAllComponents(GlListboxItem).length).toBe(0); }); }); describe('when passed an array of defaultProject as prop', () => { - beforeEach(() => { + beforeEach(async () => { createComponent({ - defaultProjects: [projects[0]], + mountFn: mountExtended, + props: { + defaultProjects: [projects[0]], + }, }); + await waitForPromises(); }); it("displays the defaultProject's name", () => { @@ -232,14 +232,18 @@ describe('ProjectsDropdownFilter component', () => { }); it('marks the defaultProject as selected', () => { - expect(findDropdownAtIndex(0).props('isChecked')).toBe(true); + expect( + wrapper.findAll('[role="group"]').at(0).findAllComponents(GlListboxItem).at(0).text(), + ).toContain(projects[0].name); }); }); describe('when multiSelect is false', () => { const blockDefaultProps = { multiSelect: false }; beforeEach(() => { - createComponent(blockDefaultProps); + createComponent({ + props: blockDefaultProps, + }); }); describe('displays the correct information', () => { @@ -248,13 +252,12 @@ describe('ProjectsDropdownFilter component', () => { }); it('renders an avatar when the project has an avatarUrl', () => { - expect(findDropdownButtonAvatarAtIndex(0).exists()).toBe(true); + expect(findDropdownButtonAvatarAtIndex(0).props('src')).toBe(projects[0].avatarUrl); expect(findDropdownButtonIdentIconAtIndex(0).exists()).toBe(false); }); - it("renders an identicon when the project doesn't have an avatarUrl", () => { - expect(findDropdownButtonAvatarAtIndex(1).exists()).toBe(false); - expect(findDropdownButtonIdentIconAtIndex(1).exists()).toBe(true); + it("does not render an avatar when the project doesn't have an avatarUrl", () => { + expect(findDropdownButtonAvatarAtIndex(1).props('src')).toEqual(null); }); it('renders the project name', () => { @@ -271,37 +274,46 @@ describe('ProjectsDropdownFilter component', () => { }); describe('on project click', () => { - it('should emit the "selected" event with the selected project', () => { - selectDropdownItemAtIndex(0); + it('should emit the "selected" event with the selected project', async () => { + await selectDropdownItemAtIndex([0], false); - expect(wrapper.emitted().selected).toEqual([[[projects[0]]]]); + expect(wrapper.emitted('selected')).toEqual([[[projects[0]]]]); }); it('should change selection when new project is clicked', () => { - selectDropdownItemAtIndex(1); + selectDropdownItemAtIndex([1], false); - expect(wrapper.emitted().selected).toEqual([[[projects[1]]]]); + expect(wrapper.emitted('selected')).toEqual([[[projects[1]]]]); }); - it('selection should be emptied when a project is deselected', () => { - selectDropdownItemAtIndex(0); // Select the item - selectDropdownItemAtIndex(0); // deselect it + it('selection should be emptied when a project is deselected', async () => { + await selectDropdownItemAtIndex([0], false); // Select the item + await selectDropdownItemAtIndex([0], false); - expect(wrapper.emitted().selected).toEqual([[[projects[0]]], [[]]]); + expect(wrapper.emitted('selected')).toEqual([[[projects[0]]], [[]]]); }); it('renders an avatar in the dropdown button when the project has an avatarUrl', async () => { - await createWithMockDropdown(blockDefaultProps); - await selectDropdownItemAtIndex(0); + createComponent({ + mountFn: mountExtended, + props: blockDefaultProps, + }); + await waitForPromises(); + + await selectDropdownItemAtIndex([0], false); expect(findSelectedButtonAvatarItemAtIndex(0).exists()).toBe(true); expect(findSelectedButtonIdentIconAtIndex(0).exists()).toBe(false); }); it("renders an identicon in the dropdown button when the project doesn't have an avatarUrl", async () => { - await createWithMockDropdown(blockDefaultProps); - await selectDropdownItemAtIndex(1); + createComponent({ + mountFn: mountExtended, + props: blockDefaultProps, + }); + await waitForPromises(); + await selectDropdownItemAtIndex([1], false); expect(findSelectedButtonAvatarItemAtIndex(0).exists()).toBe(false); expect(findSelectedButtonIdentIconAtIndex(0).exists()).toBe(true); }); @@ -310,7 +322,9 @@ describe('ProjectsDropdownFilter component', () => { describe('when multiSelect is true', () => { beforeEach(() => { - createComponent({ multiSelect: true }); + createComponent({ + props: { multiSelect: true }, + }); }); describe('displays the correct information', () => { @@ -319,13 +333,12 @@ describe('ProjectsDropdownFilter component', () => { }); it('renders an avatar when the project has an avatarUrl', () => { - expect(findDropdownButtonAvatarAtIndex(0).exists()).toBe(true); + expect(findDropdownButtonAvatarAtIndex(0).props('src')).toBe(projects[0].avatarUrl); expect(findDropdownButtonIdentIconAtIndex(0).exists()).toBe(false); }); it("renders an identicon when the project doesn't have an avatarUrl", () => { - expect(findDropdownButtonAvatarAtIndex(1).exists()).toBe(false); - expect(findDropdownButtonIdentIconAtIndex(1).exists()).toBe(true); + expect(findDropdownButtonAvatarAtIndex(1).props('src')).toEqual(null); }); it('renders the project name', () => { @@ -342,27 +355,31 @@ describe('ProjectsDropdownFilter component', () => { }); describe('on project click', () => { - it('should add to selection when new project is clicked', () => { - selectDropdownItemAtIndex(0); - selectDropdownItemAtIndex(1); + it('should add to selection when new project is clicked', async () => { + await selectDropdownItemAtIndex([0, 1]); - expect(selectedIds()).toEqual([projects[0].id, projects[1].id]); + expect(findSelectedDropdownItems().at(0).text()).toContain(projects[1].name); + expect(findSelectedDropdownItems().at(1).text()).toContain(projects[0].name); }); - it('should remove from selection when clicked again', () => { - selectDropdownItemAtIndex(0); + it('should remove from selection when clicked again', async () => { + await selectDropdownItemAtIndex([0]); - expect(selectedIds()).toEqual([projects[0].id]); + expect(findSelectedDropdownItems().at(0).text()).toContain(projects[0].name); - selectDropdownItemAtIndex(0); + await selectDropdownItemAtIndex([]); - expect(selectedIds()).toEqual([]); + expect(findSelectedDropdownItems()).toHaveLength(0); }); it('renders the correct placeholder text when multiple projects are selected', async () => { - selectDropdownItemAtIndex(0); - selectDropdownItemAtIndex(1); - await nextTick(); + createComponent({ + props: { multiSelect: true }, + mountFn: mountExtended, + }); + await waitForPromises(); + + await selectDropdownItemAtIndex([0, 1]); expect(findDropdownButton().text()).toBe('2 projects selected'); });