Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/access_tokens/components')
-rw-r--r--spec/frontend/access_tokens/components/access_token_table_app_spec.js241
-rw-r--r--spec/frontend/access_tokens/components/expires_at_field_spec.js33
-rw-r--r--spec/frontend/access_tokens/components/new_access_token_app_spec.js169
3 files changed, 436 insertions, 7 deletions
diff --git a/spec/frontend/access_tokens/components/access_token_table_app_spec.js b/spec/frontend/access_tokens/components/access_token_table_app_spec.js
new file mode 100644
index 00000000000..b45abe418e4
--- /dev/null
+++ b/spec/frontend/access_tokens/components/access_token_table_app_spec.js
@@ -0,0 +1,241 @@
+import { GlPagination, GlTable } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import AccessTokenTableApp from '~/access_tokens/components/access_token_table_app.vue';
+import { EVENT_SUCCESS, PAGE_SIZE } from '~/access_tokens/components/constants';
+import { __, s__, sprintf } from '~/locale';
+import DomElementListener from '~/vue_shared/components/dom_element_listener.vue';
+
+describe('~/access_tokens/components/access_token_table_app', () => {
+ let wrapper;
+
+ const accessTokenType = 'personal access token';
+ const accessTokenTypePlural = 'personal access tokens';
+ const initialActiveAccessTokens = [];
+ const noActiveTokensMessage = 'This user has no active personal access tokens.';
+ const showRole = false;
+
+ const defaultActiveAccessTokens = [
+ {
+ name: 'a',
+ scopes: ['api'],
+ created_at: '2021-05-01T00:00:00.000Z',
+ last_used_at: null,
+ expired: false,
+ expires_soon: true,
+ expires_at: null,
+ revoked: false,
+ revoke_path: '/-/profile/personal_access_tokens/1/revoke',
+ role: 'Maintainer',
+ },
+ {
+ name: 'b',
+ scopes: ['api', 'sudo'],
+ created_at: '2022-04-21T00:00:00.000Z',
+ last_used_at: '2022-04-21T00:00:00.000Z',
+ expired: true,
+ expires_soon: false,
+ expires_at: new Date().toISOString(),
+ revoked: false,
+ revoke_path: '/-/profile/personal_access_tokens/2/revoke',
+ role: 'Maintainer',
+ },
+ ];
+
+ const createComponent = (props = {}) => {
+ wrapper = mount(AccessTokenTableApp, {
+ provide: {
+ accessTokenType,
+ accessTokenTypePlural,
+ initialActiveAccessTokens,
+ noActiveTokensMessage,
+ showRole,
+ ...props,
+ },
+ });
+ };
+
+ const triggerSuccess = async (activeAccessTokens = defaultActiveAccessTokens) => {
+ wrapper
+ .findComponent(DomElementListener)
+ .vm.$emit(EVENT_SUCCESS, { detail: [{ active_access_tokens: activeAccessTokens }] });
+ await nextTick();
+ };
+
+ const findTable = () => wrapper.findComponent(GlTable);
+ const findHeaders = () => findTable().findAll('th > :first-child');
+ const findCells = () => findTable().findAll('td');
+ const findPagination = () => wrapper.findComponent(GlPagination);
+
+ afterEach(() => {
+ wrapper?.destroy();
+ });
+
+ it('should render the `GlTable` with default empty message', () => {
+ createComponent();
+
+ const cells = findCells();
+ expect(cells).toHaveLength(1);
+ expect(cells.at(0).text()).toBe(
+ sprintf(__('This user has no active %{accessTokenTypePlural}.'), { accessTokenTypePlural }),
+ );
+ });
+
+ it('should render the `GlTable` with custom empty message', () => {
+ const noTokensMessage = 'This group has no active access tokens.';
+ createComponent({ noActiveTokensMessage: noTokensMessage });
+
+ const cells = findCells();
+ expect(cells).toHaveLength(1);
+ expect(cells.at(0).text()).toBe(noTokensMessage);
+ });
+
+ it('should render an h5 element', () => {
+ createComponent();
+
+ expect(wrapper.find('h5').text()).toBe(
+ sprintf(__('Active %{accessTokenTypePlural} (%{totalAccessTokens})'), {
+ accessTokenTypePlural,
+ totalAccessTokens: initialActiveAccessTokens.length,
+ }),
+ );
+ });
+
+ it('should render the `GlTable` component with default 6 column headers', () => {
+ createComponent();
+
+ const headers = findHeaders();
+ expect(headers).toHaveLength(6);
+ [
+ __('Token name'),
+ __('Scopes'),
+ s__('AccessTokens|Created'),
+ __('Last Used'),
+ __('Expires'),
+ __('Action'),
+ ].forEach((text, index) => {
+ expect(headers.at(index).text()).toBe(text);
+ });
+ });
+
+ it('should render the `GlTable` component with 7 headers', () => {
+ createComponent({ showRole: true });
+
+ const headers = findHeaders();
+ expect(headers).toHaveLength(7);
+ [
+ __('Token name'),
+ __('Scopes'),
+ s__('AccessTokens|Created'),
+ __('Last Used'),
+ __('Expires'),
+ __('Role'),
+ __('Action'),
+ ].forEach((text, index) => {
+ expect(headers.at(index).text()).toBe(text);
+ });
+ });
+
+ it('`Last Used` header should contain a link and an assistive message', () => {
+ createComponent();
+
+ const headers = wrapper.findAll('th');
+ const lastUsed = headers.at(3);
+ const anchor = lastUsed.find('a');
+ const assistiveElement = lastUsed.find('.gl-sr-only');
+ expect(anchor.exists()).toBe(true);
+ expect(anchor.attributes('href')).toBe(
+ '/help/user/profile/personal_access_tokens.md#view-the-last-time-a-token-was-used',
+ );
+ expect(assistiveElement.text()).toBe(s__('AccessTokens|The last time a token was used'));
+ });
+
+ it('updates the table after a success AJAX event', async () => {
+ createComponent({ showRole: true });
+ await triggerSuccess();
+
+ const cells = findCells();
+ expect(cells).toHaveLength(14);
+
+ // First row
+ expect(cells.at(0).text()).toBe('a');
+ expect(cells.at(1).text()).toBe('api');
+ expect(cells.at(2).text()).not.toBe(__('Never'));
+ expect(cells.at(3).text()).toBe(__('Never'));
+ expect(cells.at(4).text()).toBe(__('Never'));
+ expect(cells.at(5).text()).toBe('Maintainer');
+ let anchor = cells.at(6).find('a');
+ expect(anchor.attributes()).toMatchObject({
+ 'aria-label': __('Revoke'),
+ 'data-qa-selector': __('revoke_button'),
+ href: '/-/profile/personal_access_tokens/1/revoke',
+ 'data-confirm': sprintf(
+ __(
+ 'Are you sure you want to revoke this %{accessTokenType}? This action cannot be undone.',
+ ),
+ { accessTokenType },
+ ),
+ });
+
+ expect(anchor.classes()).toContain('btn-danger-secondary');
+
+ // Second row
+ expect(cells.at(7).text()).toBe('b');
+ expect(cells.at(8).text()).toBe('api, sudo');
+ expect(cells.at(9).text()).not.toBe(__('Never'));
+ expect(cells.at(10).text()).not.toBe(__('Never'));
+ expect(cells.at(11).text()).toBe(__('Expired'));
+ expect(cells.at(12).text()).toBe('Maintainer');
+ anchor = cells.at(13).find('a');
+ expect(anchor.attributes('href')).toBe('/-/profile/personal_access_tokens/2/revoke');
+ expect(anchor.classes()).toEqual(['btn', 'btn-danger', 'btn-md', 'gl-button', 'btn-icon']);
+ });
+
+ it('sorts rows alphabetically', async () => {
+ createComponent({ showRole: true });
+ await triggerSuccess();
+
+ const cells = findCells();
+
+ // First and second rows
+ expect(cells.at(0).text()).toBe('a');
+ expect(cells.at(7).text()).toBe('b');
+
+ const headers = findHeaders();
+ await headers.at(0).trigger('click');
+ await headers.at(0).trigger('click');
+
+ // First and second rows have swapped
+ expect(cells.at(0).text()).toBe('b');
+ expect(cells.at(7).text()).toBe('a');
+ });
+
+ it('sorts rows by date', async () => {
+ createComponent({ showRole: true });
+ await triggerSuccess();
+
+ const cells = findCells();
+
+ // First and second rows
+ expect(cells.at(3).text()).toBe('Never');
+ expect(cells.at(10).text()).not.toBe('Never');
+
+ const headers = findHeaders();
+ await headers.at(3).trigger('click');
+
+ // First and second rows have swapped
+ expect(cells.at(3).text()).not.toBe('Never');
+ expect(cells.at(10).text()).toBe('Never');
+ });
+
+ it('should show the pagination component when needed', async () => {
+ createComponent();
+ expect(findPagination().exists()).toBe(false);
+
+ await triggerSuccess(Array(PAGE_SIZE).fill(defaultActiveAccessTokens[0]));
+ expect(findPagination().exists()).toBe(false);
+
+ await triggerSuccess(Array(PAGE_SIZE + 1).fill(defaultActiveAccessTokens[0]));
+ expect(findPagination().exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/access_tokens/components/expires_at_field_spec.js b/spec/frontend/access_tokens/components/expires_at_field_spec.js
index fc8edcb573f..cb899d10ba7 100644
--- a/spec/frontend/access_tokens/components/expires_at_field_spec.js
+++ b/spec/frontend/access_tokens/components/expires_at_field_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { GlDatepicker } from '@gitlab/ui';
import ExpiresAtField from '~/access_tokens/components/expires_at_field.vue';
describe('~/access_tokens/components/expires_at_field', () => {
@@ -12,22 +13,40 @@ describe('~/access_tokens/components/expires_at_field', () => {
},
};
- const createComponent = (propsData = defaultPropsData) => {
+ const findDatepicker = () => wrapper.findComponent(GlDatepicker);
+
+ const createComponent = (props = {}) => {
wrapper = shallowMount(ExpiresAtField, {
- propsData,
+ propsData: {
+ ...defaultPropsData,
+ ...props,
+ },
});
};
- beforeEach(() => {
- createComponent();
- });
-
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
it('should render datepicker with input info', () => {
+ createComponent();
+
expect(wrapper.element).toMatchSnapshot();
});
+
+ it('should set the date pickers minimum date', () => {
+ const minDate = new Date('1970-01-01');
+
+ createComponent({ minDate });
+
+ expect(findDatepicker().props('minDate')).toStrictEqual(minDate);
+ });
+
+ it('should set the date pickers maximum date', () => {
+ const maxDate = new Date('1970-01-01');
+
+ createComponent({ maxDate });
+
+ expect(findDatepicker().props('maxDate')).toStrictEqual(maxDate);
+ });
});
diff --git a/spec/frontend/access_tokens/components/new_access_token_app_spec.js b/spec/frontend/access_tokens/components/new_access_token_app_spec.js
new file mode 100644
index 00000000000..9ccadbebf7a
--- /dev/null
+++ b/spec/frontend/access_tokens/components/new_access_token_app_spec.js
@@ -0,0 +1,169 @@
+import { GlAlert } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import NewAccessTokenApp from '~/access_tokens/components/new_access_token_app.vue';
+import { EVENT_ERROR, EVENT_SUCCESS, FORM_SELECTOR } from '~/access_tokens/components/constants';
+import { createAlert, VARIANT_INFO } from '~/flash';
+import { __, sprintf } from '~/locale';
+import DomElementListener from '~/vue_shared/components/dom_element_listener.vue';
+import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue';
+
+jest.mock('~/flash');
+
+describe('~/access_tokens/components/new_access_token_app', () => {
+ let wrapper;
+
+ const accessTokenType = 'personal access token';
+
+ const createComponent = (provide = { accessTokenType }) => {
+ wrapper = mountExtended(NewAccessTokenApp, {
+ provide,
+ });
+ };
+
+ const triggerSuccess = async (newToken = 'new token') => {
+ wrapper.find(DomElementListener).vm.$emit(EVENT_SUCCESS, { detail: [{ new_token: newToken }] });
+ await nextTick();
+ };
+
+ const triggerError = async (errors = ['1', '2']) => {
+ wrapper.find(DomElementListener).vm.$emit(EVENT_ERROR, { detail: [{ errors }] });
+ await nextTick();
+ };
+
+ beforeEach(() => {
+ // NewAccessTokenApp observes a form element
+ setHTMLFixture(`<form id="${FORM_SELECTOR.slice(1)}"><input type="submit"/></form>`);
+
+ createComponent();
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
+ wrapper.destroy();
+ createAlert.mockClear();
+ });
+
+ it('should render nothing', () => {
+ expect(wrapper.findComponent(InputCopyToggleVisibility).exists()).toBe(false);
+ expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
+ });
+
+ describe('on success', () => {
+ it('should render `InputCopyToggleVisibility` component', async () => {
+ const newToken = '12345';
+ await triggerSuccess(newToken);
+
+ expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
+
+ const InputCopyToggleVisibilityComponent = wrapper.findComponent(InputCopyToggleVisibility);
+ expect(InputCopyToggleVisibilityComponent.props('value')).toBe(newToken);
+ expect(InputCopyToggleVisibilityComponent.props('copyButtonTitle')).toBe(
+ sprintf(__('Copy %{accessTokenType}'), { accessTokenType }),
+ );
+ expect(InputCopyToggleVisibilityComponent.props('initialVisibility')).toBe(true);
+ expect(InputCopyToggleVisibilityComponent.attributes('label')).toBe(
+ sprintf(__('Your new %{accessTokenType}'), { accessTokenType }),
+ );
+ });
+
+ it('input field should contain QA-related selectors', async () => {
+ const newToken = '12345';
+ await triggerSuccess(newToken);
+
+ expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
+
+ const inputAttributes = wrapper
+ .findByLabelText(sprintf(__('Your new %{accessTokenType}'), { accessTokenType }))
+ .attributes();
+ expect(inputAttributes).toMatchObject({
+ class: expect.stringContaining('qa-created-access-token'),
+ 'data-qa-selector': 'created_access_token_field',
+ });
+ });
+
+ it('should render an info alert', async () => {
+ await triggerSuccess();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: sprintf(__('Your new %{accessTokenType} has been created.'), {
+ accessTokenType,
+ }),
+ variant: VARIANT_INFO,
+ });
+ });
+
+ it('should reset the form', async () => {
+ const resetSpy = jest.spyOn(wrapper.vm.form, 'reset');
+
+ await triggerSuccess();
+
+ expect(resetSpy).toHaveBeenCalled();
+ });
+ });
+
+ describe('on error', () => {
+ it('should render an error alert', async () => {
+ await triggerError(['first', 'second']);
+
+ expect(wrapper.findComponent(InputCopyToggleVisibility).exists()).toBe(false);
+
+ let GlAlertComponent = wrapper.findComponent(GlAlert);
+ expect(GlAlertComponent.props('title')).toBe(__('The form contains the following errors:'));
+ expect(GlAlertComponent.props('variant')).toBe('danger');
+ let itemEls = wrapper.findAll('li');
+ expect(itemEls).toHaveLength(2);
+ expect(itemEls.at(0).text()).toBe('first');
+ expect(itemEls.at(1).text()).toBe('second');
+
+ await triggerError(['one']);
+
+ GlAlertComponent = wrapper.findComponent(GlAlert);
+ expect(GlAlertComponent.props('title')).toBe(__('The form contains the following error:'));
+ expect(GlAlertComponent.props('variant')).toBe('danger');
+ itemEls = wrapper.findAll('li');
+ expect(itemEls).toHaveLength(1);
+ });
+
+ it('the error alert should be dismissible', async () => {
+ await triggerError();
+
+ const GlAlertComponent = wrapper.findComponent(GlAlert);
+ expect(GlAlertComponent.exists()).toBe(true);
+
+ GlAlertComponent.vm.$emit('dismiss');
+ await nextTick();
+
+ expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
+ });
+ });
+
+ describe('before error or success', () => {
+ it('should scroll to the container', async () => {
+ const containerEl = wrapper.vm.$refs.container;
+ const scrollIntoViewSpy = jest.spyOn(containerEl, 'scrollIntoView');
+
+ await triggerSuccess();
+
+ expect(scrollIntoViewSpy).toHaveBeenCalledWith(false);
+ expect(scrollIntoViewSpy).toHaveBeenCalledTimes(1);
+
+ await triggerError();
+
+ expect(scrollIntoViewSpy).toHaveBeenCalledWith(false);
+ expect(scrollIntoViewSpy).toHaveBeenCalledTimes(2);
+ });
+
+ it('should dismiss the info alert', async () => {
+ const dismissSpy = jest.fn();
+ createAlert.mockReturnValue({ dismiss: dismissSpy });
+
+ await triggerSuccess();
+ await triggerError();
+
+ expect(dismissSpy).toHaveBeenCalled();
+ expect(dismissSpy).toHaveBeenCalledTimes(1);
+ });
+ });
+});