diff options
Diffstat (limited to 'spec/frontend')
17 files changed, 490 insertions, 159 deletions
diff --git a/spec/frontend/import/details/components/import_details_app_spec.js b/spec/frontend/import/details/components/import_details_app_spec.js index 178ce071de0..6e748a57a1d 100644 --- a/spec/frontend/import/details/components/import_details_app_spec.js +++ b/spec/frontend/import/details/components/import_details_app_spec.js @@ -1,16 +1,11 @@ import { shallowMount } from '@vue/test-utils'; import ImportDetailsApp from '~/import/details/components/import_details_app.vue'; -import { mockProject } from '../mock_data'; describe('Import details app', () => { let wrapper; const createComponent = () => { - wrapper = shallowMount(ImportDetailsApp, { - propsData: { - project: mockProject, - }, - }); + wrapper = shallowMount(ImportDetailsApp); }; describe('template', () => { diff --git a/spec/frontend/import/details/components/import_details_table_spec.js b/spec/frontend/import/details/components/import_details_table_spec.js index 43c9a66c00a..aee8573eb02 100644 --- a/spec/frontend/import/details/components/import_details_table_spec.js +++ b/spec/frontend/import/details/components/import_details_table_spec.js @@ -1,17 +1,30 @@ import { mount, shallowMount } from '@vue/test-utils'; -import { GlEmptyState, GlTable } from '@gitlab/ui'; +import { GlEmptyState, GlLoadingIcon, GlTable } from '@gitlab/ui'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import { HTTP_STATUS_OK, HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status'; +import { createAlert } from '~/alert'; +import waitForPromises from 'helpers/wait_for_promises'; import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue'; import ImportDetailsTable from '~/import/details/components/import_details_table.vue'; +import { mockImportFailures, mockHeaders } from '../mock_data'; + +jest.mock('~/alert'); describe('Import details table', () => { let wrapper; + let mock; - const createComponent = ({ mountFn = shallowMount } = {}) => { - wrapper = mountFn(ImportDetailsTable); + const createComponent = ({ mountFn = shallowMount, provide = {} } = {}) => { + wrapper = mountFn(ImportDetailsTable, { + provide, + }); }; + const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findGlTable = () => wrapper.findComponent(GlTable); + const findGlTableRows = () => findGlTable().find('tbody').findAll('tr'); const findGlEmptyState = () => findGlTable().findComponent(GlEmptyState); const findPaginationBar = () => wrapper.findComponent(PaginationBar); @@ -20,7 +33,7 @@ describe('Import details table', () => { it('renders table with empty state', () => { createComponent({ mountFn: mount }); - expect(findGlEmptyState().exists()).toBe(true); + expect(findGlEmptyState().text()).toBe(ImportDetailsTable.i18n.emptyText); }); it('does not render pagination', () => { @@ -30,4 +43,71 @@ describe('Import details table', () => { }); }); }); + + describe('fetching failures from API', () => { + const mockImportFailuresPath = '/failures'; + + beforeEach(() => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('when request is successful', () => { + beforeEach(() => { + mock.onGet(mockImportFailuresPath).reply(HTTP_STATUS_OK, mockImportFailures, mockHeaders); + + createComponent({ + mountFn: mount, + provide: { + failuresPath: mockImportFailuresPath, + }, + }); + }); + + it('renders loading icon', () => { + expect(findGlLoadingIcon().exists()).toBe(true); + }); + + it('does not render loading icon after fetch', async () => { + await waitForPromises(); + + expect(findGlLoadingIcon().exists()).toBe(false); + }); + + it('sets items and pagination info', async () => { + await waitForPromises(); + + expect(findGlTableRows().length).toBe(mockImportFailures.length); + expect(findPaginationBar().props('pageInfo')).toMatchObject({ + page: mockHeaders['x-page'], + perPage: mockHeaders['x-per-page'], + total: mockHeaders['x-total'], + totalPages: mockHeaders['x-total-pages'], + }); + }); + }); + + describe('when request fails', () => { + beforeEach(() => { + mock.onGet(mockImportFailuresPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR); + + createComponent({ + provide: { + failuresPath: mockImportFailuresPath, + }, + }); + }); + + it('displays an error', async () => { + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith({ + message: ImportDetailsTable.i18n.fetchErrorMessage, + }); + }); + }); + }); }); diff --git a/spec/frontend/import/details/mock_data.js b/spec/frontend/import/details/mock_data.js index 514fb3a923d..a8e0e53ed2b 100644 --- a/spec/frontend/import/details/mock_data.js +++ b/spec/frontend/import/details/mock_data.js @@ -1,31 +1,53 @@ -export const mockProject = { - id: 26, - name: 'acl', - fullPath: '/root/acl', - fullName: 'Administrator / acl', - refsUrl: '/root/acl/refs', - importSource: 'namespace/acl', - importStatus: 'finished', - humanImportStatusName: 'finished', - providerLink: 'https://github.com/namespace/acl', - relationType: null, - stats: { - fetched: { - note: 1, - issue: 2, - label: 5, - collaborator: 2, - pullRequest: 1, - pullRequestMergedBy: 1, +export const mockImportFailures = [ + { + type: 'pull_request', + title: 'Add one cool feature', + url: 'https://github.com/USER/REPO/pull/2', + details: { + exception_class: 'ActiveRecord::RecordInvalid', + exception_message: 'Record invalid', + source: 'Gitlab::GithubImport::Importer::PullRequestImporter', + github_identifiers: { + iid: 2, + issuable_type: 'MergeRequest', + object_type: 'pull_request', + }, }, - imported: { - note: 1, - issue: 2, - label: 6, - collaborator: 3, - pullRequest: 1, - pullRequestMergedBy: 1, - pullRequestReviewRequest: 1, + }, + { + type: 'pull_request', + title: 'Add another awesome feature', + url: 'https://github.com/USER/REPO/pull/3', + details: { + exception_class: 'ActiveRecord::RecordInvalid', + exception_message: 'Record invalid', + source: 'Gitlab::GithubImport::Importer::PullRequestImporter', + github_identifiers: { + iid: 3, + issuable_type: 'MergeRequest', + object_type: 'pull_request', + }, + }, + }, + { + type: 'lfs_object', + title: '3a9257fae9e86faee27d7208cb55e086f18e6f29f48c430bfbc26d42eb', + url: null, + details: { + exception_class: 'NameError', + exception_message: 'some message', + source: 'Gitlab::GithubImport::Importer::LfsObjectImporter', + github_identifiers: { + oid: '3a9257fae9e86faee27d7208cb55e086f18e6f29f48c430bfbc26d42eb', + size: 2473979, + }, }, }, +]; + +export const mockHeaders = { + 'x-page': 1, + 'x-per-page': 20, + 'x-total': 3, + 'x-total-pages': 1, }; diff --git a/spec/frontend/import_entities/components/import_status_spec.js b/spec/frontend/import_entities/components/import_status_spec.js index 8e569e3ec85..4c6fee35389 100644 --- a/spec/frontend/import_entities/components/import_status_spec.js +++ b/spec/frontend/import_entities/components/import_status_spec.js @@ -155,6 +155,7 @@ describe('Import entities status component', () => { describe('show details link', () => { const mockDetailsPath = 'details_path'; + const mockProjectId = 29; const mockCompleteStats = { fetched: { ...mockStatItems }, imported: { ...mockStatItems }, @@ -180,6 +181,7 @@ describe('Import entities status component', () => { beforeEach(() => { createComponent( { + projectId: mockProjectId, status: STATUSES.FINISHED, stats: partialImport ? mockIncompleteStats : mockCompleteStats, }, @@ -195,7 +197,9 @@ describe('Import entities status component', () => { it(`${expectLink ? 'renders' : 'does not render'} import details link`, () => { expect(findGlLink().exists()).toBe(expectLink); if (expectLink) { - expect(findGlLink().attributes('href')).toBe(mockDetailsPath); + expect(findGlLink().attributes('href')).toBe( + `${mockDetailsPath}?project_id=${mockProjectId}`, + ); } }); }, diff --git a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js index 8e73f76382a..57e232a4c46 100644 --- a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js +++ b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js @@ -40,6 +40,7 @@ describe('ProviderRepoTableRow', () => { const findImportButton = () => findButton('Import'); const findReimportButton = () => findButton('Re-import'); const findGroupDropdown = () => wrapper.findComponent(ImportGroupDropdown); + const findImportStatus = () => wrapper.findComponent(ImportStatus); const findCancelButton = () => { const buttons = wrapper @@ -81,7 +82,7 @@ describe('ProviderRepoTableRow', () => { }); it('renders empty import status', () => { - expect(wrapper.findComponent(ImportStatus).props().status).toBe(STATUSES.NONE); + expect(findImportStatus().props().status).toBe(STATUSES.NONE); }); it('renders a group namespace select', () => { @@ -198,9 +199,7 @@ describe('ProviderRepoTableRow', () => { }); it('renders proper import status', () => { - expect(wrapper.findComponent(ImportStatus).props().status).toBe( - repo.importedProject.importStatus, - ); + expect(findImportStatus().props().status).toBe(repo.importedProject.importStatus); }); it('does not render a namespace select', () => { @@ -236,8 +235,11 @@ describe('ProviderRepoTableRow', () => { }); }); - it('passes stats to import status component', () => { - expect(wrapper.findComponent(ImportStatus).props().stats).toBe(FAKE_STATS); + it('passes props to import status component', () => { + expect(findImportStatus().props()).toMatchObject({ + projectId: repo.importedProject.id, + stats: FAKE_STATS, + }); }); }); diff --git a/spec/frontend/jira_connect/subscriptions/components/app_spec.js b/spec/frontend/jira_connect/subscriptions/components/app_spec.js index 15a7e2e4ea0..26a9d07321c 100644 --- a/spec/frontend/jira_connect/subscriptions/components/app_spec.js +++ b/spec/frontend/jira_connect/subscriptions/components/app_spec.js @@ -50,18 +50,6 @@ describe('JiraConnectApp', () => { jest.spyOn(AccessorUtilities, 'canUseCrypto').mockReturnValue(true); }); - it('renders UserLink component', () => { - createComponent(); - - const userLink = findUserLink(); - expect(userLink.exists()).toBe(true); - expect(userLink.props()).toEqual({ - hasSubscriptions: true, - user: null, - userSignedIn: false, - }); - }); - it('renders only Jira Connect app', () => { createComponent(); @@ -79,12 +67,12 @@ describe('JiraConnectApp', () => { }); describe.each` - scenario | currentUser | shouldRenderSignInPage | shouldRenderSubscriptionsPage - ${'user is not signed in'} | ${undefined} | ${true} | ${false} - ${'user is signed in'} | ${mockCurrentUser} | ${false} | ${true} + scenario | currentUser | expectUserLink | expectSignInPage | expectSubscriptionsPage + ${'user is not signed in'} | ${undefined} | ${false} | ${true} | ${false} + ${'user is signed in'} | ${mockCurrentUser} | ${true} | ${false} | ${true} `( 'when $scenario', - ({ currentUser, shouldRenderSignInPage, shouldRenderSubscriptionsPage }) => { + ({ currentUser, expectUserLink, expectSignInPage, expectSubscriptionsPage }) => { beforeEach(() => { createComponent({ initialState: { @@ -93,18 +81,23 @@ describe('JiraConnectApp', () => { }); }); - it(`${shouldRenderSignInPage ? 'renders' : 'does not render'} sign in page`, () => { - expect(findSignInPage().isVisible()).toBe(shouldRenderSignInPage); - if (shouldRenderSignInPage) { + it(`${expectUserLink ? 'renders' : 'does not render'} user link`, () => { + expect(findUserLink().exists()).toBe(expectUserLink); + if (expectUserLink) { + expect(findUserLink().props('user')).toBe(mockCurrentUser); + } + }); + + it(`${expectSignInPage ? 'renders' : 'does not render'} sign in page`, () => { + expect(findSignInPage().isVisible()).toBe(expectSignInPage); + if (expectSignInPage) { expect(findSignInPage().props('hasSubscriptions')).toBe(true); } }); - it(`${ - shouldRenderSubscriptionsPage ? 'renders' : 'does not render' - } subscriptions page`, () => { - expect(findSubscriptionsPage().exists()).toBe(shouldRenderSubscriptionsPage); - if (shouldRenderSubscriptionsPage) { + it(`${expectSubscriptionsPage ? 'renders' : 'does not render'} subscriptions page`, () => { + expect(findSubscriptionsPage().exists()).toBe(expectSubscriptionsPage); + if (expectSubscriptionsPage) { expect(findSubscriptionsPage().props('hasSubscriptions')).toBe(true); } }); diff --git a/spec/frontend/jira_connect/subscriptions/components/user_link_spec.js b/spec/frontend/jira_connect/subscriptions/components/user_link_spec.js index a10e352ed85..77bc1d2004c 100644 --- a/spec/frontend/jira_connect/subscriptions/components/user_link_spec.js +++ b/spec/frontend/jira_connect/subscriptions/components/user_link_spec.js @@ -1,6 +1,5 @@ import { GlSprintf } from '@gitlab/ui'; import UserLink from '~/jira_connect/subscriptions/components/user_link.vue'; -import SignInOauthButton from '~/jira_connect/subscriptions/components/sign_in_oauth_button.vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; @@ -13,42 +12,18 @@ describe('UserLink', () => { provide, stubs: { GlSprintf, - SignInOauthButton, }, }); }; const findGitlabUserLink = () => wrapper.findByTestId('gitlab-user-link'); const findSprintf = () => wrapper.findComponent(GlSprintf); - const findOauthButton = () => wrapper.findComponent(SignInOauthButton); - describe.each` - userSignedIn | hasSubscriptions | expectGlSprintf | expectOauthButton - ${false} | ${false} | ${false} | ${false} - ${false} | ${true} | ${false} | ${true} - ${true} | ${false} | ${true} | ${false} - ${true} | ${true} | ${true} | ${false} - `( - 'when `userSignedIn` is $userSignedIn, `hasSubscriptions` is $hasSubscriptions', - ({ userSignedIn, hasSubscriptions, expectGlSprintf, expectOauthButton }) => { - it('renders template correctly', () => { - createComponent( - { - userSignedIn, - hasSubscriptions, - }, - { - provide: { - oauthMetadata: {}, - }, - }, - ); + it('renders template correctly', () => { + createComponent(); - expect(findSprintf().exists()).toBe(expectGlSprintf); - expect(findOauthButton().exists()).toBe(expectOauthButton); - }); - }, - ); + expect(findSprintf().exists()).toBe(true); + }); describe('gitlab user link', () => { describe.each` @@ -62,14 +37,7 @@ describe('UserLink', () => { beforeEach(() => { window.gon = { current_username, relative_root_url: '' }; - createComponent( - { - userSignedIn: true, - hasSubscriptions: true, - user, - }, - { provide: { gitlabUserPath } }, - ); + createComponent({ user }, { provide: { gitlabUserPath } }); }); it(`sets href to ${expectedUserLink}`, () => { diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js index ce6861ff460..93663319e6d 100644 --- a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js +++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js @@ -1,7 +1,6 @@ import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import SetupInstructions from '~/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue'; import SignInGitlabMultiversion from '~/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue'; import SignInOauthButton from '~/jira_connect/subscriptions/components/sign_in_oauth_button.vue'; import VersionSelectForm from '~/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue'; @@ -23,7 +22,6 @@ describe('SignInGitlabMultiversion', () => { const mockBasePath = 'gitlab.mycompany.com'; - const findSetupInstructions = () => wrapper.findComponent(SetupInstructions); const findSignInOauthButton = () => wrapper.findComponent(SignInOauthButton); const findVersionSelectForm = () => wrapper.findComponent(VersionSelectForm); const findSubtitle = () => wrapper.findByTestId('subtitle'); @@ -68,28 +66,13 @@ describe('SignInGitlabMultiversion', () => { expect(findSubtitle().text()).toBe(SignInGitlabMultiversion.i18n.signInSubtitle); }); - it('renders setup instructions', () => { - expect(findSetupInstructions().exists()).toBe(true); + it('renders sign in button', () => { + expect(findSignInOauthButton().props('gitlabBasePath')).toBe(mockBasePath); }); it('calls setApiBaseURL with correct params', () => { expect(setApiBaseURL).toHaveBeenCalledWith(mockBasePath); }); - - describe('when SetupInstructions emits `next` event', () => { - beforeEach(async () => { - findSetupInstructions().vm.$emit('next'); - await nextTick(); - }); - - it('renders sign in button', () => { - expect(findSignInOauthButton().props('gitlabBasePath')).toBe(mockBasePath); - }); - - it('hides setup instructions', () => { - expect(findSetupInstructions().exists()).toBe(false); - }); - }); }); describe('when on GitLab.com', () => { @@ -98,10 +81,6 @@ describe('SignInGitlabMultiversion', () => { createComponent(); }); - it('does not render setup instructions', () => { - expect(findSetupInstructions().exists()).toBe(false); - }); - it('renders sign in button', () => { expect(findSignInOauthButton().props('gitlabBasePath')).toBe(GITLAB_COM_BASE_PATH); }); diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions_spec.js index 5496cf008c5..40ea6058c70 100644 --- a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions_spec.js +++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions_spec.js @@ -7,8 +7,9 @@ import SetupInstructions from '~/jira_connect/subscriptions/pages/sign_in/sign_i describe('SetupInstructions', () => { let wrapper; - const findGlButton = () => wrapper.findComponent(GlButton); const findGlLink = () => wrapper.findComponent(GlLink); + const findBackButton = () => wrapper.findAllComponents(GlButton).at(0); + const findNextButton = () => wrapper.findAllComponents(GlButton).at(1); const createComponent = () => { wrapper = shallowMount(SetupInstructions); @@ -23,12 +24,23 @@ describe('SetupInstructions', () => { expect(findGlLink().attributes('href')).toBe(OAUTH_SELF_MANAGED_DOC_LINK); }); - describe('when button is clicked', () => { + describe('when "Next" button is clicked', () => { it('emits "next" event', () => { expect(wrapper.emitted('next')).toBeUndefined(); - findGlButton().vm.$emit('click'); + findNextButton().vm.$emit('click'); expect(wrapper.emitted('next')).toHaveLength(1); + expect(wrapper.emitted('back')).toBeUndefined(); + }); + }); + + describe('when "Back" button is clicked', () => { + it('emits "back" event', () => { + expect(wrapper.emitted('back')).toBeUndefined(); + findBackButton().vm.$emit('click'); + + expect(wrapper.emitted('back')).toHaveLength(1); + expect(wrapper.emitted('next')).toBeUndefined(); }); }); }); diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form_spec.js index 428aa1d734b..2a08547b048 100644 --- a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form_spec.js +++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form_spec.js @@ -1,8 +1,9 @@ import { GlFormInput, GlFormRadioGroup, GlForm } from '@gitlab/ui'; -import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import VersionSelectForm from '~/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue'; +import SelfManagedAlert from '~/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/self_managed_alert.vue'; +import SetupInstructions from '~/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue'; describe('VersionSelectForm', () => { let wrapper; @@ -10,26 +11,53 @@ describe('VersionSelectForm', () => { const findFormRadioGroup = () => wrapper.findComponent(GlFormRadioGroup); const findForm = () => wrapper.findComponent(GlForm); const findInput = () => wrapper.findComponent(GlFormInput); + const findSelfManagedAlert = () => wrapper.findComponent(SelfManagedAlert); + const findSetupInstructions = () => wrapper.findComponent(SetupInstructions); + const findBackButton = () => wrapper.findByTestId('back-button'); + const findSubmitButton = () => wrapper.findByTestId('submit-button'); const submitForm = () => findForm().vm.$emit('submit', new Event('submit')); + const expectSelfManagedFlowAtStep = (step) => { + // step 0 is for SaaS which doesn't have any of the self-managed elements + const expectSelfManagedAlert = step === 1; + const expectSetupInstructions = step === 2; + const expectSelfManagedInput = step === 3; + + it(`${expectSelfManagedAlert ? 'renders' : 'does not render'} self-managed alert`, () => { + expect(findSelfManagedAlert().exists()).toBe(expectSelfManagedAlert); + }); + + it(`${expectSetupInstructions ? 'renders' : 'does not render'} setup instructions`, () => { + expect(findSetupInstructions().exists()).toBe(expectSetupInstructions); + }); + + it(`${ + expectSelfManagedInput ? 'renders' : 'does not render' + } self-managed instance URL input`, () => { + expect(findInput().exists()).toBe(expectSelfManagedInput); + }); + }; + const createComponent = () => { wrapper = shallowMountExtended(VersionSelectForm); }; - describe('default state', () => { + describe('when "SaaS" radio option is selected (default state)', () => { beforeEach(() => { createComponent(); }); - it('selects saas radio option by default', () => { + it('selects "saas" radio option by default', () => { expect(findFormRadioGroup().vm.$attrs.checked).toBe(VersionSelectForm.radioOptions.saas); }); - it('does not render instance input', () => { - expect(findInput().exists()).toBe(false); + it('renders submit button as "Save"', () => { + expect(findSubmitButton().text()).toBe(VersionSelectForm.i18n.buttonSave); }); + expectSelfManagedFlowAtStep(0); + describe('when form is submitted', () => { it('emits "submit" event with gitlab.com as the payload', () => { submitForm(); @@ -39,26 +67,61 @@ describe('VersionSelectForm', () => { }); }); - describe('when "self-managed" radio option is selected', () => { - beforeEach(async () => { + describe('when "self-managed" radio option is selected (step 1 of 3)', () => { + beforeEach(() => { createComponent(); findFormRadioGroup().vm.$emit('input', VersionSelectForm.radioOptions.selfManaged); - await nextTick(); }); - it('reveals the self-managed input field', () => { - expect(findInput().exists()).toBe(true); + it('renders submit button as "Next"', () => { + expect(findSubmitButton().text()).toBe(VersionSelectForm.i18n.buttonNext); }); - describe('when form is submitted', () => { - it('emits "submit" event with the input field value as the payload', () => { - const mockInstanceUrl = 'https://gitlab.example.com'; + expectSelfManagedFlowAtStep(1); - findInput().vm.$emit('input', mockInstanceUrl); + describe('when user clicks "Next" button (next to step 2 of 3)', () => { + beforeEach(() => { submitForm(); + }); + + expectSelfManagedFlowAtStep(2); + + describe('when SetupInstructions emits `next` event (next to step 3 of 3)', () => { + beforeEach(() => { + findSetupInstructions().vm.$emit('next'); + }); + + expectSelfManagedFlowAtStep(3); + + describe('when form is submitted', () => { + it('emits "submit" event with the input field value as the payload', () => { + const mockInstanceUrl = 'https://gitlab.example.com'; + + findInput().vm.$emit('input', mockInstanceUrl); + submitForm(); + + expect(wrapper.emitted('submit')[0][0]).toBe(mockInstanceUrl); + }); + }); + + describe('when back button is clicked', () => { + beforeEach(() => { + findBackButton().vm.$emit('click', { + preventDefault: jest.fn(), // preventDefault is needed to prevent form submission + }); + }); + + expectSelfManagedFlowAtStep(1); + }); + }); + + describe('when SetupInstructions emits `back` event (back to step 1 of 3)', () => { + beforeEach(() => { + findSetupInstructions().vm.$emit('back'); + }); - expect(wrapper.emitted('submit')[0][0]).toBe(mockInstanceUrl); + expectSelfManagedFlowAtStep(1); }); }); }); diff --git a/spec/frontend/projects/commit_box/info/init_details_button_spec.js b/spec/frontend/projects/commit_box/info/init_details_button_spec.js new file mode 100644 index 00000000000..8aaba31e23e --- /dev/null +++ b/spec/frontend/projects/commit_box/info/init_details_button_spec.js @@ -0,0 +1,32 @@ +import { setHTMLFixture } from 'helpers/fixtures'; +import { initDetailsButton } from '~/projects/commit_box/info/init_details_button'; + +const htmlFixture = ` + <span> + <a href="#" class="js-details-expand">Expand</a> + <span class="js-details-content hide">Some branch</span> + </span>`; + +describe('~/projects/commit_box/info/init_details_button', () => { + const findExpandButton = () => document.querySelector('.js-details-expand'); + const findContent = () => document.querySelector('.js-details-content'); + + beforeEach(() => { + setHTMLFixture(htmlFixture); + initDetailsButton(); + }); + + describe('when clicking the expand button', () => { + it('renders the content by removing the `hide` class', () => { + expect(findContent().classList).toContain('hide'); + findExpandButton().click(); + expect(findContent().classList).not.toContain('hide'); + }); + + it('hides the expand button by adding the `gl-display-none` class', () => { + expect(findExpandButton().classList).not.toContain('gl-display-none'); + findExpandButton().click(); + expect(findExpandButton().classList).toContain('gl-display-none'); + }); + }); +}); diff --git a/spec/frontend/projects/commit_box/info/load_branches_spec.js b/spec/frontend/projects/commit_box/info/load_branches_spec.js index e49d92188ed..b00a6378e07 100644 --- a/spec/frontend/projects/commit_box/info/load_branches_spec.js +++ b/spec/frontend/projects/commit_box/info/load_branches_spec.js @@ -4,6 +4,9 @@ import { setHTMLFixture } from 'helpers/fixtures'; import waitForPromises from 'helpers/wait_for_promises'; import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import { loadBranches } from '~/projects/commit_box/info/load_branches'; +import { initDetailsButton } from '~/projects/commit_box/info/init_details_button'; + +jest.mock('~/projects/commit_box/info/init_details_button'); const mockCommitPath = '/commit/abcd/branches'; const mockBranchesRes = @@ -26,6 +29,13 @@ describe('~/projects/commit_box/info/load_branches', () => { mock.onGet(mockCommitPath).reply(HTTP_STATUS_OK, mockBranchesRes); }); + it('initializes the details button', async () => { + loadBranches(); + await waitForPromises(); + + expect(initDetailsButton).toHaveBeenCalled(); + }); + it('loads and renders branches info', async () => { loadBranches(); await waitForPromises(); diff --git a/spec/frontend/search/sidebar/components/scope_new_navigation_spec.js b/spec/frontend/search/sidebar/components/scope_new_navigation_spec.js index c7dd6e931b1..5207665f883 100644 --- a/spec/frontend/search/sidebar/components/scope_new_navigation_spec.js +++ b/spec/frontend/search/sidebar/components/scope_new_navigation_spec.js @@ -1,4 +1,4 @@ -import { shallowMount } from '@vue/test-utils'; +import { mount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import ScopeNewNavigation from '~/search/sidebar/components/scope_new_navigation.vue'; @@ -30,7 +30,7 @@ describe('ScopeNewNavigation', () => { getters: getterSpies, }); - wrapper = shallowMount(ScopeNewNavigation, { + wrapper = mount(ScopeNewNavigation, { store, stubs: { NavItem, diff --git a/spec/frontend/super_sidebar/components/nav_item_link_spec.js b/spec/frontend/super_sidebar/components/nav_item_link_spec.js new file mode 100644 index 00000000000..5cc1bd01d0f --- /dev/null +++ b/spec/frontend/super_sidebar/components/nav_item_link_spec.js @@ -0,0 +1,37 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import NavItemLink from '~/super_sidebar/components/nav_item_link.vue'; + +describe('NavItemLink component', () => { + let wrapper; + + const createWrapper = (item) => { + wrapper = shallowMountExtended(NavItemLink, { + propsData: { + item, + }, + }); + }; + + describe('when `item` has `is_active` set to `false`', () => { + it('renders an anchor tag without active CSS class and `aria-current` attribute', () => { + createWrapper({ title: 'foo', link: '/foo', is_active: false }); + + expect(wrapper.attributes()).toEqual({ + href: '/foo', + class: '', + }); + }); + }); + + describe('when `item` has `is_active` set to `true`', () => { + it('renders an anchor tag with active CSS class and `aria-current="page"`', () => { + createWrapper({ title: 'foo', link: '/foo', is_active: true }); + + expect(wrapper.attributes()).toEqual({ + href: '/foo', + class: 'gl-bg-t-gray-a-08', + 'aria-current': 'page', + }); + }); + }); +}); diff --git a/spec/frontend/super_sidebar/components/nav_item_router_link_spec.js b/spec/frontend/super_sidebar/components/nav_item_router_link_spec.js new file mode 100644 index 00000000000..a7ca56325fe --- /dev/null +++ b/spec/frontend/super_sidebar/components/nav_item_router_link_spec.js @@ -0,0 +1,56 @@ +import { RouterLinkStub } from '@vue/test-utils'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import NavItemRouterLink from '~/super_sidebar/components/nav_item_router_link.vue'; + +describe('NavItemRouterLink component', () => { + let wrapper; + + const createWrapper = ({ item, routerLinkSlotProps = {} }) => { + wrapper = mountExtended(NavItemRouterLink, { + propsData: { + item, + }, + stubs: { + RouterLink: { + ...RouterLinkStub, + render() { + const children = this.$scopedSlots.default({ + href: '/foo', + isActive: false, + navigate: jest.fn(), + ...routerLinkSlotProps, + }); + return children; + }, + }, + }, + }); + }; + + describe('when `RouterLink` is not active', () => { + it('renders an anchor tag without active CSS class and `aria-current` attribute', () => { + createWrapper({ item: { title: 'foo', to: { name: 'foo' } } }); + + expect(wrapper.attributes()).toEqual({ + href: '/foo', + custom: '', + }); + }); + }); + + describe('when `RouterLink` is active', () => { + it('renders an anchor tag with active CSS class and `aria-current="page"`', () => { + createWrapper({ + item: { title: 'foo', to: { name: 'foo' } }, + routerLinkSlotProps: { isActive: true }, + }); + + expect(wrapper.findComponent(RouterLinkStub).props('activeClass')).toBe('gl-bg-t-gray-a-08'); + expect(wrapper.attributes()).toEqual({ + href: '/foo', + 'aria-current': 'page', + custom: '', + }); + }); + }); +}); diff --git a/spec/frontend/super_sidebar/components/nav_item_spec.js b/spec/frontend/super_sidebar/components/nav_item_spec.js index 1714a4c3a4e..43b3f14f2f5 100644 --- a/spec/frontend/super_sidebar/components/nav_item_spec.js +++ b/spec/frontend/super_sidebar/components/nav_item_spec.js @@ -1,6 +1,9 @@ import { GlBadge } from '@gitlab/ui'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { RouterLinkStub } from '@vue/test-utils'; +import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper'; import NavItem from '~/super_sidebar/components/nav_item.vue'; +import NavItemRouterLink from '~/super_sidebar/components/nav_item_router_link.vue'; +import NavItemLink from '~/super_sidebar/components/nav_item_link.vue'; import { CLICK_MENU_ITEM_ACTION, TRACKING_UNKNOWN_ID, @@ -12,19 +15,36 @@ describe('NavItem component', () => { const findLink = () => wrapper.findByTestId('nav-item-link'); const findPill = () => wrapper.findComponent(GlBadge); - const createWrapper = (item, props = {}, provide = {}) => { - wrapper = shallowMountExtended(NavItem, { + const findNavItemRouterLink = () => extendedWrapper(wrapper.findComponent(NavItemRouterLink)); + const findNavItemLink = () => extendedWrapper(wrapper.findComponent(NavItemLink)); + + const createWrapper = ({ item, props = {}, provide = {}, routerLinkSlotProps = {} }) => { + wrapper = mountExtended(NavItem, { propsData: { item, ...props, }, provide, + stubs: { + RouterLink: { + ...RouterLinkStub, + render() { + const children = this.$scopedSlots.default({ + href: '/foo', + isActive: false, + navigate: jest.fn(), + ...routerLinkSlotProps, + }); + return children; + }, + }, + }, }); }; describe('pills', () => { it.each([0, 5, 3.4, 'foo', '10%'])('item with pill_data `%p` renders a pill', (pillCount) => { - createWrapper({ title: 'Foo', pill_count: pillCount }); + createWrapper({ item: { title: 'Foo', pill_count: pillCount } }); expect(findPill().text()).toEqual(pillCount.toString()); }); @@ -32,7 +52,7 @@ describe('NavItem component', () => { it.each([null, undefined, false, true, '', NaN, Number.POSITIVE_INFINITY])( 'item with pill_data `%p` renders no pill', (pillCount) => { - createWrapper({ title: 'Foo', pill_count: pillCount }); + createWrapper({ item: { title: 'Foo', pill_count: pillCount } }); expect(findPill().exists()).toEqual(false); }, @@ -41,21 +61,21 @@ describe('NavItem component', () => { it('applies custom link classes', () => { const customClass = 'customClass'; - createWrapper( - { title: 'Foo' }, - { + createWrapper({ + item: { title: 'Foo' }, + props: { linkClasses: { [customClass]: true, }, }, - ); + }); expect(findLink().attributes('class')).toContain(customClass); }); it('applies custom classes set in the backend', () => { const customClass = 'customBackendClass'; - createWrapper({ title: 'Foo', link_classes: customClass }); + createWrapper({ item: { title: 'Foo', link_classes: customClass } }); expect(findLink().attributes('class')).toContain(customClass); }); @@ -70,7 +90,7 @@ describe('NavItem component', () => { `( 'adds appropriate data tracking labels for id=$id and panelType=$panelType', ({ id, eventLabel, panelType, eventProperty, eventExtra }) => { - createWrapper({ title: 'Foo', id }, {}, { panelType }); + createWrapper({ item: { title: 'Foo', id }, props: {}, provide: { panelType } }); expect(findLink().attributes('data-track-action')).toBe(CLICK_MENU_ITEM_ACTION); expect(findLink().attributes('data-track-label')).toBe(eventLabel); @@ -79,4 +99,51 @@ describe('NavItem component', () => { }, ); }); + + describe('when `item` prop has `to` attribute', () => { + describe('when `RouterLink` is not active', () => { + it('renders `NavItemRouterLink` with active indicator hidden', () => { + createWrapper({ item: { title: 'Foo', to: { name: 'foo' } } }); + + expect(findNavItemRouterLink().findByTestId('active-indicator').classes()).toContain( + 'gl-bg-transparent', + ); + }); + }); + + describe('when `RouterLink` is active', () => { + it('renders `NavItemRouterLink` with active indicator shown', () => { + createWrapper({ + item: { title: 'Foo', to: { name: 'foo' } }, + routerLinkSlotProps: { isActive: true }, + }); + + expect(findNavItemRouterLink().findByTestId('active-indicator').classes()).toContain( + 'gl-bg-blue-500', + ); + }); + }); + }); + + describe('when `item` prop has `link` attribute', () => { + describe('when `item` has `is_active` set to `false`', () => { + it('renders `NavItemLink` with active indicator hidden', () => { + createWrapper({ item: { title: 'Foo', link: '/foo', is_active: false } }); + + expect(findNavItemLink().findByTestId('active-indicator').classes()).toContain( + 'gl-bg-transparent', + ); + }); + }); + + describe('when `item` has `is_active` set to `true`', () => { + it('renders `NavItemLink` with active indicator shown', () => { + createWrapper({ item: { title: 'Foo', link: '/foo', is_active: true } }); + + expect(findNavItemLink().findByTestId('active-indicator').classes()).toContain( + 'gl-bg-blue-500', + ); + }); + }); + }); }); diff --git a/spec/frontend/super_sidebar/utils_spec.js b/spec/frontend/super_sidebar/utils_spec.js index d2984254dee..8c8673ddbc4 100644 --- a/spec/frontend/super_sidebar/utils_spec.js +++ b/spec/frontend/super_sidebar/utils_spec.js @@ -2,6 +2,7 @@ import { getTopFrequentItems, trackContextAccess, formatContextSwitcherItems, + ariaCurrent, } from '~/super_sidebar/utils'; import { useLocalStorageSpy } from 'helpers/local_storage_helper'; import AccessorUtilities from '~/lib/utils/accessor'; @@ -157,4 +158,14 @@ describe('Super sidebar utils spec', () => { ]); }); }); + + describe('ariaCurrent', () => { + it.each` + isActive | expected + ${true} | ${'page'} + ${false} | ${null} + `('returns `$expected` when `isActive` is `$isActive`', ({ isActive, expected }) => { + expect(ariaCurrent(isActive)).toBe(expected); + }); + }); }); |