diff options
Diffstat (limited to 'spec/frontend/import_entities/import_groups/components')
4 files changed, 193 insertions, 198 deletions
diff --git a/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js index 60f0780fdb3..cd56f573011 100644 --- a/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js +++ b/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js @@ -1,8 +1,6 @@ import { GlButton, GlIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import { STATUSES } from '~/import_entities/constants'; import ImportActionsCell from '~/import_entities/import_groups/components/import_actions_cell.vue'; -import { generateFakeEntry } from '../graphql/fixtures'; describe('import actions cell', () => { let wrapper; @@ -10,7 +8,9 @@ describe('import actions cell', () => { const createComponent = (props) => { wrapper = shallowMount(ImportActionsCell, { propsData: { - groupPathRegex: /^[a-zA-Z]+$/, + isFinished: false, + isAvailableForImport: false, + isInvalid: false, ...props, }, }); @@ -20,10 +20,9 @@ describe('import actions cell', () => { wrapper.destroy(); }); - describe('when import status is NONE', () => { + describe('when group is available for import', () => { beforeEach(() => { - const group = generateFakeEntry({ id: 1, status: STATUSES.NONE }); - createComponent({ group }); + createComponent({ isAvailableForImport: true }); }); it('renders import button', () => { @@ -37,10 +36,9 @@ describe('import actions cell', () => { }); }); - describe('when import status is FINISHED', () => { + describe('when group is finished', () => { beforeEach(() => { - const group = generateFakeEntry({ id: 1, status: STATUSES.FINISHED }); - createComponent({ group }); + createComponent({ isAvailableForImport: true, isFinished: true }); }); it('renders re-import button', () => { @@ -58,29 +56,22 @@ describe('import actions cell', () => { }); }); - it('does not render import button when group import is in progress', () => { - const group = generateFakeEntry({ id: 1, status: STATUSES.STARTED }); - createComponent({ group }); + it('does not render import button when group is not available for import', () => { + createComponent({ isAvailableForImport: false }); const button = wrapper.findComponent(GlButton); expect(button.exists()).toBe(false); }); - it('renders import button as disabled when there are validation errors', () => { - const group = generateFakeEntry({ - id: 1, - status: STATUSES.NONE, - validation_errors: [{ field: 'new_name', message: 'something ' }], - }); - createComponent({ group }); + it('renders import button as disabled when group is invalid', () => { + createComponent({ isInvalid: true, isAvailableForImport: true }); const button = wrapper.findComponent(GlButton); expect(button.props().disabled).toBe(true); }); it('emits import-group event when import button is clicked', () => { - const group = generateFakeEntry({ id: 1, status: STATUSES.NONE }); - createComponent({ group }); + createComponent({ isAvailableForImport: true }); const button = wrapper.findComponent(GlButton); button.vm.$emit('click'); diff --git a/spec/frontend/import_entities/import_groups/components/import_source_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_source_cell_spec.js index 2a56efd1cbb..f2735d86493 100644 --- a/spec/frontend/import_entities/import_groups/components/import_source_cell_spec.js +++ b/spec/frontend/import_entities/import_groups/components/import_source_cell_spec.js @@ -4,6 +4,11 @@ import { STATUSES } from '~/import_entities/constants'; import ImportSourceCell from '~/import_entities/import_groups/components/import_source_cell.vue'; import { generateFakeEntry } from '../graphql/fixtures'; +const generateFakeTableEntry = ({ flags = {}, ...entry }) => ({ + ...generateFakeEntry(entry), + flags, +}); + describe('import source cell', () => { let wrapper; let group; @@ -23,14 +28,14 @@ describe('import source cell', () => { describe('when group status is NONE', () => { beforeEach(() => { - group = generateFakeEntry({ id: 1, status: STATUSES.NONE }); + group = generateFakeTableEntry({ id: 1, status: STATUSES.NONE }); createComponent({ group }); }); it('renders link to a group', () => { const link = wrapper.findComponent(GlLink); - expect(link.attributes().href).toBe(group.web_url); - expect(link.text()).toContain(group.full_path); + expect(link.attributes().href).toBe(group.webUrl); + expect(link.text()).toContain(group.fullPath); }); it('does not render last imported line', () => { @@ -40,20 +45,24 @@ describe('import source cell', () => { describe('when group status is FINISHED', () => { beforeEach(() => { - group = generateFakeEntry({ id: 1, status: STATUSES.FINISHED }); + group = generateFakeTableEntry({ + id: 1, + status: STATUSES.FINISHED, + flags: { + isFinished: true, + }, + }); createComponent({ group }); }); it('renders link to a group', () => { const link = wrapper.findComponent(GlLink); - expect(link.attributes().href).toBe(group.web_url); - expect(link.text()).toContain(group.full_path); + expect(link.attributes().href).toBe(group.webUrl); + expect(link.text()).toContain(group.fullPath); }); it('renders last imported line', () => { - expect(wrapper.text()).toMatchInterpolatedText( - 'fake_group_1 Last imported to root/last-group1', - ); + expect(wrapper.text()).toMatchInterpolatedText('fake_group_1 Last imported to root/group1'); }); }); }); diff --git a/spec/frontend/import_entities/import_groups/components/import_table_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_spec.js index f43e545e049..6e3df21e30a 100644 --- a/spec/frontend/import_entities/import_groups/components/import_table_spec.js +++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js @@ -1,39 +1,30 @@ -import { - GlButton, - GlEmptyState, - GlLoadingIcon, - GlSearchBoxByClick, - GlDropdown, - GlDropdownItem, - GlTable, -} from '@gitlab/ui'; -import { mount, createLocalVue } from '@vue/test-utils'; -import { nextTick } from 'vue'; +import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; +import MockAdapter from 'axios-mock-adapter'; import createMockApollo from 'helpers/mock_apollo_helper'; -import stubChildren from 'helpers/stub_children'; -import { stubComponent } from 'helpers/stub_component'; import waitForPromises from 'helpers/wait_for_promises'; +import createFlash from '~/flash'; +import httpStatus from '~/lib/utils/http_status'; +import axios from '~/lib/utils/axios_utils'; import { STATUSES } from '~/import_entities/constants'; -import ImportActionsCell from '~/import_entities/import_groups/components/import_actions_cell.vue'; +import { i18n } from '~/import_entities/import_groups/constants'; import ImportTable from '~/import_entities/import_groups/components/import_table.vue'; -import ImportTargetCell from '~/import_entities/import_groups/components/import_target_cell.vue'; import importGroupsMutation from '~/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql'; -import setImportTargetMutation from '~/import_entities/import_groups/graphql/mutations/set_import_target.mutation.graphql'; import PaginationLinks from '~/vue_shared/components/pagination_links.vue'; import { availableNamespacesFixture, generateFakeEntry } from '../graphql/fixtures'; -const localVue = createLocalVue(); -localVue.use(VueApollo); +jest.mock('~/flash'); +jest.mock('~/import_entities/import_groups/services/status_poller'); -const GlDropdownStub = stubComponent(GlDropdown, { - template: '<div><h1 ref="text"><slot name="button-content"></slot></h1><slot></slot></div>', -}); +Vue.use(VueApollo); describe('import table', () => { let wrapper; let apolloProvider; + let axiosMock; const SOURCE_URL = 'https://demo.host'; const FAKE_GROUP = generateFakeEntry({ id: 1, status: STATUSES.NONE }); @@ -44,76 +35,81 @@ describe('import table', () => { const FAKE_PAGE_INFO = { page: 1, perPage: 20, total: 40, totalPages: 2 }; const findImportSelectedButton = () => - wrapper.findAllComponents(GlButton).wrappers.find((w) => w.text() === 'Import selected'); - const findPaginationDropdown = () => wrapper.findComponent(GlDropdown); - const findPaginationDropdownText = () => findPaginationDropdown().find({ ref: 'text' }).text(); + wrapper.findAll('button').wrappers.find((w) => w.text() === 'Import selected'); + const findImportButtons = () => + wrapper.findAll('button').wrappers.filter((w) => w.text() === 'Import'); + const findPaginationDropdown = () => wrapper.find('[aria-label="Page size"]'); + const findPaginationDropdownText = () => findPaginationDropdown().find('button').text(); - // TODO: remove this ugly approach when - // issue: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1531 - const findTable = () => wrapper.vm.getTableRef(); + const selectRow = (idx) => + wrapper.findAll('tbody td input[type=checkbox]').at(idx).trigger('click'); - const createComponent = ({ bulkImportSourceGroups }) => { + const createComponent = ({ bulkImportSourceGroups, importGroups }) => { apolloProvider = createMockApollo([], { Query: { availableNamespaces: () => availableNamespacesFixture, bulkImportSourceGroups, }, Mutation: { - setTargetNamespace: jest.fn(), - setNewName: jest.fn(), - importGroup: jest.fn(), + importGroups, }, }); wrapper = mount(ImportTable, { propsData: { groupPathRegex: /.*/, + jobsPath: '/fake_job_path', sourceUrl: SOURCE_URL, - groupUrlErrorMessage: 'Please choose a group URL with no special characters or spaces.', - }, - stubs: { - ...stubChildren(ImportTable), - GlSprintf: false, - GlDropdown: GlDropdownStub, - GlTable: false, }, - localVue, apolloProvider, }); }; + beforeAll(() => { + gon.api_version = 'v4'; + }); + + beforeEach(() => { + axiosMock = new MockAdapter(axios); + axiosMock.onGet(/.*\/exists$/, () => []).reply(200); + }); + afterEach(() => { wrapper.destroy(); }); - it('renders loading icon while performing request', async () => { - createComponent({ - bulkImportSourceGroups: () => new Promise(() => {}), + describe('loading state', () => { + it('renders loading icon while performing request', async () => { + createComponent({ + bulkImportSourceGroups: () => new Promise(() => {}), + }); + await waitForPromises(); + + expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); }); - await waitForPromises(); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); - }); + it('does not renders loading icon when request is completed', async () => { + createComponent({ + bulkImportSourceGroups: () => [], + }); + await waitForPromises(); - it('does not renders loading icon when request is completed', async () => { - createComponent({ - bulkImportSourceGroups: () => [], + expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); }); - await waitForPromises(); - - expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); }); - it('renders message about empty state when no groups are available for import', async () => { - createComponent({ - bulkImportSourceGroups: () => ({ - nodes: [], - pageInfo: FAKE_PAGE_INFO, - }), - }); - await waitForPromises(); + describe('empty state', () => { + it('renders message about empty state when no groups are available for import', async () => { + createComponent({ + bulkImportSourceGroups: () => ({ + nodes: [], + pageInfo: FAKE_PAGE_INFO, + }), + }); + await waitForPromises(); - expect(wrapper.find(GlEmptyState).props().title).toBe('You have no groups to import'); + expect(wrapper.find(GlEmptyState).props().title).toBe('You have no groups to import'); + }); }); it('renders import row for each group in response', async () => { @@ -140,40 +136,51 @@ describe('import table', () => { expect(wrapper.text()).not.toContain('Showing 1-0'); }); - describe('converts row events to mutation invocations', () => { - beforeEach(() => { - createComponent({ - bulkImportSourceGroups: () => ({ nodes: [FAKE_GROUP], pageInfo: FAKE_PAGE_INFO }), - }); - return waitForPromises(); + it('invokes importGroups mutation when row button is clicked', async () => { + createComponent({ + bulkImportSourceGroups: () => ({ nodes: [FAKE_GROUP], pageInfo: FAKE_PAGE_INFO }), }); - it.each` - event | payload | mutation | variables - ${'update-target-namespace'} | ${'new-namespace'} | ${setImportTargetMutation} | ${{ sourceGroupId: FAKE_GROUP.id, targetNamespace: 'new-namespace', newName: 'group1' }} - ${'update-new-name'} | ${'new-name'} | ${setImportTargetMutation} | ${{ sourceGroupId: FAKE_GROUP.id, targetNamespace: 'root', newName: 'new-name' }} - `('correctly maps $event to mutation', async ({ event, payload, mutation, variables }) => { - jest.spyOn(apolloProvider.defaultClient, 'mutate'); - wrapper.find(ImportTargetCell).vm.$emit(event, payload); - await waitForPromises(); - expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith({ - mutation, - variables, - }); - }); + jest.spyOn(apolloProvider.defaultClient, 'mutate'); - it('invokes importGroups mutation when row button is clicked', async () => { - jest.spyOn(apolloProvider.defaultClient, 'mutate'); + await waitForPromises(); - wrapper.findComponent(ImportActionsCell).vm.$emit('import-group'); - await waitForPromises(); - expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith({ - mutation: importGroupsMutation, - variables: { sourceGroupIds: [FAKE_GROUP.id] }, - }); + await findImportButtons()[0].trigger('click'); + expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith({ + mutation: importGroupsMutation, + variables: { + importRequests: [ + { + newName: FAKE_GROUP.lastImportTarget.newName, + sourceGroupId: FAKE_GROUP.id, + targetNamespace: availableNamespacesFixture[0].fullPath, + }, + ], + }, }); }); + it('displays error if importing group fails', async () => { + createComponent({ + bulkImportSourceGroups: () => ({ nodes: [FAKE_GROUP], pageInfo: FAKE_PAGE_INFO }), + importGroups: () => { + throw new Error(); + }, + }); + + axiosMock.onPost('/import/bulk_imports.json').reply(httpStatus.BAD_REQUEST); + + await waitForPromises(); + await findImportButtons()[0].trigger('click'); + await waitForPromises(); + + expect(createFlash).toHaveBeenCalledWith( + expect.objectContaining({ + message: i18n.ERROR_IMPORT, + }), + ); + }); + describe('pagination', () => { const bulkImportSourceGroupsQueryMock = jest .fn() @@ -195,10 +202,10 @@ describe('import table', () => { }); it('updates page size when selected in Dropdown', async () => { - const otherOption = wrapper.findAllComponents(GlDropdownItem).at(1); + const otherOption = findPaginationDropdown().findAll('li p').at(1); expect(otherOption.text()).toMatchInterpolatedText('50 items per page'); - otherOption.vm.$emit('click'); + await otherOption.trigger('click'); await waitForPromises(); expect(findPaginationDropdownText()).toMatchInterpolatedText('50 items per page'); @@ -247,7 +254,11 @@ describe('import table', () => { return waitForPromises(); }); - const findFilterInput = () => wrapper.find(GlSearchBoxByClick); + const setFilter = (value) => { + const input = wrapper.find('input[placeholder="Filter by source group"]'); + input.setValue(value); + return input.trigger('keydown.enter'); + }; it('properly passes filter to graphql query when search box is submitted', async () => { createComponent({ @@ -256,7 +267,7 @@ describe('import table', () => { await waitForPromises(); const FILTER_VALUE = 'foo'; - findFilterInput().vm.$emit('submit', FILTER_VALUE); + await setFilter(FILTER_VALUE); await waitForPromises(); expect(bulkImportSourceGroupsQueryMock).toHaveBeenCalledWith( @@ -274,7 +285,7 @@ describe('import table', () => { await waitForPromises(); const FILTER_VALUE = 'foo'; - findFilterInput().vm.$emit('submit', FILTER_VALUE); + await setFilter(FILTER_VALUE); await waitForPromises(); expect(wrapper.text()).toContain('Showing 1-1 of 40 groups matching filter "foo" from'); @@ -282,12 +293,14 @@ describe('import table', () => { it('properly resets filter in graphql query when search box is cleared', async () => { const FILTER_VALUE = 'foo'; - findFilterInput().vm.$emit('submit', FILTER_VALUE); + await setFilter(FILTER_VALUE); await waitForPromises(); bulkImportSourceGroupsQueryMock.mockClear(); await apolloProvider.defaultClient.resetStore(); - findFilterInput().vm.$emit('clear'); + + await setFilter(''); + await waitForPromises(); expect(bulkImportSourceGroupsQueryMock).toHaveBeenCalledWith( @@ -320,8 +333,8 @@ describe('import table', () => { }), }); await waitForPromises(); - wrapper.find(GlTable).vm.$emit('row-selected', [FAKE_GROUPS[0]]); - await nextTick(); + + await selectRow(0); expect(findImportSelectedButton().props().disabled).toBe(false); }); @@ -337,7 +350,7 @@ describe('import table', () => { }); await waitForPromises(); - findTable().selectRow(0); + await selectRow(0); await nextTick(); expect(findImportSelectedButton().props().disabled).toBe(true); @@ -348,7 +361,6 @@ describe('import table', () => { generateFakeEntry({ id: 2, status: STATUSES.NONE, - validation_errors: [{ field: 'new_name', message: 'FAKE_VALIDATION_ERROR' }], }), ]; @@ -360,9 +372,9 @@ describe('import table', () => { }); await waitForPromises(); - // TODO: remove this ugly approach when - // issue: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1531 - findTable().selectRow(0); + await wrapper.find('tbody input[aria-label="New name"]').setValue(''); + jest.runOnlyPendingTimers(); + await selectRow(0); await nextTick(); expect(findImportSelectedButton().props().disabled).toBe(true); @@ -384,15 +396,28 @@ describe('import table', () => { jest.spyOn(apolloProvider.defaultClient, 'mutate'); await waitForPromises(); - findTable().selectRow(0); - findTable().selectRow(1); + await selectRow(0); + await selectRow(1); await nextTick(); - findImportSelectedButton().vm.$emit('click'); + await findImportSelectedButton().trigger('click'); expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith({ mutation: importGroupsMutation, - variables: { sourceGroupIds: [NEW_GROUPS[0].id, NEW_GROUPS[1].id] }, + variables: { + importRequests: [ + { + targetNamespace: availableNamespacesFixture[0].fullPath, + newName: NEW_GROUPS[0].lastImportTarget.newName, + sourceGroupId: NEW_GROUPS[0].id, + }, + { + targetNamespace: availableNamespacesFixture[0].fullPath, + newName: NEW_GROUPS[1].lastImportTarget.newName, + sourceGroupId: NEW_GROUPS[1].id, + }, + ], + }, }); }); }); diff --git a/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js index be83a61841f..3c2367e22f5 100644 --- a/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js +++ b/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js @@ -3,20 +3,20 @@ import { shallowMount } from '@vue/test-utils'; import ImportGroupDropdown from '~/import_entities/components/group_dropdown.vue'; import { STATUSES } from '~/import_entities/constants'; import ImportTargetCell from '~/import_entities/import_groups/components/import_target_cell.vue'; -import { availableNamespacesFixture } from '../graphql/fixtures'; - -const getFakeGroup = (status) => ({ - web_url: 'https://fake.host/', - full_path: 'fake_group_1', - full_name: 'fake_name_1', - import_target: { - target_namespace: 'root', - new_name: 'group1', - }, - id: 1, - validation_errors: [], - progress: { status }, -}); +import { generateFakeEntry, availableNamespacesFixture } from '../graphql/fixtures'; + +const generateFakeTableEntry = ({ flags = {}, ...config }) => { + const entry = generateFakeEntry(config); + + return { + ...entry, + importTarget: { + targetNamespace: availableNamespacesFixture[0], + newName: entry.lastImportTarget.newName, + }, + flags, + }; +}; describe('import target cell', () => { let wrapper; @@ -31,7 +31,6 @@ describe('import target cell', () => { propsData: { availableNamespaces: availableNamespacesFixture, groupPathRegex: /.*/, - groupUrlErrorMessage: 'Please choose a group URL with no special characters or spaces.', ...props, }, }); @@ -44,11 +43,11 @@ describe('import target cell', () => { describe('events', () => { beforeEach(() => { - group = getFakeGroup(STATUSES.NONE); + group = generateFakeTableEntry({ id: 1, status: STATUSES.NONE }); createComponent({ group }); }); - it('invokes $event', () => { + it('emits update-new-name when input value is changed', () => { findNameInput().vm.$emit('input', 'demo'); expect(wrapper.emitted('update-new-name')).toBeDefined(); expect(wrapper.emitted('update-new-name')[0][0]).toBe('demo'); @@ -56,18 +55,23 @@ describe('import target cell', () => { it('emits update-target-namespace when dropdown option is clicked', () => { const dropdownItem = findNamespaceDropdown().findAllComponents(GlDropdownItem).at(2); - const dropdownItemText = dropdownItem.text(); dropdownItem.vm.$emit('click'); expect(wrapper.emitted('update-target-namespace')).toBeDefined(); - expect(wrapper.emitted('update-target-namespace')[0][0]).toBe(dropdownItemText); + expect(wrapper.emitted('update-target-namespace')[0][0]).toBe(availableNamespacesFixture[1]); }); }); describe('when entity status is NONE', () => { beforeEach(() => { - group = getFakeGroup(STATUSES.NONE); + group = generateFakeTableEntry({ + id: 1, + status: STATUSES.NONE, + flags: { + isAvailableForImport: true, + }, + }); createComponent({ group }); }); @@ -78,7 +82,7 @@ describe('import target cell', () => { it('renders only no parent option if available namespaces list is empty', () => { createComponent({ - group: getFakeGroup(STATUSES.NONE), + group: generateFakeTableEntry({ id: 1, status: STATUSES.NONE }), availableNamespaces: [], }); @@ -92,7 +96,7 @@ describe('import target cell', () => { it('renders both no parent option and available namespaces list when available namespaces list is not empty', () => { createComponent({ - group: getFakeGroup(STATUSES.NONE), + group: generateFakeTableEntry({ id: 1, status: STATUSES.NONE }), availableNamespaces: availableNamespacesFixture, }); @@ -104,9 +108,12 @@ describe('import target cell', () => { expect(rest).toHaveLength(availableNamespacesFixture.length); }); - describe('when entity status is SCHEDULING', () => { + describe('when entity is not available for import', () => { beforeEach(() => { - group = getFakeGroup(STATUSES.SCHEDULING); + group = generateFakeTableEntry({ + id: 1, + flags: { isAvailableForImport: false }, + }); createComponent({ group }); }); @@ -115,9 +122,9 @@ describe('import target cell', () => { }); }); - describe('when entity status is FINISHED', () => { + describe('when entity is available for import', () => { beforeEach(() => { - group = getFakeGroup(STATUSES.FINISHED); + group = generateFakeTableEntry({ id: 1, flags: { isAvailableForImport: true } }); createComponent({ group }); }); @@ -125,41 +132,4 @@ describe('import target cell', () => { expect(findNamespaceDropdown().attributes('disabled')).toBe(undefined); }); }); - - describe('validations', () => { - it('reports invalid group name when name is not matching regex', () => { - createComponent({ - group: { - ...getFakeGroup(STATUSES.NONE), - import_target: { - target_namespace: 'root', - new_name: 'very`bad`name', - }, - }, - groupPathRegex: /^[a-zA-Z]+$/, - }); - - expect(wrapper.text()).toContain( - 'Please choose a group URL with no special characters or spaces.', - ); - }); - - it('reports invalid group name if relevant validation error exists', async () => { - const FAKE_ERROR_MESSAGE = 'fake error'; - - createComponent({ - group: { - ...getFakeGroup(STATUSES.NONE), - validation_errors: [ - { - field: 'new_name', - message: FAKE_ERROR_MESSAGE, - }, - ], - }, - }); - - expect(wrapper.text()).toContain(FAKE_ERROR_MESSAGE); - }); - }); }); |