diff options
Diffstat (limited to 'spec/frontend/security_configuration/components/training_provider_list_spec.js')
-rw-r--r-- | spec/frontend/security_configuration/components/training_provider_list_spec.js | 189 |
1 files changed, 159 insertions, 30 deletions
diff --git a/spec/frontend/security_configuration/components/training_provider_list_spec.js b/spec/frontend/security_configuration/components/training_provider_list_spec.js index 18c9ada6bde..b8c1bef0ddd 100644 --- a/spec/frontend/security_configuration/components/training_provider_list_spec.js +++ b/spec/frontend/security_configuration/components/training_provider_list_spec.js @@ -1,15 +1,20 @@ import * as Sentry from '@sentry/browser'; -import { GlAlert, GlLink, GlToggle, GlCard, GlSkeletonLoader } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; +import { GlAlert, GlLink, GlToggle, GlCard, GlSkeletonLoader, GlIcon } from '@gitlab/ui'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { TRACK_TOGGLE_TRAINING_PROVIDER_ACTION, TRACK_TOGGLE_TRAINING_PROVIDER_LABEL, + TRACK_PROVIDER_LEARN_MORE_CLICK_ACTION, + TRACK_PROVIDER_LEARN_MORE_CLICK_LABEL, } from '~/security_configuration/constants'; +import { TEMP_PROVIDER_URLS } from '~/security_configuration/components/constants'; import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue'; +import { updateSecurityTrainingOptimisticResponse } from '~/security_configuration/graphql/cache_utils'; import securityTrainingProvidersQuery from '~/security_configuration/graphql/security_training_providers.query.graphql'; import configureSecurityTrainingProvidersMutation from '~/security_configuration/graphql/configure_security_training_providers.mutation.graphql'; import dismissUserCalloutMutation from '~/graphql_shared/mutations/dismiss_user_callout.mutation.graphql'; @@ -17,16 +22,30 @@ import waitForPromises from 'helpers/wait_for_promises'; import { dismissUserCalloutResponse, dismissUserCalloutErrorResponse, - securityTrainingProviders, - securityTrainingProvidersResponse, + getSecurityTrainingProvidersData, updateSecurityTrainingProvidersResponse, updateSecurityTrainingProvidersErrorResponse, testProjectPath, - textProviderIds, + testProviderIds, + testProviderName, + tempProviderLogos, } from '../mock_data'; Vue.use(VueApollo); +const TEST_TRAINING_PROVIDERS_ALL_DISABLED = getSecurityTrainingProvidersData(); +const TEST_TRAINING_PROVIDERS_FIRST_ENABLED = getSecurityTrainingProvidersData({ + providerOverrides: { first: { isEnabled: true, isPrimary: true } }, +}); +const TEST_TRAINING_PROVIDERS_ALL_ENABLED = getSecurityTrainingProvidersData({ + providerOverrides: { + first: { isEnabled: true, isPrimary: true }, + second: { isEnabled: true, isPrimary: false }, + third: { isEnabled: true, isPrimary: false }, + }, +}); +const TEST_TRAINING_PROVIDERS_DEFAULT = TEST_TRAINING_PROVIDERS_ALL_DISABLED; + describe('TrainingProviderList component', () => { let wrapper; let apolloProvider; @@ -35,7 +54,7 @@ describe('TrainingProviderList component', () => { const defaultHandlers = [ [ securityTrainingProvidersQuery, - jest.fn().mockResolvedValue(securityTrainingProvidersResponse), + jest.fn().mockResolvedValue(TEST_TRAINING_PROVIDERS_DEFAULT.response), ], [ configureSecurityTrainingProvidersMutation, @@ -50,10 +69,13 @@ describe('TrainingProviderList component', () => { }; const createComponent = () => { - wrapper = shallowMount(TrainingProviderList, { + wrapper = shallowMountExtended(TrainingProviderList, { provide: { projectFullPath: testProjectPath, }, + directives: { + GlTooltip: createMockDirective(), + }, apolloProvider, }); }; @@ -65,10 +87,12 @@ describe('TrainingProviderList component', () => { const findLinks = () => wrapper.findAllComponents(GlLink); const findToggles = () => wrapper.findAllComponents(GlToggle); const findFirstToggle = () => findToggles().at(0); + const findPrimaryProviderRadios = () => wrapper.findAllByTestId('primary-provider-radio'); const findLoader = () => wrapper.findComponent(GlSkeletonLoader); const findErrorAlert = () => wrapper.findComponent(GlAlert); + const findLogos = () => wrapper.findAllByTestId('provider-logo'); - const toggleFirstProvider = () => findFirstToggle().vm.$emit('change', textProviderIds[0]); + const toggleFirstProvider = () => findFirstToggle().vm.$emit('change', testProviderIds[0]); afterEach(() => { wrapper.destroy(); @@ -104,7 +128,7 @@ describe('TrainingProviderList component', () => { Mutation: { configureSecurityTrainingProviders: () => ({ errors: [], - securityTrainingProviders: [], + TEST_TRAINING_PROVIDERS_DEFAULT: [], }), }, }, @@ -119,10 +143,10 @@ describe('TrainingProviderList component', () => { }); it('renders correct amount of cards', () => { - expect(findCards()).toHaveLength(securityTrainingProviders.length); + expect(findCards()).toHaveLength(TEST_TRAINING_PROVIDERS_DEFAULT.data.length); }); - securityTrainingProviders.forEach(({ name, description, url, isEnabled }, index) => { + TEST_TRAINING_PROVIDERS_DEFAULT.data.forEach(({ name, description, isEnabled }, index) => { it(`shows the name for card ${index}`, () => { expect(findCards().at(index).text()).toContain(name); }); @@ -131,23 +155,76 @@ describe('TrainingProviderList component', () => { expect(findCards().at(index).text()).toContain(description); }); - it(`shows the learn more link for card ${index}`, () => { - expect(findLinks().at(index).attributes()).toEqual({ - target: '_blank', - href: url, - }); + it(`shows the learn more link for enabled card ${index}`, () => { + const learnMoreLink = findCards().at(index).find(GlLink); + const tempLogo = TEMP_PROVIDER_URLS[name]; + + if (tempLogo) { + expect(learnMoreLink.attributes()).toEqual({ + target: '_blank', + href: TEMP_PROVIDER_URLS[name], + }); + } else { + expect(learnMoreLink.exists()).toBe(false); + } }); it(`shows the toggle with the correct value for card ${index}`, () => { expect(findToggles().at(index).props('value')).toEqual(isEnabled); }); + it(`shows a radio button to select the provider as primary within card ${index}`, () => { + const primaryProviderRadioForCurrentCard = findPrimaryProviderRadios().at(index); + + // if the given provider is not enabled it should not be possible select it as primary + expect(primaryProviderRadioForCurrentCard.find('input').attributes('disabled')).toBe( + isEnabled ? undefined : 'disabled', + ); + + expect(primaryProviderRadioForCurrentCard.text()).toBe( + TrainingProviderList.i18n.primaryTraining, + ); + }); + + it('shows a info-tooltip that describes the purpose of a primary provider', () => { + const infoIcon = findPrimaryProviderRadios().at(index).find(GlIcon); + const tooltip = getBinding(infoIcon.element, 'gl-tooltip'); + + expect(infoIcon.props()).toMatchObject({ + name: 'information-o', + }); + expect(tooltip.value).toBe(TrainingProviderList.i18n.primaryTrainingDescription); + }); + it('does not show loader when query is populated', () => { expect(findLoader().exists()).toBe(false); }); }); }); + describe('provider logo', () => { + beforeEach(async () => { + wrapper.vm.$options.TEMP_PROVIDER_LOGOS = tempProviderLogos; + await waitForQueryToBeLoaded(); + }); + + const providerIndexArray = [0, 1]; + + it.each(providerIndexArray)('displays the correct width for provider %s', (provider) => { + expect(findLogos().at(provider).attributes('style')).toBe('width: 18px;'); + }); + + it.each(providerIndexArray)('has a11y decorative attribute for provider %s', (provider) => { + expect(findLogos().at(provider).attributes('role')).toBe('presentation'); + }); + + it.each(providerIndexArray)('renders the svg content for provider %s', (provider) => { + expect(findLogos().at(provider).html()).toContain( + tempProviderLogos[testProviderName[provider]].svg, + ); + }); + }); + describe('storing training provider settings', () => { beforeEach(async () => { jest.spyOn(apolloProvider.defaultClient, 'mutate'); @@ -157,26 +234,15 @@ describe('TrainingProviderList component', () => { await toggleFirstProvider(); }); - it.each` - loading | wait | desc - ${true} | ${false} | ${'enables loading of GlToggle when mutation is called'} - ${false} | ${true} | ${'disables loading of GlToggle when mutation is complete'} - `('$desc', async ({ loading, wait }) => { - if (wait) { - await waitForMutationToBeLoaded(); - } - expect(findFirstToggle().props('isLoading')).toBe(loading); - }); - it('calls mutation when toggle is changed', () => { expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith( expect.objectContaining({ mutation: configureSecurityTrainingProvidersMutation, variables: { input: { - providerId: textProviderIds[0], + providerId: testProviderIds[0], isEnabled: true, - isPrimary: false, + isPrimary: true, projectPath: testProjectPath, }, }, @@ -184,6 +250,20 @@ describe('TrainingProviderList component', () => { ); }); + it('returns an optimistic response when calling the mutation', () => { + const optimisticResponse = updateSecurityTrainingOptimisticResponse({ + id: TEST_TRAINING_PROVIDERS_DEFAULT.data[0].id, + isEnabled: true, + isPrimary: true, + }); + + expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith( + expect.objectContaining({ + optimisticResponse, + }), + ); + }); + it('dismisses the callout when the feature gets first enabled', async () => { // wait for configuration update mutation to complete await waitForMutationToBeLoaded(); @@ -237,13 +317,62 @@ describe('TrainingProviderList component', () => { // Once https://gitlab.com/gitlab-org/gitlab/-/issues/348985 and https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79492 // are merged this will be much easer to do and should be tackled then. expect(trackingSpy).toHaveBeenCalledWith(undefined, TRACK_TOGGLE_TRAINING_PROVIDER_ACTION, { - property: securityTrainingProviders[0].id, + property: TEST_TRAINING_PROVIDERS_DEFAULT.data[0].id, label: TRACK_TOGGLE_TRAINING_PROVIDER_LABEL, extra: { providerIsEnabled: true, }, }); }); + + it(`tracks when a provider's "Learn more" link is clicked`, () => { + const firstProviderLink = findLinks().at(0); + const [{ id: firstProviderId }] = TEST_TRAINING_PROVIDERS_DEFAULT.data; + + expect(trackingSpy).not.toHaveBeenCalled(); + + firstProviderLink.vm.$emit('click'); + + expect(trackingSpy).toHaveBeenCalledWith( + undefined, + TRACK_PROVIDER_LEARN_MORE_CLICK_ACTION, + { + label: TRACK_PROVIDER_LEARN_MORE_CLICK_LABEL, + property: firstProviderId, + }, + ); + }); + }); + }); + + describe('primary provider settings', () => { + it.each` + description | initialProviderData | expectedMutationInput + ${'sets the provider to be non-primary when it gets disabled'} | ${TEST_TRAINING_PROVIDERS_FIRST_ENABLED.response} | ${{ providerId: TEST_TRAINING_PROVIDERS_FIRST_ENABLED.data[0].id, isEnabled: false, isPrimary: false }} + ${'sets a provider to be primary when it is the only one enabled'} | ${TEST_TRAINING_PROVIDERS_ALL_DISABLED.response} | ${{ providerId: TEST_TRAINING_PROVIDERS_ALL_DISABLED.data[0].id, isEnabled: true, isPrimary: true }} + ${'sets the first other enabled provider to be primary when the primary one gets disabled'} | ${TEST_TRAINING_PROVIDERS_ALL_ENABLED.response} | ${{ providerId: TEST_TRAINING_PROVIDERS_ALL_ENABLED.data[1].id, isEnabled: true, isPrimary: true }} + `('$description', async ({ initialProviderData, expectedMutationInput }) => { + createApolloProvider({ + handlers: [ + [securityTrainingProvidersQuery, jest.fn().mockResolvedValue(initialProviderData)], + ], + }); + jest.spyOn(apolloProvider.defaultClient, 'mutate'); + createComponent(); + + await waitForQueryToBeLoaded(); + await toggleFirstProvider(); + + expect(apolloProvider.defaultClient.mutate).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + variables: { + input: expect.objectContaining({ + ...expectedMutationInput, + }), + }, + }), + ); }); }); |