diff options
Diffstat (limited to 'spec/frontend/ml/model_registry/components')
4 files changed, 245 insertions, 148 deletions
diff --git a/spec/frontend/ml/model_registry/components/actions_dropdown_spec.js b/spec/frontend/ml/model_registry/components/actions_dropdown_spec.js new file mode 100644 index 00000000000..6285d7360c7 --- /dev/null +++ b/spec/frontend/ml/model_registry/components/actions_dropdown_spec.js @@ -0,0 +1,39 @@ +import { mount } from '@vue/test-utils'; +import { GlDisclosureDropdownItem } from '@gitlab/ui'; +import ActionsDropdown from '~/ml/model_registry/components/actions_dropdown.vue'; + +describe('ml/model_registry/components/actions_dropdown', () => { + let wrapper; + + const showToast = jest.fn(); + + const createWrapper = () => { + wrapper = mount(ActionsDropdown, { + mocks: { + $toast: { + show: showToast, + }, + }, + provide: { + mlflowTrackingUrl: 'path/to/mlflow', + }, + }); + }; + + const findCopyLinkDropdownItem = () => wrapper.findComponent(GlDisclosureDropdownItem); + + it('has data-clipboard-text set to the correct url', () => { + createWrapper(); + + expect(findCopyLinkDropdownItem().text()).toBe('Copy MLflow tracking URL'); + expect(findCopyLinkDropdownItem().attributes()['data-clipboard-text']).toBe('path/to/mlflow'); + }); + + it('shows a success toast after copying the url to the clipboard', () => { + createWrapper(); + + findCopyLinkDropdownItem().find('button').trigger('click'); + + expect(showToast).toHaveBeenCalledWith('Copied MLflow tracking URL to clipboard'); + }); +}); diff --git a/spec/frontend/ml/model_registry/components/candidate_list_spec.js b/spec/frontend/ml/model_registry/components/candidate_list_spec.js index c10222a99fd..8491c7be16f 100644 --- a/spec/frontend/ml/model_registry/components/candidate_list_spec.js +++ b/spec/frontend/ml/model_registry/components/candidate_list_spec.js @@ -1,13 +1,11 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; -import { GlAlert } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; +import { mount } from '@vue/test-utils'; import * as Sentry from '~/sentry/sentry_browser_wrapper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import CandidateList from '~/ml/model_registry/components/candidate_list.vue'; -import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue'; -import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue'; +import SearchableList from '~/ml/model_registry/components/searchable_list.vue'; import CandidateListRow from '~/ml/model_registry/components/candidate_list_row.vue'; import getModelCandidatesQuery from '~/ml/model_registry/graphql/queries/get_model_candidates.query.graphql'; import { GRAPHQL_PAGE_SIZE } from '~/ml/model_registry/constants'; @@ -24,10 +22,7 @@ describe('ml/model_registry/components/candidate_list.vue', () => { let wrapper; let apolloProvider; - const findAlert = () => wrapper.findComponent(GlAlert); - const findLoader = () => wrapper.findComponent(PackagesListLoader); - const findRegistryList = () => wrapper.findComponent(RegistryList); - const findListRow = () => wrapper.findComponent(CandidateListRow); + const findSearchableList = () => wrapper.findComponent(SearchableList); const findAllRows = () => wrapper.findAllComponents(CandidateListRow); const mountComponent = ({ @@ -37,15 +32,12 @@ describe('ml/model_registry/components/candidate_list.vue', () => { const requestHandlers = [[getModelCandidatesQuery, resolver]]; apolloProvider = createMockApollo(requestHandlers); - wrapper = shallowMount(CandidateList, { + wrapper = mount(CandidateList, { apolloProvider, propsData: { modelId: 2, ...props, }, - stubs: { - RegistryList, - }, }); }; @@ -60,25 +52,9 @@ describe('ml/model_registry/components/candidate_list.vue', () => { await waitForPromises(); }); - it('displays empty slot message', () => { + it('shows empty state', () => { expect(wrapper.text()).toContain('This model has no candidates'); }); - - it('does not display loader', () => { - expect(findLoader().exists()).toBe(false); - }); - - it('does not display rows', () => { - expect(findListRow().exists()).toBe(false); - }); - - it('does not display registry list', () => { - expect(findRegistryList().exists()).toBe(false); - }); - - it('does not display alert', () => { - expect(findAlert().exists()).toBe(false); - }); }); describe('if load fails, alert', () => { @@ -90,19 +66,9 @@ describe('ml/model_registry/components/candidate_list.vue', () => { }); it('is displayed', () => { - expect(findAlert().exists()).toBe(true); - }); - - it('shows error message', () => { - expect(findAlert().text()).toContain('Failed to load model candidates with error: Failure!'); - }); - - it('is not dismissible', () => { - expect(findAlert().props('dismissible')).toBe(false); - }); - - it('is of variant danger', () => { - expect(findAlert().attributes('variant')).toBe('danger'); + expect(findSearchableList().props('errorMessage')).toBe( + 'Failed to load model candidates with error: Failure!', + ); }); it('error is logged in sentry', () => { @@ -116,21 +82,11 @@ describe('ml/model_registry/components/candidate_list.vue', () => { await waitForPromises(); }); - it('displays package registry list', () => { - expect(findRegistryList().exists()).toEqual(true); + it('Passes items to list', () => { + expect(findSearchableList().props('items')).toEqual(graphqlCandidates); }); - it('binds the right props', () => { - expect(findRegistryList().props()).toMatchObject({ - items: graphqlCandidates, - pagination: {}, - isLoading: false, - hiddenDelete: true, - }); - }); - - it('displays candidate rows', () => { - expect(findAllRows().exists()).toEqual(true); + it('displays package version rows', () => { expect(findAllRows()).toHaveLength(graphqlCandidates.length); }); @@ -143,17 +99,9 @@ describe('ml/model_registry/components/candidate_list.vue', () => { candidate: expect.objectContaining(graphqlCandidates[1]), }); }); - - it('does not display loader', () => { - expect(findLoader().exists()).toBe(false); - }); - - it('does not display empty message', () => { - expect(findAlert().exists()).toBe(false); - }); }); - describe('when user interacts with pagination', () => { + describe('when list requests update', () => { const resolver = jest.fn().mockResolvedValue(modelCandidatesQuery()); beforeEach(async () => { @@ -161,21 +109,17 @@ describe('ml/model_registry/components/candidate_list.vue', () => { await waitForPromises(); }); - it('when list emits next-page fetches the next set of records', async () => { - findRegistryList().vm.$emit('next-page'); - await waitForPromises(); - - expect(resolver).toHaveBeenLastCalledWith( - expect.objectContaining({ after: graphqlPageInfo.endCursor, first: GRAPHQL_PAGE_SIZE }), - ); - }); + it('when list emits fetch-page fetches the next set of records', async () => { + findSearchableList().vm.$emit('fetch-page', { + after: 'eyJpZCI6IjIifQ', + first: 30, + id: 'gid://gitlab/Ml::Model/2', + }); - it('when list emits prev-page fetches the prev set of records', async () => { - findRegistryList().vm.$emit('prev-page'); await waitForPromises(); expect(resolver).toHaveBeenLastCalledWith( - expect.objectContaining({ before: graphqlPageInfo.startCursor, last: GRAPHQL_PAGE_SIZE }), + expect.objectContaining({ after: graphqlPageInfo.endCursor, first: GRAPHQL_PAGE_SIZE }), ); }); }); diff --git a/spec/frontend/ml/model_registry/components/model_version_list_spec.js b/spec/frontend/ml/model_registry/components/model_version_list_spec.js index 41f7e71c543..f5d6acf3bae 100644 --- a/spec/frontend/ml/model_registry/components/model_version_list_spec.js +++ b/spec/frontend/ml/model_registry/components/model_version_list_spec.js @@ -1,13 +1,11 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; -import { GlAlert } from '@gitlab/ui'; import * as Sentry from '~/sentry/sentry_browser_wrapper'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import ModelVersionList from '~/ml/model_registry/components/model_version_list.vue'; -import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue'; -import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue'; +import SearchableList from '~/ml/model_registry/components/searchable_list.vue'; import ModelVersionRow from '~/ml/model_registry/components/model_version_row.vue'; import getModelVersionsQuery from '~/ml/model_registry/graphql/queries/get_model_versions.query.graphql'; import EmptyState from '~/ml/model_registry/components/empty_state.vue'; @@ -25,11 +23,8 @@ describe('ModelVersionList', () => { let wrapper; let apolloProvider; - const findAlert = () => wrapper.findComponent(GlAlert); - const findLoader = () => wrapper.findComponent(PackagesListLoader); - const findRegistryList = () => wrapper.findComponent(RegistryList); + const findSearchableList = () => wrapper.findComponent(SearchableList); const findEmptyState = () => wrapper.findComponent(EmptyState); - const findListRow = () => wrapper.findComponent(ModelVersionRow); const findAllRows = () => wrapper.findAllComponents(ModelVersionRow); const mountComponent = ({ @@ -39,15 +34,12 @@ describe('ModelVersionList', () => { const requestHandlers = [[getModelVersionsQuery, resolver]]; apolloProvider = createMockApollo(requestHandlers); - wrapper = shallowMountExtended(ModelVersionList, { + wrapper = mountExtended(ModelVersionList, { apolloProvider, propsData: { modelId: 2, ...props, }, - stubs: { - RegistryList, - }, }); }; @@ -65,22 +57,6 @@ describe('ModelVersionList', () => { it('shows empty state', () => { expect(findEmptyState().props('entityType')).toBe(MODEL_ENTITIES.modelVersion); }); - - it('does not display loader', () => { - expect(findLoader().exists()).toBe(false); - }); - - it('does not display rows', () => { - expect(findListRow().exists()).toBe(false); - }); - - it('does not display registry list', () => { - expect(findRegistryList().exists()).toBe(false); - }); - - it('does not display alert', () => { - expect(findAlert().exists()).toBe(false); - }); }); describe('if load fails, alert', () => { @@ -92,19 +68,9 @@ describe('ModelVersionList', () => { }); it('is displayed', () => { - expect(findAlert().exists()).toBe(true); - }); - - it('shows error message', () => { - expect(findAlert().text()).toContain('Failed to load model versions with error: Failure!'); - }); - - it('is not dismissible', () => { - expect(findAlert().props('dismissible')).toBe(false); - }); - - it('is of variant danger', () => { - expect(findAlert().attributes('variant')).toBe('danger'); + expect(findSearchableList().props('errorMessage')).toBe( + 'Failed to load model versions with error: Failure!', + ); }); it('error is logged in sentry', () => { @@ -118,21 +84,11 @@ describe('ModelVersionList', () => { await waitForPromises(); }); - it('displays package registry list', () => { - expect(findRegistryList().exists()).toEqual(true); - }); - - it('binds the right props', () => { - expect(findRegistryList().props()).toMatchObject({ - items: graphqlModelVersions, - pagination: {}, - isLoading: false, - hiddenDelete: true, - }); + it('Passes items to list', () => { + expect(findSearchableList().props('items')).toEqual(graphqlModelVersions); }); it('displays package version rows', () => { - expect(findAllRows().exists()).toEqual(true); expect(findAllRows()).toHaveLength(graphqlModelVersions.length); }); @@ -145,17 +101,9 @@ describe('ModelVersionList', () => { modelVersion: expect.objectContaining(graphqlModelVersions[1]), }); }); - - it('does not display loader', () => { - expect(findLoader().exists()).toBe(false); - }); - - it('does not display empty state', () => { - expect(findEmptyState().exists()).toBe(false); - }); }); - describe('when user interacts with pagination', () => { + describe('when list requests update', () => { const resolver = jest.fn().mockResolvedValue(modelVersionsQuery()); beforeEach(async () => { @@ -163,21 +111,17 @@ describe('ModelVersionList', () => { await waitForPromises(); }); - it('when list emits next-page fetches the next set of records', async () => { - findRegistryList().vm.$emit('next-page'); - await waitForPromises(); - - expect(resolver).toHaveBeenLastCalledWith( - expect.objectContaining({ after: graphqlPageInfo.endCursor, first: GRAPHQL_PAGE_SIZE }), - ); - }); + it('when list emits fetch-page fetches the next set of records', async () => { + findSearchableList().vm.$emit('fetch-page', { + after: 'eyJpZCI6IjIifQ', + first: 30, + id: 'gid://gitlab/Ml::Model/2', + }); - it('when list emits prev-page fetches the prev set of records', async () => { - findRegistryList().vm.$emit('prev-page'); await waitForPromises(); expect(resolver).toHaveBeenLastCalledWith( - expect.objectContaining({ before: graphqlPageInfo.startCursor, last: GRAPHQL_PAGE_SIZE }), + expect.objectContaining({ after: graphqlPageInfo.endCursor, first: GRAPHQL_PAGE_SIZE }), ); }); }); diff --git a/spec/frontend/ml/model_registry/components/searchable_list_spec.js b/spec/frontend/ml/model_registry/components/searchable_list_spec.js new file mode 100644 index 00000000000..ea58a9a830a --- /dev/null +++ b/spec/frontend/ml/model_registry/components/searchable_list_spec.js @@ -0,0 +1,170 @@ +import { GlAlert } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import SearchableList from '~/ml/model_registry/components/searchable_list.vue'; +import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue'; +import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue'; +import { defaultPageInfo } from '../mock_data'; + +describe('ml/model_registry/components/searchable_list.vue', () => { + let wrapper; + + const findAlert = () => wrapper.findComponent(GlAlert); + const findLoader = () => wrapper.findComponent(PackagesListLoader); + const findRegistryList = () => wrapper.findComponent(RegistryList); + const findEmptyState = () => wrapper.findByTestId('empty-state-slot'); + const findFirstRow = () => wrapper.findByTestId('element'); + const findRows = () => wrapper.findAllByTestId('element'); + + const defaultProps = { + items: ['a', 'b', 'c'], + pageInfo: defaultPageInfo, + isLoading: false, + errorMessage: '', + }; + + const mountComponent = (props = {}) => { + wrapper = shallowMountExtended(SearchableList, { + propsData: { + ...defaultProps, + ...props, + }, + stubs: { + RegistryList, + }, + slots: { + 'empty-state': '<div data-testid="empty-state-slot">This is empty</div>', + item: '<div data-testid="element"></div>', + }, + }); + }; + + describe('when list is loaded and has no data', () => { + beforeEach(() => mountComponent({ items: [] })); + + it('shows empty state', () => { + expect(findEmptyState().text()).toBe('This is empty'); + }); + + it('does not display loader', () => { + expect(findLoader().exists()).toBe(false); + }); + + it('does not display rows', () => { + expect(findFirstRow().exists()).toBe(false); + }); + + it('does not display registry list', () => { + expect(findRegistryList().exists()).toBe(false); + }); + + it('does not display alert', () => { + expect(findAlert().exists()).toBe(false); + }); + }); + + describe('if errorMessage', () => { + beforeEach(() => mountComponent({ errorMessage: 'Failure!' })); + + it('shows error message', () => { + expect(findAlert().text()).toContain('Failure!'); + }); + + it('is not dismissible', () => { + expect(findAlert().props('dismissible')).toBe(false); + }); + + it('is of variant danger', () => { + expect(findAlert().attributes('variant')).toBe('danger'); + }); + + it('hides loader', () => { + expect(findLoader().exists()).toBe(false); + }); + + it('hides registry list', () => { + expect(findRegistryList().exists()).toBe(false); + }); + + it('hides empty state', () => { + expect(findEmptyState().exists()).toBe(false); + }); + }); + + describe('if loading', () => { + beforeEach(() => mountComponent({ isLoading: true })); + + it('shows loader', () => { + expect(findLoader().exists()).toBe(true); + }); + + it('hides error message', () => { + expect(findAlert().exists()).toBe(false); + }); + + it('hides registry list', () => { + expect(findRegistryList().exists()).toBe(false); + }); + + it('hides empty state', () => { + expect(findEmptyState().exists()).toBe(false); + }); + }); + + describe('when list is loaded with data', () => { + beforeEach(() => mountComponent()); + + it('displays package registry list', () => { + expect(findRegistryList().exists()).toEqual(true); + }); + + it('binds the right props', () => { + expect(findRegistryList().props()).toMatchObject({ + items: ['a', 'b', 'c'], + isLoading: false, + pagination: defaultPageInfo, + hiddenDelete: true, + }); + }); + + it('displays package version rows', () => { + expect(findRows().exists()).toEqual(true); + expect(findRows()).toHaveLength(3); + }); + + it('does not display loader', () => { + expect(findLoader().exists()).toBe(false); + }); + + it('does not display empty state', () => { + expect(findEmptyState().exists()).toBe(false); + }); + }); + + describe('when user interacts with pagination', () => { + beforeEach(() => mountComponent()); + + it('when list emits next-page emits fetchPage with correct pageInfo', () => { + findRegistryList().vm.$emit('next-page'); + + const expectedNewPageInfo = { + after: 'eyJpZCI6IjIifQ', + first: 30, + last: null, + }; + + expect(wrapper.emitted('fetch-page')).toEqual([[expectedNewPageInfo]]); + }); + + it('when list emits prev-page emits fetchPage with correct pageInfo', () => { + findRegistryList().vm.$emit('prev-page'); + + const expectedNewPageInfo = { + before: 'eyJpZCI6IjE2In0', + first: null, + last: 30, + }; + + expect(wrapper.emitted('fetch-page')).toEqual([[expectedNewPageInfo]]); + }); + }); +}); |