Commit 8b34af88 authored by Miguel Rincon's avatar Miguel Rincon Committed by Peter Hegman

Refactor masked registration token

This change migrates the registration token dropdown to use
input_copy_toggle_visibility.vue instead of manual implementation.
parent 87bf25e6
<script>
import { GlButtonGroup, GlButton, GlTooltipDirective } from '@gitlab/ui';
import { s__, __ } from '~/locale';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
import { s__ } from '~/locale';
import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue';
export default {
components: {
GlButtonGroup,
GlButton,
ModalCopyButton,
},
directives: {
GlTooltip: GlTooltipDirective,
InputCopyToggleVisibility,
},
props: {
value: {
......@@ -19,65 +13,21 @@ export default {
default: '',
},
},
data() {
return {
isMasked: true,
};
},
computed: {
maskLabel() {
if (this.isMasked) {
return __('Click to reveal');
}
return __('Click to hide');
},
maskIcon() {
if (this.isMasked) {
return 'eye';
}
return 'eye-slash';
},
displayedValue() {
if (this.isMasked && this.value?.length) {
return '*'.repeat(this.value.length);
}
return this.value;
},
},
methods: {
onToggleMasked() {
this.isMasked = !this.isMasked;
},
onCopied() {
onCopy() {
// value already in the clipboard, simply notify the user
this.$toast?.show(s__('Runners|Registration token copied!'));
},
},
i18n: {
copyLabel: s__('Runners|Copy registration token'),
},
I18N_COPY_BUTTON_TITLE: s__('Runners|Copy registration token'),
};
</script>
<template>
<gl-button-group>
<gl-button class="gl-font-monospace" data-testid="token-value" label>
{{ displayedValue }}
</gl-button>
<gl-button
v-gl-tooltip
:aria-label="maskLabel"
:title="maskLabel"
:icon="maskIcon"
class="gl-w-auto! gl-flex-shrink-0!"
data-testid="toggle-masked"
@click.stop="onToggleMasked"
/>
<modal-copy-button
class="gl-w-auto! gl-flex-shrink-0!"
:aria-label="$options.i18n.copyLabel"
:title="$options.i18n.copyLabel"
:text="value"
@success="onCopied"
/>
</gl-button-group>
<input-copy-toggle-visibility
class="gl-m-0"
:value="value"
data-testid="token-value"
:copy-button-title="$options.I18N_COPY_BUTTON_TITLE"
@copy="onCopy"
/>
</template>
......@@ -110,7 +110,7 @@ export default {
v-gl-tooltip.hover="toggleVisibilityLabel"
:aria-label="toggleVisibilityLabel"
:icon="toggleVisibilityIcon"
@click="handleToggleVisibilityButtonClick"
@click.stop="handleToggleVisibilityButtonClick"
/>
<clipboard-button
v-if="showCopyButton"
......
......@@ -8,6 +8,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
import RegistrationToken from '~/runner/components/registration/registration_token.vue';
import RegistrationTokenResetDropdownItem from '~/runner/components/registration/registration_token_reset_dropdown_item.vue';
import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
......@@ -30,11 +31,11 @@ describe('RegistrationDropdown', () => {
const findRegistrationInstructionsDropdownItem = () => wrapper.findComponent(GlDropdownItem);
const findTokenDropdownItem = () => wrapper.findComponent(GlDropdownForm);
const findRegistrationToken = () => wrapper.findComponent(RegistrationToken);
const findRegistrationTokenInput = () => wrapper.findByTestId('token-value').find('input');
const findTokenResetDropdownItem = () =>
wrapper.findComponent(RegistrationTokenResetDropdownItem);
const findToggleMaskButton = () => wrapper.findByTestId('toggle-masked');
const createComponent = ({ props = {}, ...options } = {}, mountFn = shallowMount) => {
wrapper = extendedWrapper(
mountFn(RegistrationDropdown, {
......@@ -134,9 +135,7 @@ describe('RegistrationDropdown', () => {
it('Displays masked value by default', () => {
createComponent({}, mount);
expect(findTokenDropdownItem().text()).toMatchInterpolatedText(
`Registration token ${maskToken}`,
);
expect(findRegistrationTokenInput().element.value).toBe(maskToken);
});
});
......@@ -155,16 +154,14 @@ describe('RegistrationDropdown', () => {
});
it('Updates the token when it gets reset', async () => {
const newToken = 'mock1';
createComponent({}, mount);
const newToken = 'mock1';
expect(findRegistrationTokenInput().props('value')).not.toBe(newToken);
findTokenResetDropdownItem().vm.$emit('tokenReset', newToken);
findToggleMaskButton().vm.$emit('click', { stopPropagation: jest.fn() });
await nextTick();
expect(findTokenDropdownItem().text()).toMatchInterpolatedText(
`Registration token ${newToken}`,
);
expect(findRegistrationToken().props('value')).toBe(newToken);
});
});
import { nextTick } from 'vue';
import { GlToast } from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import RegistrationToken from '~/runner/components/registration/registration_token.vue';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue';
const mockToken = '01234567890';
const mockMasked = '***********';
describe('RegistrationToken', () => {
let wrapper;
let stopPropagation;
let showToast;
const findToggleMaskButton = () => wrapper.findByTestId('toggle-masked');
const findCopyButton = () => wrapper.findComponent(ModalCopyButton);
const findInputCopyToggleVisibility = () => wrapper.findComponent(InputCopyToggleVisibility);
const vueWithGlToast = () => {
const localVue = createLocalVue();
......@@ -22,10 +19,14 @@ describe('RegistrationToken', () => {
return localVue;
};
const createComponent = ({ props = {}, withGlToast = true } = {}) => {
const createComponent = ({
props = {},
withGlToast = true,
mountFn = shallowMountExtended,
} = {}) => {
const localVue = withGlToast ? vueWithGlToast() : undefined;
wrapper = shallowMountExtended(RegistrationToken, {
wrapper = mountFn(RegistrationToken, {
propsData: {
value: mockToken,
...props,
......@@ -36,61 +37,33 @@ describe('RegistrationToken', () => {
showToast = wrapper.vm.$toast ? jest.spyOn(wrapper.vm.$toast, 'show') : null;
};
beforeEach(() => {
stopPropagation = jest.fn();
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('Displays masked value by default', () => {
expect(wrapper.text()).toBe(mockMasked);
});
it('Displays value and copy button', () => {
createComponent();
it('Displays button to reveal token', () => {
expect(findToggleMaskButton().attributes('aria-label')).toBe('Click to reveal');
expect(findInputCopyToggleVisibility().props('value')).toBe(mockToken);
expect(findInputCopyToggleVisibility().props('copyButtonTitle')).toBe(
'Copy registration token',
);
});
it('Can copy the original token value', () => {
expect(findCopyButton().props('text')).toBe(mockToken);
// Component integration test to ensure secure masking
it('Displays masked value by default', () => {
createComponent({ mountFn: mountExtended });
expect(wrapper.find('input').element.value).toBe(mockMasked);
});
describe('When the reveal icon is clicked', () => {
describe('When the copy to clipboard button is clicked', () => {
beforeEach(() => {
findToggleMaskButton().vm.$emit('click', { stopPropagation });
});
it('Click event is not propagated', async () => {
expect(stopPropagation).toHaveBeenCalledTimes(1);
createComponent();
});
it('Displays the actual value', () => {
expect(wrapper.text()).toBe(mockToken);
});
it('Can copy the original token value', () => {
expect(findCopyButton().props('text')).toBe(mockToken);
});
it('Displays button to mask token', () => {
expect(findToggleMaskButton().attributes('aria-label')).toBe('Click to hide');
});
it('When user clicks again, displays masked value', async () => {
findToggleMaskButton().vm.$emit('click', { stopPropagation });
await nextTick();
expect(wrapper.text()).toBe(mockMasked);
expect(findToggleMaskButton().attributes('aria-label')).toBe('Click to reveal');
});
});
describe('When the copy to clipboard button is clicked', () => {
it('shows a copied message', () => {
findCopyButton().vm.$emit('success');
findInputCopyToggleVisibility().vm.$emit('copy');
expect(showToast).toHaveBeenCalledTimes(1);
expect(showToast).toHaveBeenCalledWith('Registration token copied!');
......@@ -98,7 +71,7 @@ describe('RegistrationToken', () => {
it('does not fail when toast is not defined', () => {
createComponent({ withGlToast: false });
findCopyButton().vm.$emit('success');
findInputCopyToggleVisibility().vm.$emit('copy');
// This block also tests for unhandled errors
expect(showToast).toBeNull();
......
......@@ -89,8 +89,11 @@ describe('InputCopyToggleVisibility', () => {
});
describe('when clicked', () => {
let event;
beforeEach(async () => {
await findRevealButton().trigger('click');
event = { stopPropagation: jest.fn() };
await findRevealButton().trigger('click', event);
});
it('displays value', () => {
......@@ -110,6 +113,11 @@ describe('InputCopyToggleVisibility', () => {
it('emits `visibility-change` event', () => {
expect(wrapper.emitted('visibility-change')[0]).toEqual([true]);
});
it('stops propagation on click event', () => {
// in case the input is located in a dropdown or modal
expect(event.stopPropagation).toHaveBeenCalledTimes(1);
});
});
});
......
......@@ -35,11 +35,11 @@ RSpec.shared_examples 'shows and resets runner registration token' do
it 'has a registration token' do
click_on 'Click to reveal'
expect(page.find('[data-testid="token-value"]')).to have_content(registration_token)
expect(page.find('[data-testid="token-value"] input').value).to have_content(registration_token)
end
describe 'reset registration token' do
let!(:old_registration_token) { find('[data-testid="token-value"]').text }
let!(:old_registration_token) { find('[data-testid="token-value"] input').value }
before do
click_on 'Reset registration token'
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment