diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-06-20 11:10:13 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-06-20 11:10:13 +0000 |
commit | 0ea3fcec397b69815975647f5e2aa5fe944a8486 (patch) | |
tree | 7979381b89d26011bcf9bdc989a40fcc2f1ed4ff /spec/frontend/access_tokens | |
parent | 72123183a20411a36d607d70b12d57c484394c8e (diff) | |
download | gitlab-ce-0ea3fcec397b69815975647f5e2aa5fe944a8486.tar.gz |
Add latest changes from gitlab-org/gitlab@15-1-stable-eev15.1.0-rc42
Diffstat (limited to 'spec/frontend/access_tokens')
4 files changed, 623 insertions, 34 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); + }); + }); +}); diff --git a/spec/frontend/access_tokens/index_spec.js b/spec/frontend/access_tokens/index_spec.js index 1d8ac7cec25..b6119f1d167 100644 --- a/spec/frontend/access_tokens/index_spec.js +++ b/spec/frontend/access_tokens/index_spec.js @@ -1,27 +1,118 @@ +/* eslint-disable vue/require-prop-types */ +/* eslint-disable vue/one-component-per-file */ import { createWrapper } from '@vue/test-utils'; import Vue from 'vue'; +import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; -import { initExpiresAtField, initProjectsField } from '~/access_tokens'; +import { + initAccessTokenTableApp, + initExpiresAtField, + initNewAccessTokenApp, + initProjectsField, + initTokensApp, +} from '~/access_tokens'; +import * as AccessTokenTableApp from '~/access_tokens/components/access_token_table_app.vue'; import * as ExpiresAtField from '~/access_tokens/components/expires_at_field.vue'; +import * as NewAccessTokenApp from '~/access_tokens/components/new_access_token_app.vue'; import * as ProjectsField from '~/access_tokens/components/projects_field.vue'; +import * as TokensApp from '~/access_tokens/components/tokens_app.vue'; +import { FEED_TOKEN, INCOMING_EMAIL_TOKEN, STATIC_OBJECT_TOKEN } from '~/access_tokens/constants'; +import { __, sprintf } from '~/locale'; describe('access tokens', () => { - const FakeComponent = Vue.component('FakeComponent', { - props: { - inputAttrs: { - type: Object, - required: true, - }, - }, - render: () => null, - }); + let wrapper; - beforeEach(() => { - window.gon = { features: { personalAccessTokensScopedToProjects: true } }; + afterEach(() => { + wrapper?.destroy(); + resetHTMLFixture(); }); - afterEach(() => { - document.body.innerHTML = ''; + describe('initAccessTokenTableApp', () => { + const accessTokenType = 'personal access token'; + const accessTokenTypePlural = 'personal access tokens'; + const initialActiveAccessTokens = [{ id: '1' }]; + + const FakeAccessTokenTableApp = Vue.component('FakeComponent', { + inject: [ + 'accessTokenType', + 'accessTokenTypePlural', + 'initialActiveAccessTokens', + 'noActiveTokensMessage', + 'showRole', + ], + props: [ + 'accessTokenType', + 'accessTokenTypePlural', + 'initialActiveAccessTokens', + 'noActiveTokensMessage', + 'showRole', + ], + render: () => null, + }); + AccessTokenTableApp.default = FakeAccessTokenTableApp; + + it('mounts the component and provides required values', () => { + setHTMLFixture( + `<div id="js-access-token-table-app" + data-access-token-type="${accessTokenType}" + data-access-token-type-plural="${accessTokenTypePlural}" + data-initial-active-access-tokens=${JSON.stringify(initialActiveAccessTokens)} + > + </div>`, + ); + + const vueInstance = initAccessTokenTableApp(); + + wrapper = createWrapper(vueInstance); + const component = wrapper.findComponent(FakeAccessTokenTableApp); + + expect(component.exists()).toBe(true); + + expect(component.props()).toMatchObject({ + // Required value + accessTokenType, + accessTokenTypePlural, + initialActiveAccessTokens, + + // Default values + noActiveTokensMessage: sprintf(__('This user has no active %{accessTokenTypePlural}.'), { + accessTokenTypePlural, + }), + showRole: false, + }); + }); + + it('mounts the component and provides all values', () => { + const noActiveTokensMessage = 'This group has no active access tokens.'; + setHTMLFixture( + `<div id="js-access-token-table-app" + data-access-token-type="${accessTokenType}" + data-access-token-type-plural="${accessTokenTypePlural}" + data-initial-active-access-tokens=${JSON.stringify(initialActiveAccessTokens)} + data-no-active-tokens-message="${noActiveTokensMessage}" + data-show-role + > + </div>`, + ); + + const vueInstance = initAccessTokenTableApp(); + + wrapper = createWrapper(vueInstance); + const component = wrapper.findComponent(FakeAccessTokenTableApp); + + expect(component.exists()).toBe(true); + expect(component.props()).toMatchObject({ + accessTokenType, + accessTokenTypePlural, + initialActiveAccessTokens, + noActiveTokensMessage, + showRole: true, + }); + }); + + it('returns `null`', () => { + expect(initNewAccessTokenApp()).toBe(null); + }); }); describe.each` @@ -30,33 +121,42 @@ describe('access tokens', () => { ${initProjectsField} | ${'js-access-tokens-projects'} | ${'projects'} | ${ProjectsField} `('$initFunction', ({ initFunction, mountSelector, fieldName, expectedComponent }) => { describe('when mount element exists', () => { + const FakeComponent = Vue.component('FakeComponent', { + props: ['inputAttrs'], + render: () => null, + }); + const nameAttribute = `access_tokens[${fieldName}]`; const idAttribute = `access_tokens_${fieldName}`; beforeEach(() => { - const mountEl = document.createElement('div'); - mountEl.classList.add(mountSelector); - - const input = document.createElement('input'); - input.setAttribute('name', nameAttribute); - input.setAttribute('data-js-name', fieldName); - input.setAttribute('id', idAttribute); - input.setAttribute('placeholder', 'Foo bar'); - input.setAttribute('value', '1,2'); + window.gon = { features: { personalAccessTokensScopedToProjects: true } }; - mountEl.appendChild(input); - - document.body.appendChild(mountEl); + setHTMLFixture( + `<div class="${mountSelector}"> + <input + name="${nameAttribute}" + data-js-name="${fieldName}" + id="${idAttribute}" + placeholder="Foo bar" + value="1,2" + /> + </div>`, + ); // Mock component so we don't have to deal with mocking Apollo // eslint-disable-next-line no-param-reassign expectedComponent.default = FakeComponent; }); + afterEach(() => { + delete window.gon; + }); + it('mounts component and sets `inputAttrs` prop', async () => { const vueInstance = await initFunction(); - const wrapper = createWrapper(vueInstance); + wrapper = createWrapper(vueInstance); const component = wrapper.findComponent(FakeComponent); expect(component.exists()).toBe(true); @@ -75,4 +175,64 @@ describe('access tokens', () => { }); }); }); + + describe('initNewAccessTokenApp', () => { + it('mounts the component and sets `accessTokenType` prop', () => { + const accessTokenType = 'personal access token'; + setHTMLFixture( + `<div id="js-new-access-token-app" data-access-token-type="${accessTokenType}"></div>`, + ); + + const FakeNewAccessTokenApp = Vue.component('FakeComponent', { + inject: ['accessTokenType'], + props: ['accessTokenType'], + render: () => null, + }); + NewAccessTokenApp.default = FakeNewAccessTokenApp; + + const vueInstance = initNewAccessTokenApp(); + + wrapper = createWrapper(vueInstance); + const component = wrapper.findComponent(FakeNewAccessTokenApp); + + expect(component.exists()).toBe(true); + expect(component.props('accessTokenType')).toEqual(accessTokenType); + }); + + it('returns `null`', () => { + expect(initNewAccessTokenApp()).toBe(null); + }); + }); + + describe('initTokensApp', () => { + it('mounts the component and provides`tokenTypes` ', () => { + const tokensData = { + [FEED_TOKEN]: FEED_TOKEN, + [INCOMING_EMAIL_TOKEN]: INCOMING_EMAIL_TOKEN, + [STATIC_OBJECT_TOKEN]: STATIC_OBJECT_TOKEN, + }; + setHTMLFixture( + `<div id="js-tokens-app" data-tokens-data=${JSON.stringify(tokensData)}></div>`, + ); + + const FakeTokensApp = Vue.component('FakeComponent', { + inject: ['tokenTypes'], + props: ['tokenTypes'], + render: () => null, + }); + TokensApp.default = FakeTokensApp; + + const vueInstance = initTokensApp(); + + wrapper = createWrapper(vueInstance); + const component = wrapper.findComponent(FakeTokensApp); + + expect(component.exists()).toBe(true); + expect(component.props('tokenTypes')).toEqual(tokensData); + }); + + it('returns `null`', () => { + expect(initNewAccessTokenApp()).toBe(null); + }); + }); }); |