diff options
Diffstat (limited to 'spec/frontend')
8 files changed, 368 insertions, 66 deletions
diff --git a/spec/frontend/ml/model_registry/apps/show_ml_model_spec.js b/spec/frontend/ml/model_registry/apps/show_ml_model_spec.js index d58fc70af64..cf282f81cbc 100644 --- a/spec/frontend/ml/model_registry/apps/show_ml_model_spec.js +++ b/spec/frontend/ml/model_registry/apps/show_ml_model_spec.js @@ -1,16 +1,28 @@ import { GlBadge, GlTab } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; import { ShowMlModel } from '~/ml/model_registry/apps'; import ModelVersionList from '~/ml/model_registry/components/model_version_list.vue'; +import CandidateList from '~/ml/model_registry/components/candidate_list.vue'; import ModelVersionDetail from '~/ml/model_registry/components/model_version_detail.vue'; import TitleArea from '~/vue_shared/components/registry/title_area.vue'; import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue'; import { NO_VERSIONS_LABEL } from '~/ml/model_registry/translations'; +import createMockApollo from 'helpers/mock_apollo_helper'; import { MODEL, makeModel } from '../mock_data'; +const apolloProvider = createMockApollo([]); let wrapper; + +Vue.use(VueApollo); + const createWrapper = (model = MODEL) => { - wrapper = shallowMount(ShowMlModel, { propsData: { model }, stubs: { GlTab } }); + wrapper = shallowMount(ShowMlModel, { + apolloProvider, + propsData: { model }, + stubs: { GlTab }, + }); }; const findDetailTab = () => wrapper.findAllComponents(GlTab).at(0); @@ -19,6 +31,7 @@ const findVersionsCountBadge = () => findVersionsTab().findComponent(GlBadge); const findModelVersionList = () => findVersionsTab().findComponent(ModelVersionList); const findModelVersionDetail = () => findDetailTab().findComponent(ModelVersionDetail); const findCandidateTab = () => wrapper.findAllComponents(GlTab).at(2); +const findCandidateList = () => findCandidateTab().findComponent(CandidateList); const findCandidatesCountBadge = () => findCandidateTab().findComponent(GlBadge); const findTitleArea = () => wrapper.findComponent(TitleArea); const findVersionCountMetadataItem = () => findTitleArea().findComponent(MetadataItem); @@ -90,5 +103,9 @@ describe('ShowMlModel', () => { it('shows the number of candidates in the tab', () => { expect(findCandidatesCountBadge().text()).toBe(MODEL.candidateCount.toString()); }); + + it('shows a list of candidates', () => { + expect(findCandidateList().props('modelId')).toBe(MODEL.id); + }); }); }); diff --git a/spec/frontend/ml/model_registry/components/candidate_list_row_spec.js b/spec/frontend/ml/model_registry/components/candidate_list_row_spec.js new file mode 100644 index 00000000000..5ac8d07ff01 --- /dev/null +++ b/spec/frontend/ml/model_registry/components/candidate_list_row_spec.js @@ -0,0 +1,39 @@ +import { GlLink, GlSprintf, GlTruncate } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import ListItem from '~/vue_shared/components/registry/list_item.vue'; +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import CandidateListRow from '~/ml/model_registry/components/candidate_list_row.vue'; +import { graphqlCandidates } from '../graphql_mock_data'; + +const CANDIDATE = graphqlCandidates[0]; + +let wrapper; +const createWrapper = (candidate = CANDIDATE) => { + wrapper = shallowMount(CandidateListRow, { + propsData: { candidate }, + stubs: { + GlSprintf, + GlTruncate, + }, + }); +}; + +const findListItem = () => wrapper.findComponent(ListItem); +const findLink = () => findListItem().findComponent(GlLink); +const findTruncated = () => findLink().findComponent(GlTruncate); +const findTooltip = () => findListItem().findComponent(TimeAgoTooltip); + +describe('ml/model_registry/components/candidate_list_row.vue', () => { + beforeEach(() => { + createWrapper(); + }); + + it('Has a link to the candidate', () => { + expect(findTruncated().props('text')).toBe(CANDIDATE.name); + expect(findLink().attributes('href')).toBe(CANDIDATE._links.showPath); + }); + + it('Shows created at', () => { + expect(findTooltip().props('time')).toBe(CANDIDATE.createdAt); + }); +}); diff --git a/spec/frontend/ml/model_registry/components/candidate_list_spec.js b/spec/frontend/ml/model_registry/components/candidate_list_spec.js new file mode 100644 index 00000000000..c10222a99fd --- /dev/null +++ b/spec/frontend/ml/model_registry/components/candidate_list_spec.js @@ -0,0 +1,182 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import { GlAlert } from '@gitlab/ui'; +import { shallowMount } 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 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'; +import { + emptyCandidateQuery, + modelCandidatesQuery, + graphqlCandidates, + graphqlPageInfo, +} from '../graphql_mock_data'; + +Vue.use(VueApollo); + +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 findAllRows = () => wrapper.findAllComponents(CandidateListRow); + + const mountComponent = ({ + props = {}, + resolver = jest.fn().mockResolvedValue(modelCandidatesQuery()), + } = {}) => { + const requestHandlers = [[getModelCandidatesQuery, resolver]]; + apolloProvider = createMockApollo(requestHandlers); + + wrapper = shallowMount(CandidateList, { + apolloProvider, + propsData: { + modelId: 2, + ...props, + }, + stubs: { + RegistryList, + }, + }); + }; + + beforeEach(() => { + jest.spyOn(Sentry, 'captureException').mockImplementation(); + }); + + describe('when list is loaded and has no data', () => { + const resolver = jest.fn().mockResolvedValue(emptyCandidateQuery); + beforeEach(async () => { + mountComponent({ resolver }); + await waitForPromises(); + }); + + it('displays empty slot message', () => { + 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', () => { + beforeEach(async () => { + const error = new Error('Failure!'); + mountComponent({ resolver: jest.fn().mockRejectedValue(error) }); + + await waitForPromises(); + }); + + 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'); + }); + + it('error is logged in sentry', () => { + expect(Sentry.captureException).toHaveBeenCalled(); + }); + }); + + describe('when list is loaded with data', () => { + beforeEach(async () => { + mountComponent(); + await waitForPromises(); + }); + + it('displays package registry list', () => { + expect(findRegistryList().exists()).toEqual(true); + }); + + 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); + expect(findAllRows()).toHaveLength(graphqlCandidates.length); + }); + + it('binds the correct props', () => { + expect(findAllRows().at(0).props()).toMatchObject({ + candidate: expect.objectContaining(graphqlCandidates[0]), + }); + + expect(findAllRows().at(1).props()).toMatchObject({ + 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', () => { + const resolver = jest.fn().mockResolvedValue(modelCandidatesQuery()); + + beforeEach(async () => { + mountComponent({ resolver }); + 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 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 }), + ); + }); + }); +}); 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 52010820b80..1d57baa050b 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 @@ -17,7 +17,7 @@ import { modelVersionsQuery, graphqlModelVersions, graphqlPageInfo, -} from '../mock_data'; +} from '../graphql_mock_data'; Vue.use(VueApollo); @@ -85,7 +85,8 @@ describe('ModelVersionList', () => { describe('if load fails, alert', () => { beforeEach(async () => { - mountComponent({ resolver: jest.fn().mockRejectedValue() }); + const error = new Error('Failure!'); + mountComponent({ resolver: jest.fn().mockRejectedValue(error) }); await waitForPromises(); }); @@ -95,7 +96,7 @@ describe('ModelVersionList', () => { }); it('shows error message', () => { - expect(findAlert().text()).toMatchInterpolatedText('Failed to load model versions'); + expect(findAlert().text()).toContain('Failed to load model versions with error: Failure!'); }); it('is not dismissible', () => { diff --git a/spec/frontend/ml/model_registry/components/model_version_row_spec.js b/spec/frontend/ml/model_registry/components/model_version_row_spec.js index bcdc8faa61f..9f709f2e072 100644 --- a/spec/frontend/ml/model_registry/components/model_version_row_spec.js +++ b/spec/frontend/ml/model_registry/components/model_version_row_spec.js @@ -3,7 +3,7 @@ import { shallowMount } from '@vue/test-utils'; import ListItem from '~/vue_shared/components/registry/list_item.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import ModelVersionRow from '~/ml/model_registry/components/model_version_row.vue'; -import { graphqlModelVersions } from '../mock_data'; +import { graphqlModelVersions } from '../graphql_mock_data'; let wrapper; const createWrapper = (modelVersion = graphqlModelVersions[0]) => { diff --git a/spec/frontend/ml/model_registry/graphql_mock_data.js b/spec/frontend/ml/model_registry/graphql_mock_data.js new file mode 100644 index 00000000000..1c31ee4627f --- /dev/null +++ b/spec/frontend/ml/model_registry/graphql_mock_data.js @@ -0,0 +1,116 @@ +import { defaultPageInfo } from './mock_data'; + +export const graphqlPageInfo = { + ...defaultPageInfo, + __typename: 'PageInfo', +}; + +export const graphqlModelVersions = [ + { + createdAt: '2021-08-10T09:33:54Z', + id: 'gid://gitlab/Ml::ModelVersion/243', + version: '1.0.1', + _links: { + showPath: '/path/to/modelversion/243', + }, + __typename: 'MlModelVersion', + }, + { + createdAt: '2021-08-10T09:33:54Z', + id: 'gid://gitlab/Ml::ModelVersion/244', + version: '1.0.2', + _links: { + showPath: '/path/to/modelversion/244', + }, + __typename: 'MlModelVersion', + }, +]; + +export const modelVersionsQuery = (versions = graphqlModelVersions) => ({ + data: { + mlModel: { + id: 'gid://gitlab/Ml::Model/2', + versions: { + count: versions.length, + nodes: versions, + pageInfo: graphqlPageInfo, + __typename: 'MlModelConnection', + }, + __typename: 'MlModelType', + }, + }, +}); + +export const graphqlCandidates = [ + { + id: 'gid://gitlab/Ml::Candidate/1', + name: 'narwhal-aardvark-heron-6953', + createdAt: '2023-12-06T12:41:48Z', + _links: { + showPath: '/path/to/candidate/1', + }, + }, + { + id: 'gid://gitlab/Ml::Candidate/2', + name: 'anteater-chimpanzee-snake-1254', + createdAt: '2023-12-06T12:41:48Z', + _links: { + showPath: '/path/to/candidate/2', + }, + }, +]; + +export const modelCandidatesQuery = (candidates = graphqlCandidates) => ({ + data: { + mlModel: { + id: 'gid://gitlab/Ml::Model/2', + candidates: { + count: candidates.length, + nodes: candidates, + pageInfo: graphqlPageInfo, + __typename: 'MlCandidateConnection', + }, + __typename: 'MlModelType', + }, + }, +}); + +export const emptyModelVersionsQuery = { + data: { + mlModel: { + id: 'gid://gitlab/Ml::Model/2', + versions: { + count: 0, + nodes: [], + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + endCursor: 'endCursor', + startCursor: 'startCursor', + }, + __typename: 'MlModelConnection', + }, + __typename: 'MlModelType', + }, + }, +}; + +export const emptyCandidateQuery = { + data: { + mlModel: { + id: 'gid://gitlab/Ml::Model/2', + candidates: { + count: 0, + nodes: [], + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + endCursor: 'endCursor', + startCursor: 'startCursor', + }, + __typename: 'MlCandidateConnection', + }, + __typename: 'MlModelType', + }, + }, +}; diff --git a/spec/frontend/ml/model_registry/mock_data.js b/spec/frontend/ml/model_registry/mock_data.js index 78e22eda7b9..4399df38990 100644 --- a/spec/frontend/ml/model_registry/mock_data.js +++ b/spec/frontend/ml/model_registry/mock_data.js @@ -102,64 +102,3 @@ export const defaultPageInfo = Object.freeze({ hasNextPage: true, hasPreviousPage: true, }); - -export const graphqlPageInfo = { - ...defaultPageInfo, - __typename: 'PageInfo', -}; - -export const graphqlModelVersions = [ - { - createdAt: '2021-08-10T09:33:54Z', - id: 'gid://gitlab/Ml::ModelVersion/243', - version: '1.0.1', - _links: { - showPath: '/path/to/modelversion/243', - }, - __typename: 'MlModelVersion', - }, - { - createdAt: '2021-08-10T09:33:54Z', - id: 'gid://gitlab/Ml::ModelVersion/244', - version: '1.0.2', - _links: { - showPath: '/path/to/modelversion/244', - }, - __typename: 'MlModelVersion', - }, -]; - -export const modelVersionsQuery = (versions = graphqlModelVersions) => ({ - data: { - mlModel: { - id: 'gid://gitlab/Ml::Model/2', - versions: { - count: versions.length, - nodes: versions, - pageInfo: graphqlPageInfo, - __typename: 'MlModelConnection', - }, - __typename: 'MlModelType', - }, - }, -}); - -export const emptyModelVersionsQuery = { - data: { - mlModel: { - id: 'gid://gitlab/Ml::Model/2', - versions: { - count: 0, - nodes: [], - pageInfo: { - hasNextPage: false, - hasPreviousPage: false, - endCursor: 'endCursor', - startCursor: 'startCursor', - }, - __typename: 'MlModelConnection', - }, - __typename: 'MlModelType', - }, - }, -}; diff --git a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js index 8b672ff3f32..207ce8c1ffa 100644 --- a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js +++ b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js @@ -137,6 +137,7 @@ describe('Settings Panel', () => { const findMonitorSettings = () => wrapper.findComponent({ ref: 'monitor-settings' }); const findModelExperimentsSettings = () => wrapper.findComponent({ ref: 'model-experiments-settings' }); + const findModelRegistrySettings = () => wrapper.findComponent({ ref: 'model-registry-settings' }); describe('Project Visibility', () => { it('should set the project visibility help path', () => { @@ -758,4 +759,11 @@ describe('Settings Panel', () => { expect(findModelExperimentsSettings().exists()).toBe(true); }); }); + describe('Model registry', () => { + it('shows model registry toggle', () => { + wrapper = mountComponent({}); + + expect(findModelRegistrySettings().exists()).toBe(true); + }); + }); }); |