Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
eb004dc626
commit
2b1e7f7dac
|
@ -79,3 +79,33 @@ export default function initCopyToClipboard() {
|
|||
clipboardData.setData('text/x-gfm', json.gfm);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Programmatically triggers a click event on a
|
||||
* "copy to clipboard" button, causing its
|
||||
* contents to be copied. Handles some of the messiniess
|
||||
* around managing the button's tooltip.
|
||||
* @param {HTMLElement} btnElement
|
||||
*/
|
||||
export function clickCopyToClipboardButton(btnElement) {
|
||||
const $btnElement = $(btnElement);
|
||||
|
||||
// Ensure the button has already been tooltip'd.
|
||||
// If the use hasn't yet interacted (i.e. hovered or clicked)
|
||||
// with the button, Bootstrap hasn't yet initialized
|
||||
// the tooltip, and its `data-original-title` will be `undefined`.
|
||||
// This value is used in the functions above.
|
||||
$btnElement.tooltip();
|
||||
btnElement.dispatchEvent(new MouseEvent('mouseover'));
|
||||
|
||||
btnElement.click();
|
||||
|
||||
// Manually trigger the necessary events to hide the
|
||||
// button's tooltip and allow the button to perform its
|
||||
// tooltip cleanup (updating the title from "Copied" back
|
||||
// to its original title, "Copy branch name").
|
||||
setTimeout(() => {
|
||||
btnElement.dispatchEvent(new MouseEvent('mouseout'));
|
||||
$btnElement.tooltip('hide');
|
||||
}, 2000);
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ import Sidebar from '../../right_sidebar';
|
|||
import Shortcuts from './shortcuts';
|
||||
import { CopyAsGFM } from '../markdown/copy_as_gfm';
|
||||
import { getSelectedFragment } from '~/lib/utils/common_utils';
|
||||
import { isElementVisible } from '~/lib/utils/dom_utils';
|
||||
import { clickCopyToClipboardButton } from '~/behaviors/copy_to_clipboard';
|
||||
|
||||
export default class ShortcutsIssuable extends Shortcuts {
|
||||
constructor() {
|
||||
|
@ -14,6 +16,7 @@ export default class ShortcutsIssuable extends Shortcuts {
|
|||
Mousetrap.bind('l', () => ShortcutsIssuable.openSidebarDropdown('labels'));
|
||||
Mousetrap.bind('r', ShortcutsIssuable.replyWithSelectedText);
|
||||
Mousetrap.bind('e', ShortcutsIssuable.editIssue);
|
||||
Mousetrap.bind('b', ShortcutsIssuable.copyBranchName);
|
||||
}
|
||||
|
||||
static replyWithSelectedText() {
|
||||
|
@ -98,4 +101,18 @@ export default class ShortcutsIssuable extends Shortcuts {
|
|||
Sidebar.instance.openDropdown(name);
|
||||
return false;
|
||||
}
|
||||
|
||||
static copyBranchName() {
|
||||
// There are two buttons - one that is shown when the sidebar
|
||||
// is expanded, and one that is shown when it's collapsed.
|
||||
const allCopyBtns = Array.from(document.querySelectorAll('.sidebar-source-branch button'));
|
||||
|
||||
// Select whichever button is currently visible so that
|
||||
// the "Copied" tooltip is shown when a click is simulated.
|
||||
const visibleBtn = allCopyBtns.find(isElementVisible);
|
||||
|
||||
if (visibleBtn) {
|
||||
clickCopyToClipboardButton(visibleBtn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,128 +0,0 @@
|
|||
import { escape } from 'lodash';
|
||||
import axios from '../lib/utils/axios_utils';
|
||||
import { s__ } from '../locale';
|
||||
import { deprecatedCreateFlash as Flash } from '../flash';
|
||||
import { parseBoolean } from '../lib/utils/common_utils';
|
||||
import statusCodes from '../lib/utils/http_status';
|
||||
import VariableList from './ci_variable_list';
|
||||
|
||||
function generateErrorBoxContent(errors) {
|
||||
const errorList = [].concat(errors).map(
|
||||
errorString => `
|
||||
<li>
|
||||
${escape(errorString)}
|
||||
</li>
|
||||
`,
|
||||
);
|
||||
|
||||
return `
|
||||
<p>
|
||||
${s__('CiVariable|Validation failed')}
|
||||
</p>
|
||||
<ul>
|
||||
${errorList.join('')}
|
||||
</ul>
|
||||
`;
|
||||
}
|
||||
|
||||
// Used for the variable list on CI/CD projects/groups settings page
|
||||
export default class AjaxVariableList {
|
||||
constructor({
|
||||
container,
|
||||
saveButton,
|
||||
errorBox,
|
||||
formField = 'variables',
|
||||
saveEndpoint,
|
||||
maskableRegex,
|
||||
}) {
|
||||
this.container = container;
|
||||
this.saveButton = saveButton;
|
||||
this.errorBox = errorBox;
|
||||
this.saveEndpoint = saveEndpoint;
|
||||
this.maskableRegex = maskableRegex;
|
||||
|
||||
this.variableList = new VariableList({
|
||||
container: this.container,
|
||||
formField,
|
||||
maskableRegex,
|
||||
});
|
||||
|
||||
this.bindEvents();
|
||||
this.variableList.init();
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
this.saveButton.addEventListener('click', this.onSaveClicked.bind(this));
|
||||
}
|
||||
|
||||
onSaveClicked() {
|
||||
const loadingIcon = this.saveButton.querySelector('.js-ci-variables-save-loading-icon');
|
||||
loadingIcon.classList.toggle('hide', false);
|
||||
this.errorBox.classList.toggle('hide', true);
|
||||
// We use this to prevent a user from changing a key before we have a chance
|
||||
// to match it up in `updateRowsWithPersistedVariables`
|
||||
this.variableList.toggleEnableRow(false);
|
||||
|
||||
return axios
|
||||
.patch(
|
||||
this.saveEndpoint,
|
||||
{
|
||||
variables_attributes: this.variableList.getAllData(),
|
||||
},
|
||||
{
|
||||
// We want to be able to process the `res.data` from a 400 error response
|
||||
// and print the validation messages such as duplicate variable keys
|
||||
validateStatus: status =>
|
||||
(status >= statusCodes.OK && status < statusCodes.MULTIPLE_CHOICES) ||
|
||||
status === statusCodes.BAD_REQUEST,
|
||||
},
|
||||
)
|
||||
.then(res => {
|
||||
loadingIcon.classList.toggle('hide', true);
|
||||
this.variableList.toggleEnableRow(true);
|
||||
|
||||
if (res.status === statusCodes.OK && res.data) {
|
||||
this.updateRowsWithPersistedVariables(res.data.variables);
|
||||
this.variableList.hideValues();
|
||||
} else if (res.status === statusCodes.BAD_REQUEST) {
|
||||
// Validation failed
|
||||
this.errorBox.innerHTML = generateErrorBoxContent(res.data);
|
||||
this.errorBox.classList.toggle('hide', false);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
loadingIcon.classList.toggle('hide', true);
|
||||
this.variableList.toggleEnableRow(true);
|
||||
Flash(s__('CiVariable|Error occurred while saving variables'));
|
||||
});
|
||||
}
|
||||
|
||||
updateRowsWithPersistedVariables(persistedVariables = []) {
|
||||
const persistedVariableMap = [].concat(persistedVariables).reduce(
|
||||
(variableMap, variable) => ({
|
||||
...variableMap,
|
||||
[variable.key]: variable,
|
||||
}),
|
||||
{},
|
||||
);
|
||||
|
||||
this.container.querySelectorAll('.js-row').forEach(row => {
|
||||
// If we submitted a row that was destroyed, remove it so we don't try
|
||||
// to destroy it again which would cause a BE error
|
||||
const destroyInput = row.querySelector('.js-ci-variable-input-destroy');
|
||||
if (parseBoolean(destroyInput.value)) {
|
||||
row.remove();
|
||||
// Update the ID input so any future edits and `_destroy` will apply on the BE
|
||||
} else {
|
||||
const key = row.querySelector('.js-ci-variable-input-key').value;
|
||||
const persistedVariable = persistedVariableMap[key];
|
||||
|
||||
if (persistedVariable) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
row.querySelector('.js-ci-variable-input-id').value = persistedVariable.id;
|
||||
row.setAttribute('data-is-persisted', 'true');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -60,7 +60,7 @@ export default {
|
|||
</script>
|
||||
<template>
|
||||
<gl-dropdown :text="value">
|
||||
<gl-search-box-by-type v-model.trim="searchTerm" />
|
||||
<gl-search-box-by-type v-model.trim="searchTerm" data-testid="ci-environment-search" />
|
||||
<gl-dropdown-item
|
||||
v-for="environment in filteredResults"
|
||||
:key="environment"
|
||||
|
@ -75,7 +75,7 @@ export default {
|
|||
}}</gl-dropdown-item>
|
||||
<template v-if="shouldRenderCreateButton">
|
||||
<gl-dropdown-divider />
|
||||
<gl-dropdown-item @click="createClicked">
|
||||
<gl-dropdown-item data-testid="create-wildcard-button" @click="createClicked">
|
||||
{{ composedCreateButtonLabel }}
|
||||
</gl-dropdown-item>
|
||||
</template>
|
||||
|
|
|
@ -236,6 +236,7 @@ export default {
|
|||
:label="__('Environment scope')"
|
||||
label-for="ci-variable-env"
|
||||
class="w-50"
|
||||
data-testid="environment-scope"
|
||||
>
|
||||
<ci-environments-dropdown
|
||||
class="w-100"
|
||||
|
@ -247,7 +248,11 @@ export default {
|
|||
</div>
|
||||
|
||||
<gl-form-group :label="__('Flags')" label-for="ci-variable-flags">
|
||||
<gl-form-checkbox v-model="protected_variable" class="mb-0">
|
||||
<gl-form-checkbox
|
||||
v-model="protected_variable"
|
||||
class="mb-0"
|
||||
data-testid="ci-variable-protected-checkbox"
|
||||
>
|
||||
{{ __('Protect variable') }}
|
||||
<gl-link target="_blank" :href="protectedEnvironmentVariablesLink">
|
||||
<gl-icon name="question" :size="12" />
|
||||
|
@ -261,6 +266,7 @@ export default {
|
|||
ref="masked-ci-variable"
|
||||
v-model="masked"
|
||||
data-qa-selector="ci_variable_masked_checkbox"
|
||||
data-testid="ci-variable-masked-checkbox"
|
||||
>
|
||||
{{ __('Mask variable') }}
|
||||
<gl-link target="_blank" :href="maskedEnvironmentVariablesLink">
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
<script>
|
||||
/* eslint-disable vue/no-v-html */
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import { GlLoadingIcon, GlButton } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlLoadingIcon,
|
||||
GlButton,
|
||||
},
|
||||
props: {
|
||||
isLoading: {
|
||||
|
@ -41,24 +42,24 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="text-center p-3">
|
||||
<div class="gl-text-center gl-p-5">
|
||||
<div v-if="illustrationPath" class="svg-content svg-130"><img :src="illustrationPath" /></div>
|
||||
<h4>{{ __('Web Terminal') }}</h4>
|
||||
<gl-loading-icon v-if="isLoading" size="lg" class="gl-mt-3" />
|
||||
<template v-else>
|
||||
<p>{{ __('Run tests against your code live using the Web Terminal') }}</p>
|
||||
<p>
|
||||
<button
|
||||
<gl-button
|
||||
:disabled="!isValid"
|
||||
class="btn btn-info"
|
||||
type="button"
|
||||
category="primary"
|
||||
variant="info"
|
||||
data-qa-selector="start_web_terminal_button"
|
||||
@click="onStart"
|
||||
>
|
||||
{{ __('Start Web Terminal') }}
|
||||
</button>
|
||||
</gl-button>
|
||||
</p>
|
||||
<div v-if="!isValid && message" class="bs-callout text-left" v-html="message"></div>
|
||||
<div v-if="!isValid && message" class="bs-callout gl-text-left" v-html="message"></div>
|
||||
<p v-else>
|
||||
<a
|
||||
v-if="helpPath"
|
||||
|
|
|
@ -47,3 +47,25 @@ export const parseBooleanDataAttributes = ({ dataset }, names) =>
|
|||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
/**
|
||||
* Returns whether or not the provided element is currently visible.
|
||||
* This function operates identically to jQuery's `:visible` pseudo-selector.
|
||||
* Documentation for this selector: https://api.jquery.com/visible-selector/
|
||||
* Implementation of this selector: https://github.com/jquery/jquery/blob/d0ce00cdfa680f1f0c38460bc51ea14079ae8b07/src/css/hiddenVisibleSelectors.js#L8
|
||||
* @param {HTMLElement} element The element to test
|
||||
* @returns {Boolean} `true` if the element is currently visible, otherwise false
|
||||
*/
|
||||
export const isElementVisible = element =>
|
||||
Boolean(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
|
||||
|
||||
/**
|
||||
* The opposite of `isElementVisible`.
|
||||
* Returns whether or not the provided element is currently hidden.
|
||||
* This function operates identically to jQuery's `:hidden` pseudo-selector.
|
||||
* Documentation for this selector: https://api.jquery.com/hidden-selector/
|
||||
* Implementation of this selector: https://github.com/jquery/jquery/blob/d0ce00cdfa680f1f0c38460bc51ea14079ae8b07/src/css/hiddenVisibleSelectors.js#L6
|
||||
* @param {HTMLElement} element The element to test
|
||||
* @returns {Boolean} `true` if the element is currently hidden, otherwise false
|
||||
*/
|
||||
export const isElementHidden = element => !isElementVisible(element);
|
||||
|
|
|
@ -40,6 +40,7 @@ import { initUserTracking, initDefaultTrackers } from './tracking';
|
|||
import { __ } from './locale';
|
||||
|
||||
import * as tooltips from '~/tooltips';
|
||||
import * as popovers from '~/popovers';
|
||||
|
||||
import 'ee_else_ce/main_ee';
|
||||
|
||||
|
@ -81,7 +82,7 @@ document.addEventListener('beforeunload', () => {
|
|||
// Close any open tooltips
|
||||
tooltips.dispose(document.querySelectorAll('.has-tooltip, [data-toggle="tooltip"]'));
|
||||
// Close any open popover
|
||||
$('[data-toggle="popover"]').popover('dispose');
|
||||
popovers.dispose();
|
||||
});
|
||||
|
||||
window.addEventListener('hashchange', handleLocationHash);
|
||||
|
@ -166,13 +167,7 @@ function deferredInitialisation() {
|
|||
});
|
||||
|
||||
// Initialize popovers
|
||||
$body.popover({
|
||||
selector: '[data-toggle="popover"]',
|
||||
trigger: 'focus',
|
||||
// set the viewport to the main content, excluding the navigation bar, so
|
||||
// the navigation can't overlap the popover
|
||||
viewport: '.layout-page',
|
||||
});
|
||||
popovers.initPopovers();
|
||||
|
||||
// Adding a helper class to activate animations only after all is rendered
|
||||
setTimeout(() => $body.addClass('page-initialised'), 1000);
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import initSettingsPanels from '~/settings_panels';
|
||||
import AjaxVariableList from '~/ci_variable_list/ajax_variable_list';
|
||||
import initVariableList from '~/ci_variable_list';
|
||||
import initFilteredSearch from '~/pages/search/init_filtered_search';
|
||||
import GroupRunnersFilteredSearchTokenKeys from '~/filtered_search/group_runners_filtered_search_token_keys';
|
||||
|
@ -17,19 +16,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
useDefaultState: false,
|
||||
});
|
||||
|
||||
if (gon.features.newVariablesUi) {
|
||||
initVariableList();
|
||||
} else {
|
||||
const variableListEl = document.querySelector('.js-ci-variable-list-section');
|
||||
// eslint-disable-next-line no-new
|
||||
new AjaxVariableList({
|
||||
container: variableListEl,
|
||||
saveButton: variableListEl.querySelector('.js-ci-variables-save-button'),
|
||||
errorBox: variableListEl.querySelector('.js-ci-variable-error-box'),
|
||||
saveEndpoint: variableListEl.dataset.saveEndpoint,
|
||||
maskableRegex: variableListEl.dataset.maskableRegex,
|
||||
});
|
||||
}
|
||||
|
||||
initSharedRunnersForm();
|
||||
initVariableList();
|
||||
});
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import initSettingsPanels from '~/settings_panels';
|
||||
import SecretValues from '~/behaviors/secret_values';
|
||||
import AjaxVariableList from '~/ci_variable_list/ajax_variable_list';
|
||||
import registrySettingsApp from '~/registry/settings/registry_settings_bundle';
|
||||
import initVariableList from '~/ci_variable_list';
|
||||
import initDeployFreeze from '~/deploy_freeze';
|
||||
|
@ -18,19 +17,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
runnerTokenSecretValue.init();
|
||||
}
|
||||
|
||||
if (gon.features.newVariablesUi) {
|
||||
initVariableList();
|
||||
} else {
|
||||
const variableListEl = document.querySelector('.js-ci-variable-list-section');
|
||||
// eslint-disable-next-line no-new
|
||||
new AjaxVariableList({
|
||||
container: variableListEl,
|
||||
saveButton: variableListEl.querySelector('.js-ci-variables-save-button'),
|
||||
errorBox: variableListEl.querySelector('.js-ci-variable-error-box'),
|
||||
saveEndpoint: variableListEl.dataset.saveEndpoint,
|
||||
maskableRegex: variableListEl.dataset.maskableRegex,
|
||||
});
|
||||
}
|
||||
initVariableList();
|
||||
|
||||
// hide extra auto devops settings based checkbox state
|
||||
const autoDevOpsExtraSettings = document.querySelector('.js-extra-settings');
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
<script>
|
||||
// We can't use v-safe-html here as the popover's title or content might contains SVGs that would
|
||||
// be stripped by the directive's sanitizer. Instead, we fallback on v-html and we use GitLab's
|
||||
// dompurify config that lets SVGs be rendered properly.
|
||||
// Context: https://gitlab.com/gitlab-org/gitlab/-/issues/247207
|
||||
/* eslint-disable vue/no-v-html */
|
||||
import { GlPopover } from '@gitlab/ui';
|
||||
import { sanitize } from '~/lib/dompurify';
|
||||
|
||||
const newPopover = element => {
|
||||
const { content, html, placement, title, triggers = 'focus' } = element.dataset;
|
||||
|
||||
return {
|
||||
target: element,
|
||||
content,
|
||||
html,
|
||||
placement,
|
||||
title,
|
||||
triggers,
|
||||
};
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlPopover,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
popovers: [],
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.observer = new MutationObserver(mutations => {
|
||||
mutations.forEach(mutation => {
|
||||
mutation.removedNodes.forEach(this.dispose);
|
||||
});
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.observer.disconnect();
|
||||
},
|
||||
methods: {
|
||||
addPopovers(elements) {
|
||||
const newPopovers = elements.reduce((acc, element) => {
|
||||
if (this.popoverExists(element)) {
|
||||
return acc;
|
||||
}
|
||||
const popover = newPopover(element);
|
||||
this.observe(popover);
|
||||
return [...acc, popover];
|
||||
}, []);
|
||||
|
||||
this.popovers.push(...newPopovers);
|
||||
},
|
||||
observe(popover) {
|
||||
this.observer.observe(popover.target.parentElement, {
|
||||
childList: true,
|
||||
});
|
||||
},
|
||||
dispose(target) {
|
||||
if (!target) {
|
||||
this.popovers = [];
|
||||
} else {
|
||||
const index = this.popovers.findIndex(popover => popover.target === target);
|
||||
|
||||
if (index > -1) {
|
||||
this.popovers.splice(index, 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
popoverExists(element) {
|
||||
return this.popovers.some(popover => popover.target === element);
|
||||
},
|
||||
getSafeHtml(html) {
|
||||
return sanitize(html);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<gl-popover v-for="(popover, index) in popovers" :key="index" v-bind="popover">
|
||||
<template #title>
|
||||
<span v-if="popover.html" v-html="getSafeHtml(popover.title)"></span>
|
||||
<span v-else>{{ popover.title }}</span>
|
||||
</template>
|
||||
<span v-if="popover.html" v-html="getSafeHtml(popover.content)"></span>
|
||||
<span v-else>{{ popover.content }}</span>
|
||||
</gl-popover>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,51 @@
|
|||
import Vue from 'vue';
|
||||
import { toArray } from 'lodash';
|
||||
import PopoversComponent from './components/popovers.vue';
|
||||
|
||||
let app;
|
||||
|
||||
const APP_ELEMENT_ID = 'gl-popovers-app';
|
||||
|
||||
const getPopoversApp = () => {
|
||||
if (!app) {
|
||||
const container = document.createElement('div');
|
||||
container.setAttribute('id', APP_ELEMENT_ID);
|
||||
document.body.appendChild(container);
|
||||
|
||||
const Popovers = Vue.extend(PopoversComponent);
|
||||
app = new Popovers();
|
||||
app.$mount(`#${APP_ELEMENT_ID}`);
|
||||
}
|
||||
|
||||
return app;
|
||||
};
|
||||
|
||||
const isPopover = (node, selector) => node.matches && node.matches(selector);
|
||||
|
||||
const handlePopoverEvent = (rootTarget, e, selector) => {
|
||||
for (let { target } = e; target && target !== rootTarget; target = target.parentNode) {
|
||||
if (isPopover(target, selector)) {
|
||||
getPopoversApp().addPopovers([target]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const initPopovers = () => {
|
||||
['mouseenter', 'focus', 'click'].forEach(event => {
|
||||
document.addEventListener(
|
||||
event,
|
||||
e => handlePopoverEvent(document, e, '[data-toggle="popover"]'),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
return getPopoversApp();
|
||||
};
|
||||
|
||||
export const dispose = elements => toArray(elements).forEach(getPopoversApp().dispose);
|
||||
|
||||
export const destroy = () => {
|
||||
getPopoversApp().$destroy();
|
||||
app = null;
|
||||
};
|
|
@ -1,6 +1,5 @@
|
|||
<script>
|
||||
/* eslint-disable vue/no-v-html */
|
||||
import Mousetrap from 'mousetrap';
|
||||
import { escape } from 'lodash';
|
||||
import {
|
||||
GlButton,
|
||||
|
@ -84,17 +83,6 @@ export default {
|
|||
: '';
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
Mousetrap.bind('b', this.copyBranchName);
|
||||
},
|
||||
beforeDestroy() {
|
||||
Mousetrap.unbind('b');
|
||||
},
|
||||
methods: {
|
||||
copyBranchName() {
|
||||
this.$refs.copyBranchNameButton.$el.click();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
|
@ -110,7 +98,6 @@ export default {
|
|||
class="label-branch label-truncate js-source-branch"
|
||||
v-html="mr.sourceBranchLink"
|
||||
/><clipboard-button
|
||||
ref="copyBranchNameButton"
|
||||
data-testid="mr-widget-copy-clipboard"
|
||||
:text="branchNameClipboardData"
|
||||
:title="__('Copy branch name')"
|
||||
|
|
|
@ -9,9 +9,15 @@ $notification-box-shadow-color: rgba(0, 0, 0, 0.25);
|
|||
|
||||
&.sticky {
|
||||
position: sticky;
|
||||
position: -webkit-sticky;
|
||||
top: $flash-container-top;
|
||||
z-index: 251;
|
||||
|
||||
.flash-alert,
|
||||
.flash-notice,
|
||||
.flash-success,
|
||||
.flash-warning {
|
||||
@include gl-mb-4;
|
||||
}
|
||||
}
|
||||
|
||||
&.flash-container-page {
|
||||
|
|
|
@ -1039,9 +1039,3 @@ $mr-widget-min-height: 69px;
|
|||
.diff-file-row.is-active {
|
||||
background-color: $gray-50;
|
||||
}
|
||||
|
||||
.merge-request-container {
|
||||
.flash-container {
|
||||
@include gl-mb-4;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,9 +8,6 @@ module Groups
|
|||
skip_cross_project_access_check :show
|
||||
before_action :authorize_admin_group!
|
||||
before_action :authorize_update_max_artifacts_size!, only: [:update]
|
||||
before_action do
|
||||
push_frontend_feature_flag(:new_variables_ui, @group, default_enabled: true)
|
||||
end
|
||||
before_action :define_variables, only: [:show]
|
||||
|
||||
feature_category :continuous_integration
|
||||
|
|
|
@ -55,7 +55,7 @@ class Projects::ImportsController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def require_namespace_project_creation_permission
|
||||
render_404 unless current_user.can?(:admin_project, @project) || current_user.can?(:create_projects, @project.namespace)
|
||||
render_404 unless can?(current_user, :admin_project, @project) || can?(current_user, :create_projects, @project.namespace)
|
||||
end
|
||||
|
||||
def redirect_if_progress
|
||||
|
|
|
@ -20,7 +20,10 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
|
|||
end
|
||||
|
||||
def diffs_batch
|
||||
diffs = @compare.diffs_in_batch(params[:page], params[:per_page], diff_options: diff_options)
|
||||
diff_options_hash = diff_options
|
||||
diff_options_hash[:paths] = params[:paths] if params[:paths]
|
||||
|
||||
diffs = @compare.diffs_in_batch(params[:page], params[:per_page], diff_options: diff_options_hash)
|
||||
positions = @merge_request.note_positions_for_paths(diffs.diff_file_paths, current_user)
|
||||
environment = @merge_request.environments_for(current_user, latest: true).last
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ module Projects
|
|||
before_action :authorize_admin_pipeline!
|
||||
before_action :define_variables
|
||||
before_action do
|
||||
push_frontend_feature_flag(:new_variables_ui, @project, default_enabled: true)
|
||||
push_frontend_feature_flag(:ajax_new_deploy_token, @project)
|
||||
end
|
||||
|
||||
|
|
|
@ -31,8 +31,18 @@ class EnvironmentNamesFinder
|
|||
end
|
||||
|
||||
def namespace_environments
|
||||
projects =
|
||||
project_or_group.all_projects.public_or_visible_to_user(current_user)
|
||||
# We assume reporter access is needed for the :read_environment permission
|
||||
# here. This expection is also present in
|
||||
# IssuableFinder::Params#min_access_level, which is used for filtering out
|
||||
# merge requests that don't have the right permissions.
|
||||
#
|
||||
# We use this approach so we don't need to load every project into memory
|
||||
# just to verify if we can see their environments. Doing so would not be
|
||||
# efficient, and possibly mess up pagination if certain projects are not
|
||||
# meant to be visible.
|
||||
projects = project_or_group
|
||||
.all_projects
|
||||
.public_or_visible_to_user(current_user, Gitlab::Access::REPORTER)
|
||||
|
||||
Environment.for_project(projects)
|
||||
end
|
||||
|
|
|
@ -57,10 +57,7 @@ module PageLayoutHelper
|
|||
|
||||
subject = @project || @user || @group
|
||||
|
||||
args = {}
|
||||
args[:only_path] = false if Feature.enabled?(:avatar_with_host)
|
||||
|
||||
image = subject.avatar_url(args) if subject.present?
|
||||
image = subject.avatar_url(only_path: false) if subject.present?
|
||||
image || default
|
||||
end
|
||||
|
||||
|
|
|
@ -8,6 +8,10 @@ class MergeRequestDiffFile < ApplicationRecord
|
|||
belongs_to :merge_request_diff, inverse_of: :merge_request_diff_files
|
||||
alias_attribute :index, :relative_order
|
||||
|
||||
scope :by_paths, ->(paths) do
|
||||
where("new_path in (?) OR old_path in (?)", paths, paths)
|
||||
end
|
||||
|
||||
def utf8_diff
|
||||
return '' if diff.blank?
|
||||
|
||||
|
|
|
@ -11,8 +11,13 @@ class PaginatedDiffEntity < Grape::Entity
|
|||
expose :diff_files do |diffs, options|
|
||||
submodule_links = Gitlab::SubmoduleLinks.new(merge_request.project.repository)
|
||||
|
||||
DiffFileEntity.represent(diffs.diff_files,
|
||||
options.merge(submodule_links: submodule_links, code_navigation_path: code_navigation_path(diffs)))
|
||||
DiffFileEntity.represent(
|
||||
diffs.diff_files,
|
||||
options.merge(
|
||||
submodule_links: submodule_links,
|
||||
code_navigation_path: code_navigation_path(diffs)
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
expose :pagination do
|
||||
|
|
|
@ -5,42 +5,20 @@
|
|||
- link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('ci/variables/README', anchor: 'protect-a-custom-variable') }
|
||||
= s_('Environment variables are configured by your administrator to be %{link_start}protected%{link_end} by default').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
|
||||
|
||||
- if Feature.enabled?(:new_variables_ui, @project || @group, default_enabled: true)
|
||||
- is_group = !@group.nil?
|
||||
- is_group = !@group.nil?
|
||||
|
||||
#js-ci-project-variables{ data: { endpoint: save_endpoint,
|
||||
project_id: @project&.id || '',
|
||||
group: is_group.to_s,
|
||||
maskable_regex: ci_variable_maskable_regex,
|
||||
protected_by_default: ci_variable_protected_by_default?.to_s,
|
||||
aws_logo_svg_path: image_path('aws_logo.svg'),
|
||||
aws_tip_deploy_link: help_page_path('ci/cloud_deployment/index.md', anchor: 'deploy-your-application-to-the-aws-elastic-container-service-ecs'),
|
||||
aws_tip_commands_link: help_page_path('ci/cloud_deployment/index.md', anchor: 'run-aws-commands-from-gitlab-cicd'),
|
||||
aws_tip_learn_link: help_page_path('ci/cloud_deployment/index.md', anchor: 'aws'),
|
||||
protected_environment_variables_link: help_page_path('ci/variables/README', anchor: 'protect-a-custom-variable'),
|
||||
masked_environment_variables_link: help_page_path('ci/variables/README', anchor: 'mask-a-custom-variable'),
|
||||
} }
|
||||
|
||||
- else
|
||||
.row
|
||||
.col-lg-12.js-ci-variable-list-section{ data: { save_endpoint: save_endpoint, maskable_regex: ci_variable_maskable_regex } }
|
||||
.hide.gl-alert.gl-alert-danger.js-ci-variable-error-box
|
||||
|
||||
%ul.ci-variable-list
|
||||
= render 'ci/variables/variable_header'
|
||||
- @variables.each.each do |variable|
|
||||
= render 'ci/variables/variable_row', form_field: 'variables', variable: variable
|
||||
= render 'ci/variables/variable_row', form_field: 'variables'
|
||||
.prepend-top-20
|
||||
%button.btn.btn-success.js-ci-variables-save-button{ type: 'button' }
|
||||
%span.hide.js-ci-variables-save-loading-icon
|
||||
.spinner.spinner-light.mr-1
|
||||
= _('Save variables')
|
||||
%button.btn.btn-info.btn-inverted.gl-ml-3.js-secret-value-reveal-button{ type: 'button', data: { secret_reveal_status: "#{@variables.size == 0}" } }
|
||||
- if @variables.size == 0
|
||||
= n_('Hide value', 'Hide values', @variables.size)
|
||||
- else
|
||||
= n_('Reveal value', 'Reveal values', @variables.size)
|
||||
#js-ci-project-variables{ data: { endpoint: save_endpoint,
|
||||
project_id: @project&.id || '',
|
||||
group: is_group.to_s,
|
||||
maskable_regex: ci_variable_maskable_regex,
|
||||
protected_by_default: ci_variable_protected_by_default?.to_s,
|
||||
aws_logo_svg_path: image_path('aws_logo.svg'),
|
||||
aws_tip_deploy_link: help_page_path('ci/cloud_deployment/index.md', anchor: 'deploy-your-application-to-the-aws-elastic-container-service-ecs'),
|
||||
aws_tip_commands_link: help_page_path('ci/cloud_deployment/index.md', anchor: 'run-aws-commands-from-gitlab-cicd'),
|
||||
aws_tip_learn_link: help_page_path('ci/cloud_deployment/index.md', anchor: 'aws'),
|
||||
protected_environment_variables_link: help_page_path('ci/variables/README', anchor: 'protect-a-custom-variable'),
|
||||
masked_environment_variables_link: help_page_path('ci/variables/README', anchor: 'mask-a-custom-variable'),
|
||||
} }
|
||||
|
||||
- if !@group && @project.group
|
||||
.settings-header.border-top.prepend-top-20
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
toggle: "popover",
|
||||
placement: "top",
|
||||
html: "true",
|
||||
trigger: "focus",
|
||||
triggers: "focus",
|
||||
title: "<div class='gl-font-weight-normal gl-line-height-normal'>#{popover_title_text}</div>",
|
||||
content: "<a href='#{popover_content_url}' target='_blank' rel='noopener noreferrer nofollow'>#{popover_content_text}</a>",
|
||||
} }
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Enable rendering avatars with full url
|
||||
merge_request: 46206
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix bug with robots and .git suffix
|
||||
merge_request: 45866
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix bug accessing import route with no user
|
||||
merge_request: 46215
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove new_variables_ui feature flag
|
||||
merge_request: 41412
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update copy branch keyboard shortcut to click sidebar button
|
||||
merge_request: 45436
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Reduce whitespace on MR page header
|
||||
merge_request: 45966
|
||||
author:
|
||||
type: fixed
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
name: avatar_with_host
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45776
|
||||
rollout_issue_url:
|
||||
type: development
|
||||
group: group::editor
|
||||
default_enabled: false
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
name: new_variables_ui
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25260
|
||||
rollout_issue_url:
|
||||
group: group::continuous integration
|
||||
type: development
|
||||
default_enabled: true
|
|
@ -212,39 +212,7 @@ When the user is added back to the SCIM app, GitLab cannot create a new user bec
|
|||
|
||||
Solution: Have a user sign in directly to GitLab, then [manually link](#user-access-and-linking-setup) their account.
|
||||
|
||||
### Azure
|
||||
|
||||
#### How do I verify my SCIM configuration is correct?
|
||||
|
||||
Review the following:
|
||||
|
||||
- Ensure that the SCIM value for `id` matches the SAML value for `NameId`.
|
||||
- Ensure that the SCIM value for `externalId` matches the SAML value for `NameId`.
|
||||
|
||||
Review the following SCIM parameters for sensible values:
|
||||
|
||||
- `userName`
|
||||
- `displayName`
|
||||
- `emails[type eq "work"].value`
|
||||
|
||||
#### Testing Azure connection: invalid credentials
|
||||
|
||||
When testing the connection, you may encounter an error: **You appear to have entered invalid credentials. Please confirm you are using the correct information for an administrative account**. If `Tenant URL` and `secret token` are correct, check whether your group path contains characters that may be considered invalid JSON primitives (such as `.`). Removing such characters from the group path typically resolves the error.
|
||||
|
||||
#### Azure: (Field) can't be blank sync error
|
||||
|
||||
When checking the Audit Logs for the Provisioning, you can sometimes see the
|
||||
error `Namespace can't be blank, Name can't be blank, and User can't be blank.`
|
||||
|
||||
This is likely caused because not all required fields (such as first name and last name) are present for all users being mapped.
|
||||
|
||||
As a workaround, try an alternate mapping:
|
||||
|
||||
1. Follow the Azure mapping instructions from above.
|
||||
1. Delete the `name.formatted` target attribute entry.
|
||||
1. Change the `displayName` source attribute to have `name.formatted` target attribute.
|
||||
|
||||
#### How do I diagnose why a user is unable to sign in
|
||||
### How do I diagnose why a user is unable to sign in
|
||||
|
||||
Ensure that the user has been added to the SCIM app.
|
||||
|
||||
|
@ -256,7 +224,7 @@ This value is also used by SCIM to match users on the `id`, and is updated by SC
|
|||
|
||||
It is important that this SCIM `id` and SCIM `externalId` are configured to the same value as the SAML `NameId`. SAML responses can be traced using [debugging tools](./index.md#saml-debugging-tools), and any errors can be checked against our [SAML troubleshooting docs](./index.md#troubleshooting).
|
||||
|
||||
#### How do I verify user's SAML NameId matches the SCIM externalId
|
||||
### How do I verify user's SAML NameId matches the SCIM externalId
|
||||
|
||||
Group owners can see the list of users and the `externalId` stored for each user in the group SAML SSO Settings page.
|
||||
|
||||
|
@ -264,7 +232,7 @@ A possible alternative is to use the [SCIM API](../../../api/scim.md#get-a-list-
|
|||
|
||||
To see how the `external_uid` compares to the value returned as the SAML NameId, you can have the user use a [SAML Tracer](index.md#saml-debugging-tools).
|
||||
|
||||
#### Update or fix mismatched SCIM externalId and SAML NameId
|
||||
### Update or fix mismatched SCIM externalId and SAML NameId
|
||||
|
||||
Whether the value was changed or you need to map to a different field, ensure `id`, `externalId`, and `NameId` all map to the same field.
|
||||
|
||||
|
@ -284,8 +252,40 @@ you can address the problem in the following ways:
|
|||
|
||||
It is important not to update these to incorrect values, since this will cause users to be unable to sign in. It is also important not to assign a value to the wrong user, as this would cause users to get signed into the wrong account.
|
||||
|
||||
#### I need to change my SCIM app
|
||||
### I need to change my SCIM app
|
||||
|
||||
Individual users can follow the instructions in the ["SAML authentication failed: User has already been taken"](./index.md#i-need-to-change-my-saml-app) section.
|
||||
|
||||
Alternatively, users can be removed from the SCIM app which will delink all removed users. Sync can then be turned on for the new SCIM app to [link existing users](#user-access-and-linking-setup).
|
||||
|
||||
### Azure
|
||||
|
||||
#### How do I verify my SCIM configuration is correct?
|
||||
|
||||
Review the following:
|
||||
|
||||
- Ensure that the SCIM value for `id` matches the SAML value for `NameId`.
|
||||
- Ensure that the SCIM value for `externalId` matches the SAML value for `NameId`.
|
||||
|
||||
Review the following SCIM parameters for sensible values:
|
||||
|
||||
- `userName`
|
||||
- `displayName`
|
||||
- `emails[type eq "work"].value`
|
||||
|
||||
#### Testing Azure connection: invalid credentials
|
||||
|
||||
When testing the connection, you may encounter an error: **You appear to have entered invalid credentials. Please confirm you are using the correct information for an administrative account**. If `Tenant URL` and `secret token` are correct, check whether your group path contains characters that may be considered invalid JSON primitives (such as `.`). Removing such characters from the group path typically resolves the error.
|
||||
|
||||
#### (Field) can't be blank sync error
|
||||
|
||||
When checking the Audit Logs for the Provisioning, you can sometimes see the
|
||||
error `Namespace can't be blank, Name can't be blank, and User can't be blank.`
|
||||
|
||||
This is likely caused because not all required fields (such as first name and last name) are present for all users being mapped.
|
||||
|
||||
As a workaround, try an alternate mapping:
|
||||
|
||||
1. Follow the Azure mapping instructions from above.
|
||||
1. Delete the `name.formatted` target attribute entry.
|
||||
1. Change the `displayName` source attribute to have `name.formatted` target attribute.
|
||||
|
|
|
@ -18,10 +18,8 @@ module Gitlab
|
|||
def initialize(merge_request_diff, batch_page, batch_size, diff_options:)
|
||||
super(merge_request_diff, diff_options: diff_options)
|
||||
|
||||
batch_page ||= DEFAULT_BATCH_PAGE
|
||||
batch_size ||= DEFAULT_BATCH_SIZE
|
||||
@paginated_collection = load_paginated_collection(batch_page, batch_size, diff_options)
|
||||
|
||||
@paginated_collection = relation.page(batch_page).per(batch_size)
|
||||
@pagination_data = {
|
||||
current_page: @paginated_collection.current_page,
|
||||
next_page: @paginated_collection.next_page,
|
||||
|
@ -63,6 +61,18 @@ module Gitlab
|
|||
def relation
|
||||
@merge_request_diff.merge_request_diff_files
|
||||
end
|
||||
|
||||
def load_paginated_collection(batch_page, batch_size, diff_options)
|
||||
batch_page ||= DEFAULT_BATCH_PAGE
|
||||
batch_size ||= DEFAULT_BATCH_SIZE
|
||||
|
||||
paths = diff_options&.fetch(:paths, nil)
|
||||
|
||||
paginated_collection = relation.page(batch_page).per(batch_size)
|
||||
paginated_collection = paginated_collection.by_paths(paths) if paths
|
||||
|
||||
paginated_collection
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5321,9 +5321,6 @@ msgstr ""
|
|||
msgid "CiVariable|Create wildcard"
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariable|Error occurred while saving variables"
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariable|Masked"
|
||||
msgstr ""
|
||||
|
||||
|
@ -5342,9 +5339,6 @@ msgstr ""
|
|||
msgid "CiVariable|Toggle protected"
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariable|Validation failed"
|
||||
msgstr ""
|
||||
|
||||
msgid "Classification Label (optional)"
|
||||
msgstr ""
|
||||
|
||||
|
@ -23050,9 +23044,6 @@ msgstr ""
|
|||
msgid "Save space and find tags in the Container Registry more easily. Enable the cleanup policy to remove stale tags and keep only the ones you need."
|
||||
msgstr ""
|
||||
|
||||
msgid "Save variables"
|
||||
msgstr ""
|
||||
|
||||
msgid "Saved scan settings and target site settings which are reusable."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ Disallow: /groups/*/group_members
|
|||
|
||||
# Project details
|
||||
User-Agent: *
|
||||
Disallow: /*/*.git
|
||||
Disallow: /*/*.git$
|
||||
Disallow: /*/archive/
|
||||
Disallow: /*/repository/archive*
|
||||
Disallow: /*/activity
|
||||
|
|
|
@ -7,10 +7,21 @@ RSpec.describe Projects::ImportsController do
|
|||
let(:project) { create(:project) }
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
sign_in(user) if user
|
||||
end
|
||||
|
||||
describe 'GET #show' do
|
||||
context 'when user is not authenticated and the project is public' do
|
||||
let(:user) { nil }
|
||||
let(:project) { create(:project, :public) }
|
||||
|
||||
it 'returns 404 response' do
|
||||
get :show, params: { namespace_id: project.namespace.to_param, project_id: project }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the user has maintainer rights' do
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
|
|
|
@ -454,6 +454,31 @@ RSpec.describe Projects::MergeRequests::DiffsController do
|
|||
it_behaves_like 'successful request'
|
||||
end
|
||||
|
||||
context 'with paths param' do
|
||||
let(:example_file_path) { "README" }
|
||||
let(:file_path_option) { { paths: [example_file_path] } }
|
||||
|
||||
subject do
|
||||
go(file_path_option)
|
||||
end
|
||||
|
||||
it_behaves_like 'serializes diffs with expected arguments' do
|
||||
let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
|
||||
let(:expected_options) do
|
||||
collection_arguments(current_page: 1, total_pages: 1)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'successful request'
|
||||
|
||||
it 'filters down the response to the expected file path' do
|
||||
subject
|
||||
|
||||
expect(json_response["diff_files"].size).to eq(1)
|
||||
expect(json_response["diff_files"].first["file_path"]).to eq(example_file_path)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with default params' do
|
||||
subject { go }
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ RSpec.describe 'Group variables', :js do
|
|||
before do
|
||||
group.add_owner(user)
|
||||
gitlab_sign_in(user)
|
||||
stub_feature_flags(new_variables_ui: false)
|
||||
wait_for_requests
|
||||
visit page_path
|
||||
end
|
||||
|
||||
|
|
|
@ -24,7 +24,6 @@ RSpec.describe 'Project group variables', :js do
|
|||
sign_in(user)
|
||||
project.add_maintainer(user)
|
||||
group.add_owner(user)
|
||||
stub_feature_flags(new_variables_ui: false)
|
||||
end
|
||||
|
||||
it 'project in group shows inherited vars from ancestor group' do
|
||||
|
@ -53,9 +52,13 @@ RSpec.describe 'Project group variables', :js do
|
|||
|
||||
it 'project origin keys link to ancestor groups ci_cd settings' do
|
||||
visit project_path
|
||||
|
||||
find('.group-origin-link').click
|
||||
page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do
|
||||
expect(find('.js-ci-variable-input-key').value).to eq(key1)
|
||||
|
||||
wait_for_requests
|
||||
|
||||
page.within('.ci-variable-table') do
|
||||
expect(find('.js-ci-variable-row:nth-child(1) [data-label="Key"]').text).to eq(key1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,32 +12,29 @@ RSpec.describe 'Project variables', :js do
|
|||
sign_in(user)
|
||||
project.add_maintainer(user)
|
||||
project.variables << variable
|
||||
stub_feature_flags(new_variables_ui: false)
|
||||
visit page_path
|
||||
end
|
||||
|
||||
it_behaves_like 'variable list'
|
||||
|
||||
it 'adds new variable with a special environment scope' do
|
||||
page.within('.js-ci-variable-list-section .js-row:last-child') do
|
||||
find('.js-ci-variable-input-key').set('somekey')
|
||||
find('.js-ci-variable-input-value').set('somevalue')
|
||||
it 'adds a new variable with an environment scope' do
|
||||
click_button('Add Variable')
|
||||
|
||||
find('.js-variable-environment-toggle').click
|
||||
find('.js-variable-environment-dropdown-wrapper .dropdown-input-field').set('review/*')
|
||||
find('.js-variable-environment-dropdown-wrapper .js-dropdown-create-new-item').click
|
||||
page.within('#add-ci-variable') do
|
||||
find('[data-qa-selector="ci_variable_key_field"] input').set('akey')
|
||||
find('#ci-variable-value').set('akey_value')
|
||||
find('[data-testid="environment-scope"]').click
|
||||
find_button('clear').click
|
||||
find('[data-testid="ci-environment-search"]').set('review/*')
|
||||
find('[data-testid="create-wildcard-button"]').click
|
||||
|
||||
expect(find('input[name="variables[variables_attributes][][environment_scope]"]', visible: false).value).to eq('review/*')
|
||||
click_button('Add variable')
|
||||
end
|
||||
|
||||
click_button('Save variables')
|
||||
wait_for_requests
|
||||
|
||||
visit page_path
|
||||
|
||||
page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do
|
||||
expect(find('.js-ci-variable-input-key').value).to eq('somekey')
|
||||
expect(page).to have_content('review/*')
|
||||
page.within('.ci-variable-table') do
|
||||
expect(find('.js-ci-variable-row:first-child [data-label="Environments"]').text).to eq('review/*')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -18,7 +18,6 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
|
|||
|
||||
sign_in(user)
|
||||
stub_container_registry_config(enabled: container_registry_enabled)
|
||||
stub_feature_flags(new_variables_ui: false)
|
||||
end
|
||||
|
||||
context 'as owner' do
|
||||
|
|
|
@ -5,74 +5,178 @@ require 'spec_helper'
|
|||
RSpec.describe EnvironmentNamesFinder do
|
||||
describe '#execute' do
|
||||
let!(:group) { create(:group) }
|
||||
let!(:project1) { create(:project, :public, namespace: group) }
|
||||
let!(:project2) { create(:project, :private, namespace: group) }
|
||||
let!(:public_project) { create(:project, :public, namespace: group) }
|
||||
let!(:private_project) { create(:project, :private, namespace: group) }
|
||||
let!(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
create(:environment, name: 'gstg', project: project1)
|
||||
create(:environment, name: 'gprd', project: project1)
|
||||
create(:environment, name: 'gprd', project: project2)
|
||||
create(:environment, name: 'gcny', project: project2)
|
||||
create(:environment, name: 'gstg', project: public_project)
|
||||
create(:environment, name: 'gprd', project: public_project)
|
||||
create(:environment, name: 'gprd', project: private_project)
|
||||
create(:environment, name: 'gcny', project: private_project)
|
||||
end
|
||||
|
||||
context 'using a group and a group member' do
|
||||
it 'returns environment names for all projects' do
|
||||
group.add_developer(user)
|
||||
context 'using a group' do
|
||||
context 'with a group developer' do
|
||||
it 'returns environment names for all projects' do
|
||||
group.add_developer(user)
|
||||
|
||||
names = described_class.new(group, user).execute
|
||||
names = described_class.new(group, user).execute
|
||||
|
||||
expect(names).to eq(%w[gcny gprd gstg])
|
||||
expect(names).to eq(%w[gcny gprd gstg])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a group reporter' do
|
||||
it 'returns environment names for all projects' do
|
||||
group.add_reporter(user)
|
||||
|
||||
names = described_class.new(group, user).execute
|
||||
|
||||
expect(names).to eq(%w[gcny gprd gstg])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a public project reporter' do
|
||||
it 'returns environment names for all public projects' do
|
||||
public_project.add_reporter(user)
|
||||
|
||||
names = described_class.new(group, user).execute
|
||||
|
||||
expect(names).to eq(%w[gprd gstg])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a private project reporter' do
|
||||
it 'returns environment names for all public projects' do
|
||||
private_project.add_reporter(user)
|
||||
|
||||
names = described_class.new(group, user).execute
|
||||
|
||||
expect(names).to eq(%w[gcny gprd gstg])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a group guest' do
|
||||
it 'returns environment names for all public projects' do
|
||||
group.add_guest(user)
|
||||
|
||||
names = described_class.new(group, user).execute
|
||||
|
||||
expect(names).to eq(%w[gprd gstg])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a non-member' do
|
||||
it 'returns environment names for all public projects' do
|
||||
names = described_class.new(group, user).execute
|
||||
|
||||
expect(names).to eq(%w[gprd gstg])
|
||||
end
|
||||
end
|
||||
|
||||
context 'without a user' do
|
||||
it 'returns environment names for all public projects' do
|
||||
names = described_class.new(group).execute
|
||||
|
||||
expect(names).to eq(%w[gprd gstg])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'using a group and a guest' do
|
||||
it 'returns environment names for all public projects' do
|
||||
names = described_class.new(group, user).execute
|
||||
context 'using a public project' do
|
||||
context 'with a project developer' do
|
||||
it 'returns all the unique environment names' do
|
||||
public_project.add_developer(user)
|
||||
|
||||
expect(names).to eq(%w[gprd gstg])
|
||||
names = described_class.new(public_project, user).execute
|
||||
|
||||
expect(names).to eq(%w[gprd gstg])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a project reporter' do
|
||||
it 'returns all the unique environment names' do
|
||||
public_project.add_reporter(user)
|
||||
|
||||
names = described_class.new(public_project, user).execute
|
||||
|
||||
expect(names).to eq(%w[gprd gstg])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a project guest' do
|
||||
it 'returns all the unique environment names' do
|
||||
public_project.add_guest(user)
|
||||
|
||||
names = described_class.new(public_project, user).execute
|
||||
|
||||
expect(names).to eq(%w[gprd gstg])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a non-member' do
|
||||
it 'returns all the unique environment names' do
|
||||
names = described_class.new(public_project, user).execute
|
||||
|
||||
expect(names).to eq(%w[gprd gstg])
|
||||
end
|
||||
end
|
||||
|
||||
context 'without a user' do
|
||||
it 'returns all the unique environment names' do
|
||||
names = described_class.new(public_project).execute
|
||||
|
||||
expect(names).to eq(%w[gprd gstg])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'using a public project and a project member' do
|
||||
it 'returns all the unique environment names' do
|
||||
project1.team.add_developer(user)
|
||||
context 'using a private project' do
|
||||
context 'with a project developer' do
|
||||
it 'returns all the unique environment names' do
|
||||
private_project.add_developer(user)
|
||||
|
||||
names = described_class.new(project1, user).execute
|
||||
names = described_class.new(private_project, user).execute
|
||||
|
||||
expect(names).to eq(%w[gprd gstg])
|
||||
expect(names).to eq(%w[gcny gprd])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'using a public project and a guest' do
|
||||
it 'returns all the unique environment names' do
|
||||
names = described_class.new(project1, user).execute
|
||||
context 'with a project reporter' do
|
||||
it 'returns all the unique environment names' do
|
||||
private_project.add_reporter(user)
|
||||
|
||||
expect(names).to eq(%w[gprd gstg])
|
||||
names = described_class.new(private_project, user).execute
|
||||
|
||||
expect(names).to eq(%w[gcny gprd])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'using a private project and a guest' do
|
||||
it 'returns all the unique environment names' do
|
||||
names = described_class.new(project2, user).execute
|
||||
context 'with a project guest' do
|
||||
it 'does not return any environment names' do
|
||||
private_project.add_guest(user)
|
||||
|
||||
expect(names).to be_empty
|
||||
names = described_class.new(private_project, user).execute
|
||||
|
||||
expect(names).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'using a public project without a user' do
|
||||
it 'returns all the unique environment names' do
|
||||
names = described_class.new(project1).execute
|
||||
context 'with a non-member' do
|
||||
it 'does not return any environment names' do
|
||||
names = described_class.new(private_project, user).execute
|
||||
|
||||
expect(names).to eq(%w[gprd gstg])
|
||||
expect(names).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'using a private project without a user' do
|
||||
it 'does not return any environment names' do
|
||||
names = described_class.new(project2).execute
|
||||
context 'without a user' do
|
||||
it 'does not return any environment names' do
|
||||
names = described_class.new(private_project).execute
|
||||
|
||||
expect(names).to eq([])
|
||||
expect(names).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -38,8 +38,16 @@ jest.mock('@gitlab/ui/dist/components/base/popover/popover.js', () => ({
|
|||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
...Object.fromEntries(['target', 'triggers', 'placement'].map(prop => [prop, {}])),
|
||||
},
|
||||
render(h) {
|
||||
return h('div', this.$attrs, Object.keys(this.$slots).map(s => this.$slots[s]));
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
class: 'gl-popover',
|
||||
...this.$attrs,
|
||||
},
|
||||
Object.keys(this.$slots).map(s => this.$slots[s]),
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
|
|
@ -1,20 +1,19 @@
|
|||
import $ from 'jquery';
|
||||
import 'mousetrap';
|
||||
import Mousetrap from 'mousetrap';
|
||||
import initCopyAsGFM, { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm';
|
||||
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
|
||||
import { getSelectedFragment } from '~/lib/utils/common_utils';
|
||||
|
||||
const FORM_SELECTOR = '.js-main-target-form .js-vue-comment-form';
|
||||
|
||||
jest.mock('~/lib/utils/common_utils', () => ({
|
||||
...jest.requireActual('~/lib/utils/common_utils'),
|
||||
getSelectedFragment: jest.fn().mockName('getSelectedFragment'),
|
||||
}));
|
||||
|
||||
describe('ShortcutsIssuable', () => {
|
||||
const fixtureName = 'snippets/show.html';
|
||||
const snippetShowFixtureName = 'snippets/show.html';
|
||||
const mrShowFixtureName = 'merge_requests/merge_request_of_current_user.html';
|
||||
|
||||
preloadFixtures(fixtureName);
|
||||
preloadFixtures(snippetShowFixtureName, mrShowFixtureName);
|
||||
|
||||
beforeAll(done => {
|
||||
initCopyAsGFM();
|
||||
|
@ -27,25 +26,27 @@ describe('ShortcutsIssuable', () => {
|
|||
.catch(done.fail);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
loadFixtures(fixtureName);
|
||||
$('body').append(
|
||||
`<div class="js-main-target-form">
|
||||
<textarea class="js-vue-comment-form"></textarea>
|
||||
</div>`,
|
||||
);
|
||||
document.querySelector('.js-new-note-form').classList.add('js-main-target-form');
|
||||
|
||||
window.shortcut = new ShortcutsIssuable(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
$(FORM_SELECTOR).remove();
|
||||
|
||||
delete window.shortcut;
|
||||
});
|
||||
|
||||
describe('replyWithSelectedText', () => {
|
||||
const FORM_SELECTOR = '.js-main-target-form .js-vue-comment-form';
|
||||
|
||||
beforeEach(() => {
|
||||
loadFixtures(snippetShowFixtureName);
|
||||
$('body').append(
|
||||
`<div class="js-main-target-form">
|
||||
<textarea class="js-vue-comment-form"></textarea>
|
||||
</div>`,
|
||||
);
|
||||
document.querySelector('.js-new-note-form').classList.add('js-main-target-form');
|
||||
|
||||
window.shortcut = new ShortcutsIssuable(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
$(FORM_SELECTOR).remove();
|
||||
|
||||
delete window.shortcut;
|
||||
});
|
||||
|
||||
// Stub getSelectedFragment to return a node with the provided HTML.
|
||||
const stubSelection = (html, invalidNode) => {
|
||||
getSelectedFragment.mockImplementation(() => {
|
||||
|
@ -319,4 +320,55 @@ describe('ShortcutsIssuable', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('copyBranchName', () => {
|
||||
let sidebarCollapsedBtn;
|
||||
let sidebarExpandedBtn;
|
||||
|
||||
beforeEach(() => {
|
||||
loadFixtures(mrShowFixtureName);
|
||||
|
||||
window.shortcut = new ShortcutsIssuable();
|
||||
|
||||
[sidebarCollapsedBtn, sidebarExpandedBtn] = document.querySelectorAll(
|
||||
'.sidebar-source-branch button',
|
||||
);
|
||||
|
||||
[sidebarCollapsedBtn, sidebarExpandedBtn].forEach(btn => jest.spyOn(btn, 'click'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete window.shortcut;
|
||||
});
|
||||
|
||||
describe('when the sidebar is expanded', () => {
|
||||
beforeEach(() => {
|
||||
// simulate the applied CSS styles when the
|
||||
// sidebar is expanded
|
||||
sidebarCollapsedBtn.style.display = 'none';
|
||||
|
||||
Mousetrap.trigger('b');
|
||||
});
|
||||
|
||||
it('clicks the "expanded" version of the copy source branch button', () => {
|
||||
expect(sidebarExpandedBtn.click).toHaveBeenCalled();
|
||||
expect(sidebarCollapsedBtn.click).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the sidebar is collapsed', () => {
|
||||
beforeEach(() => {
|
||||
// simulate the applied CSS styles when the
|
||||
// sidebar is collapsed
|
||||
sidebarExpandedBtn.style.display = 'none';
|
||||
|
||||
Mousetrap.trigger('b');
|
||||
});
|
||||
|
||||
it('clicks the "collapsed" version of the copy source branch button', () => {
|
||||
expect(sidebarCollapsedBtn.click).toHaveBeenCalled();
|
||||
expect(sidebarExpandedBtn.click).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,203 +0,0 @@
|
|||
import $ from 'jquery';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import AjaxFormVariableList from '~/ci_variable_list/ajax_variable_list';
|
||||
|
||||
const VARIABLE_PATCH_ENDPOINT = 'http://test.host/frontend-fixtures/builds-project/-/variables';
|
||||
const HIDE_CLASS = 'hide';
|
||||
|
||||
describe('AjaxFormVariableList', () => {
|
||||
preloadFixtures('projects/ci_cd_settings.html');
|
||||
preloadFixtures('projects/ci_cd_settings_with_variables.html');
|
||||
|
||||
let container;
|
||||
let saveButton;
|
||||
let errorBox;
|
||||
|
||||
let mock;
|
||||
let ajaxVariableList;
|
||||
|
||||
beforeEach(() => {
|
||||
loadFixtures('projects/ci_cd_settings.html');
|
||||
container = document.querySelector('.js-ci-variable-list-section');
|
||||
|
||||
mock = new MockAdapter(axios);
|
||||
|
||||
const ajaxVariableListEl = document.querySelector('.js-ci-variable-list-section');
|
||||
saveButton = ajaxVariableListEl.querySelector('.js-ci-variables-save-button');
|
||||
errorBox = container.querySelector('.js-ci-variable-error-box');
|
||||
ajaxVariableList = new AjaxFormVariableList({
|
||||
container,
|
||||
formField: 'variables',
|
||||
saveButton,
|
||||
errorBox,
|
||||
saveEndpoint: container.dataset.saveEndpoint,
|
||||
maskableRegex: container.dataset.maskableRegex,
|
||||
});
|
||||
|
||||
jest.spyOn(ajaxVariableList, 'updateRowsWithPersistedVariables');
|
||||
jest.spyOn(ajaxVariableList.variableList, 'toggleEnableRow');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
describe('onSaveClicked', () => {
|
||||
it('shows loading spinner while waiting for the request', () => {
|
||||
const loadingIcon = saveButton.querySelector('.js-ci-variables-save-loading-icon');
|
||||
|
||||
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(() => {
|
||||
expect(loadingIcon.classList.contains(HIDE_CLASS)).toEqual(false);
|
||||
|
||||
return [200, {}];
|
||||
});
|
||||
|
||||
expect(loadingIcon.classList.contains(HIDE_CLASS)).toEqual(true);
|
||||
|
||||
return ajaxVariableList.onSaveClicked().then(() => {
|
||||
expect(loadingIcon.classList.contains(HIDE_CLASS)).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('calls `updateRowsWithPersistedVariables` with the persisted variables', () => {
|
||||
const variablesResponse = [{ id: 1, key: 'foo', value: 'bar' }];
|
||||
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(200, {
|
||||
variables: variablesResponse,
|
||||
});
|
||||
|
||||
return ajaxVariableList.onSaveClicked().then(() => {
|
||||
expect(ajaxVariableList.updateRowsWithPersistedVariables).toHaveBeenCalledWith(
|
||||
variablesResponse,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('hides any previous error box', () => {
|
||||
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(200);
|
||||
|
||||
expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true);
|
||||
|
||||
return ajaxVariableList.onSaveClicked().then(() => {
|
||||
expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('disables remove buttons while waiting for the request', () => {
|
||||
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(() => {
|
||||
expect(ajaxVariableList.variableList.toggleEnableRow).toHaveBeenCalledWith(false);
|
||||
|
||||
return [200, {}];
|
||||
});
|
||||
|
||||
return ajaxVariableList.onSaveClicked().then(() => {
|
||||
expect(ajaxVariableList.variableList.toggleEnableRow).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('hides secret values', () => {
|
||||
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(200, {});
|
||||
|
||||
const row = container.querySelector('.js-row');
|
||||
const valueInput = row.querySelector('.js-ci-variable-input-value');
|
||||
const valuePlaceholder = row.querySelector('.js-secret-value-placeholder');
|
||||
|
||||
valueInput.value = 'bar';
|
||||
$(valueInput).trigger('input');
|
||||
|
||||
expect(valuePlaceholder.classList.contains(HIDE_CLASS)).toBe(true);
|
||||
expect(valueInput.classList.contains(HIDE_CLASS)).toBe(false);
|
||||
|
||||
return ajaxVariableList.onSaveClicked().then(() => {
|
||||
expect(valuePlaceholder.classList.contains(HIDE_CLASS)).toBe(false);
|
||||
expect(valueInput.classList.contains(HIDE_CLASS)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('shows error box with validation errors', () => {
|
||||
const validationError = 'some validation error';
|
||||
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(400, [validationError]);
|
||||
|
||||
expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true);
|
||||
|
||||
return ajaxVariableList.onSaveClicked().then(() => {
|
||||
expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(false);
|
||||
expect(errorBox.textContent.trim().replace(/\n+\s+/m, ' ')).toEqual(
|
||||
`Validation failed ${validationError}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('shows flash message when request fails', () => {
|
||||
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(500);
|
||||
|
||||
expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true);
|
||||
|
||||
return ajaxVariableList.onSaveClicked().then(() => {
|
||||
expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateRowsWithPersistedVariables', () => {
|
||||
beforeEach(() => {
|
||||
loadFixtures('projects/ci_cd_settings_with_variables.html');
|
||||
container = document.querySelector('.js-ci-variable-list-section');
|
||||
|
||||
const ajaxVariableListEl = document.querySelector('.js-ci-variable-list-section');
|
||||
saveButton = ajaxVariableListEl.querySelector('.js-ci-variables-save-button');
|
||||
errorBox = container.querySelector('.js-ci-variable-error-box');
|
||||
ajaxVariableList = new AjaxFormVariableList({
|
||||
container,
|
||||
formField: 'variables',
|
||||
saveButton,
|
||||
errorBox,
|
||||
saveEndpoint: container.dataset.saveEndpoint,
|
||||
});
|
||||
});
|
||||
|
||||
it('removes variable that was removed', () => {
|
||||
expect(container.querySelectorAll('.js-row').length).toBe(3);
|
||||
|
||||
container.querySelector('.js-row-remove-button').click();
|
||||
|
||||
expect(container.querySelectorAll('.js-row').length).toBe(3);
|
||||
|
||||
ajaxVariableList.updateRowsWithPersistedVariables([]);
|
||||
|
||||
expect(container.querySelectorAll('.js-row').length).toBe(2);
|
||||
});
|
||||
|
||||
it('updates new variable row with persisted ID', () => {
|
||||
const row = container.querySelector('.js-row:last-child');
|
||||
const idInput = row.querySelector('.js-ci-variable-input-id');
|
||||
const keyInput = row.querySelector('.js-ci-variable-input-key');
|
||||
const valueInput = row.querySelector('.js-ci-variable-input-value');
|
||||
|
||||
keyInput.value = 'foo';
|
||||
$(keyInput).trigger('input');
|
||||
valueInput.value = 'bar';
|
||||
$(valueInput).trigger('input');
|
||||
|
||||
expect(idInput.value).toEqual('');
|
||||
|
||||
ajaxVariableList.updateRowsWithPersistedVariables([
|
||||
{
|
||||
id: 3,
|
||||
key: 'foo',
|
||||
value: 'bar',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(idInput.value).toEqual('3');
|
||||
expect(row.dataset.isPersisted).toEqual('true');
|
||||
});
|
||||
});
|
||||
|
||||
describe('maskableRegex', () => {
|
||||
it('takes in the regex provided by the data attribute', () => {
|
||||
expect(container.dataset.maskableRegex).toBe('^[a-zA-Z0-9_+=/@:.-]{8,}$');
|
||||
expect(ajaxVariableList.maskableRegex).toBe(container.dataset.maskableRegex);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,5 +1,4 @@
|
|||
import $ from 'jquery';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import VariableList from '~/ci_variable_list/ci_variable_list';
|
||||
|
||||
const HIDE_CLASS = 'hide';
|
||||
|
@ -7,7 +6,6 @@ const HIDE_CLASS = 'hide';
|
|||
describe('VariableList', () => {
|
||||
preloadFixtures('pipeline_schedules/edit.html');
|
||||
preloadFixtures('pipeline_schedules/edit_with_variables.html');
|
||||
preloadFixtures('projects/ci_cd_settings.html');
|
||||
|
||||
let $wrapper;
|
||||
let variableList;
|
||||
|
@ -113,92 +111,6 @@ describe('VariableList', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('with all inputs(key, value, protected)', () => {
|
||||
beforeEach(() => {
|
||||
loadFixtures('projects/ci_cd_settings.html');
|
||||
$wrapper = $('.js-ci-variable-list-section');
|
||||
|
||||
$wrapper.find('.js-ci-variable-input-protected').attr('data-default', 'false');
|
||||
|
||||
variableList = new VariableList({
|
||||
container: $wrapper,
|
||||
formField: 'variables',
|
||||
});
|
||||
variableList.init();
|
||||
});
|
||||
|
||||
it('should not add another row when editing the last rows protected checkbox', () => {
|
||||
const $row = $wrapper.find('.js-row:last-child');
|
||||
$row.find('.ci-variable-protected-item .js-project-feature-toggle').click();
|
||||
|
||||
return waitForPromises().then(() => {
|
||||
expect($wrapper.find('.js-row').length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not add another row when editing the last rows masked checkbox', () => {
|
||||
jest.spyOn(variableList, 'checkIfRowTouched');
|
||||
const $row = $wrapper.find('.js-row:last-child');
|
||||
$row.find('.ci-variable-masked-item .js-project-feature-toggle').click();
|
||||
|
||||
return waitForPromises().then(() => {
|
||||
// This validates that we are checking after the event listener has run
|
||||
expect(variableList.checkIfRowTouched).toHaveBeenCalled();
|
||||
expect($wrapper.find('.js-row').length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateMaskability', () => {
|
||||
let $row;
|
||||
|
||||
const maskingErrorElement = '.js-row:last-child .masking-validation-error';
|
||||
const clickToggle = () =>
|
||||
$row.find('.ci-variable-masked-item .js-project-feature-toggle').click();
|
||||
|
||||
beforeEach(() => {
|
||||
$row = $wrapper.find('.js-row:last-child');
|
||||
});
|
||||
|
||||
it('has a regex provided via a data attribute', () => {
|
||||
clickToggle();
|
||||
|
||||
expect($wrapper.attr('data-maskable-regex')).toBe('^[a-zA-Z0-9_+=/@:.-]{8,}$');
|
||||
});
|
||||
|
||||
it('allows values that are 8 characters long', () => {
|
||||
$row.find('.js-ci-variable-input-value').val('looooong');
|
||||
|
||||
clickToggle();
|
||||
|
||||
expect($wrapper.find(maskingErrorElement)).toHaveClass('hide');
|
||||
});
|
||||
|
||||
it('rejects values that are shorter than 8 characters', () => {
|
||||
$row.find('.js-ci-variable-input-value').val('short');
|
||||
|
||||
clickToggle();
|
||||
|
||||
expect($wrapper.find(maskingErrorElement)).toBeVisible();
|
||||
});
|
||||
|
||||
it('allows values with base 64 characters', () => {
|
||||
$row.find('.js-ci-variable-input-value').val('abcABC123_+=/-');
|
||||
|
||||
clickToggle();
|
||||
|
||||
expect($wrapper.find(maskingErrorElement)).toHaveClass('hide');
|
||||
});
|
||||
|
||||
it('rejects values with other special characters', () => {
|
||||
$row.find('.js-ci-variable-input-value').val('1234567$');
|
||||
|
||||
clickToggle();
|
||||
|
||||
expect($wrapper.find(maskingErrorElement)).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleEnableRow method', () => {
|
||||
beforeEach(() => {
|
||||
loadFixtures('pipeline_schedules/edit_with_variables.html');
|
||||
|
@ -247,36 +159,4 @@ describe('VariableList', () => {
|
|||
expect($wrapper.find('.js-ci-variable-input-key:not([disabled])').length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hideValues', () => {
|
||||
beforeEach(() => {
|
||||
loadFixtures('projects/ci_cd_settings.html');
|
||||
$wrapper = $('.js-ci-variable-list-section');
|
||||
|
||||
variableList = new VariableList({
|
||||
container: $wrapper,
|
||||
formField: 'variables',
|
||||
});
|
||||
variableList.init();
|
||||
});
|
||||
|
||||
it('should hide value input and show placeholder stars', () => {
|
||||
const $row = $wrapper.find('.js-row');
|
||||
const $inputValue = $row.find('.js-ci-variable-input-value');
|
||||
const $placeholder = $row.find('.js-secret-value-placeholder');
|
||||
|
||||
$row
|
||||
.find('.js-ci-variable-input-value')
|
||||
.val('foo')
|
||||
.trigger('input');
|
||||
|
||||
expect($placeholder.hasClass(HIDE_CLASS)).toBe(true);
|
||||
expect($inputValue.hasClass(HIDE_CLASS)).toBe(false);
|
||||
|
||||
variableList.hideValues();
|
||||
|
||||
expect($placeholder.hasClass(HIDE_CLASS)).toBe(false);
|
||||
expect($inputValue.hasClass(HIDE_CLASS)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,7 +15,6 @@ RSpec.describe 'Groups (JavaScript fixtures)', type: :controller do
|
|||
end
|
||||
|
||||
before do
|
||||
stub_feature_flags(new_variables_ui: false)
|
||||
group.add_maintainer(admin)
|
||||
sign_in(admin)
|
||||
end
|
||||
|
@ -27,12 +26,4 @@ RSpec.describe 'Groups (JavaScript fixtures)', type: :controller do
|
|||
expect(response).to be_successful
|
||||
end
|
||||
end
|
||||
|
||||
describe Groups::Settings::CiCdController, '(JavaScript fixtures)', type: :controller do
|
||||
it 'groups/ci_cd_settings.html' do
|
||||
get :show, params: { group_id: group }
|
||||
|
||||
expect(response).to be_successful
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -20,7 +20,6 @@ RSpec.describe 'Projects (JavaScript fixtures)', type: :controller do
|
|||
end
|
||||
|
||||
before do
|
||||
stub_feature_flags(new_variables_ui: false)
|
||||
project.add_maintainer(admin)
|
||||
sign_in(admin)
|
||||
allow(SecureRandom).to receive(:hex).and_return('securerandomhex:thereisnospoon')
|
||||
|
@ -58,27 +57,4 @@ RSpec.describe 'Projects (JavaScript fixtures)', type: :controller do
|
|||
expect(response).to be_successful
|
||||
end
|
||||
end
|
||||
|
||||
describe Projects::Settings::CiCdController, '(JavaScript fixtures)', type: :controller do
|
||||
it 'projects/ci_cd_settings.html' do
|
||||
get :show, params: {
|
||||
namespace_id: project.namespace.to_param,
|
||||
project_id: project
|
||||
}
|
||||
|
||||
expect(response).to be_successful
|
||||
end
|
||||
|
||||
it 'projects/ci_cd_settings_with_variables.html' do
|
||||
create(:ci_variable, project: project_variable_populated)
|
||||
create(:ci_variable, project: project_variable_populated)
|
||||
|
||||
get :show, params: {
|
||||
namespace_id: project_variable_populated.namespace.to_param,
|
||||
project_id: project_variable_populated
|
||||
}
|
||||
|
||||
expect(response).to be_successful
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import { GlLoadingIcon, GlButton } from '@gitlab/ui';
|
||||
import { TEST_HOST } from 'spec/test_constants';
|
||||
import TerminalEmptyState from '~/ide/components/terminal/empty_state.vue';
|
||||
|
||||
|
@ -36,7 +36,7 @@ describe('IDE TerminalEmptyState', () => {
|
|||
const img = wrapper.find('.svg-content img');
|
||||
|
||||
expect(img.exists()).toBe(true);
|
||||
expect(img.attributes('src')).toEqual(TEST_PATH);
|
||||
expect(img.attributes('src')).toBe(TEST_PATH);
|
||||
});
|
||||
|
||||
it('when loading, shows loading icon', () => {
|
||||
|
@ -71,24 +71,23 @@ describe('IDE TerminalEmptyState', () => {
|
|||
},
|
||||
});
|
||||
|
||||
button = wrapper.find('button');
|
||||
button = wrapper.find(GlButton);
|
||||
});
|
||||
|
||||
it('shows button', () => {
|
||||
expect(button.text()).toEqual('Start Web Terminal');
|
||||
expect(button.attributes('disabled')).toBeFalsy();
|
||||
expect(button.text()).toBe('Start Web Terminal');
|
||||
expect(button.props('disabled')).toBe(false);
|
||||
});
|
||||
|
||||
it('emits start when button is clicked', () => {
|
||||
expect(wrapper.emitted().start).toBeFalsy();
|
||||
|
||||
button.trigger('click');
|
||||
expect(wrapper.emitted().start).toBeUndefined();
|
||||
button.vm.$emit('click');
|
||||
|
||||
expect(wrapper.emitted().start).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('shows help path link', () => {
|
||||
expect(wrapper.find('a').attributes('href')).toEqual(TEST_HELP_PATH);
|
||||
expect(wrapper.find('a').attributes('href')).toBe(TEST_HELP_PATH);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -101,7 +100,7 @@ describe('IDE TerminalEmptyState', () => {
|
|||
},
|
||||
});
|
||||
|
||||
expect(wrapper.find('button').attributes('disabled')).not.toBe(null);
|
||||
expect(wrapper.find('.bs-callout').element.innerHTML).toEqual(TEST_HTML_MESSAGE);
|
||||
expect(wrapper.find(GlButton).props('disabled')).toBe(true);
|
||||
expect(wrapper.find('.bs-callout').element.innerHTML).toBe(TEST_HTML_MESSAGE);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,6 +3,8 @@ import {
|
|||
canScrollUp,
|
||||
canScrollDown,
|
||||
parseBooleanDataAttributes,
|
||||
isElementVisible,
|
||||
isElementHidden,
|
||||
} from '~/lib/utils/dom_utils';
|
||||
|
||||
const TEST_MARGIN = 5;
|
||||
|
@ -160,4 +162,35 @@ describe('DOM Utils', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe.each`
|
||||
offsetWidth | offsetHeight | clientRectsLength | visible
|
||||
${0} | ${0} | ${0} | ${false}
|
||||
${1} | ${0} | ${0} | ${true}
|
||||
${0} | ${1} | ${0} | ${true}
|
||||
${0} | ${0} | ${1} | ${true}
|
||||
`(
|
||||
'isElementVisible and isElementHidden',
|
||||
({ offsetWidth, offsetHeight, clientRectsLength, visible }) => {
|
||||
const element = {
|
||||
offsetWidth,
|
||||
offsetHeight,
|
||||
getClientRects: () => new Array(clientRectsLength),
|
||||
};
|
||||
|
||||
const paramDescription = `offsetWidth=${offsetWidth}, offsetHeight=${offsetHeight}, and getClientRects().length=${clientRectsLength}`;
|
||||
|
||||
describe('isElementVisible', () => {
|
||||
it(`returns ${visible} when ${paramDescription}`, () => {
|
||||
expect(isElementVisible(element)).toBe(visible);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isElementHidden', () => {
|
||||
it(`returns ${!visible} when ${paramDescription}`, () => {
|
||||
expect(isElementHidden(element)).toBe(!visible);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlPopover } from '@gitlab/ui';
|
||||
import { useMockMutationObserver } from 'helpers/mock_dom_observer';
|
||||
import Popovers from '~/popovers/components/popovers.vue';
|
||||
|
||||
describe('popovers/components/popovers.vue', () => {
|
||||
const { trigger: triggerMutate, observersCount } = useMockMutationObserver();
|
||||
let wrapper;
|
||||
|
||||
const buildWrapper = (...targets) => {
|
||||
wrapper = shallowMount(Popovers);
|
||||
wrapper.vm.addPopovers(targets);
|
||||
return wrapper.vm.$nextTick();
|
||||
};
|
||||
|
||||
const createPopoverTarget = (options = {}) => {
|
||||
const target = document.createElement('button');
|
||||
const dataset = {
|
||||
title: 'default title',
|
||||
content: 'some content',
|
||||
...options,
|
||||
};
|
||||
|
||||
Object.entries(dataset).forEach(([key, value]) => {
|
||||
target.dataset[key] = value;
|
||||
});
|
||||
|
||||
document.body.appendChild(target);
|
||||
|
||||
return target;
|
||||
};
|
||||
|
||||
const allPopovers = () => wrapper.findAll(GlPopover);
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
describe('addPopovers', () => {
|
||||
it('attaches popovers to the targets specified', async () => {
|
||||
const target = createPopoverTarget();
|
||||
await buildWrapper(target);
|
||||
expect(wrapper.find(GlPopover).props('target')).toBe(target);
|
||||
});
|
||||
|
||||
it('does not attach a popover twice to the same element', async () => {
|
||||
const target = createPopoverTarget();
|
||||
buildWrapper(target);
|
||||
wrapper.vm.addPopovers([target]);
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.findAll(GlPopover)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('supports HTML content', async () => {
|
||||
const content = 'content with <b>HTML</b>';
|
||||
await buildWrapper(
|
||||
createPopoverTarget({
|
||||
content,
|
||||
html: true,
|
||||
}),
|
||||
);
|
||||
const html = wrapper.find(GlPopover).html();
|
||||
|
||||
expect(html).toContain(content);
|
||||
});
|
||||
|
||||
it.each`
|
||||
option | value
|
||||
${'placement'} | ${'bottom'}
|
||||
${'triggers'} | ${'manual'}
|
||||
`('sets $option to $value when data-$option is set in target', async ({ option, value }) => {
|
||||
await buildWrapper(createPopoverTarget({ [option]: value }));
|
||||
|
||||
expect(wrapper.find(GlPopover).props(option)).toBe(value);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dispose', () => {
|
||||
it('removes all popovers when elements is nil', async () => {
|
||||
await buildWrapper(createPopoverTarget(), createPopoverTarget());
|
||||
|
||||
wrapper.vm.dispose();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(allPopovers()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('removes the popovers that target the elements specified', async () => {
|
||||
const target = createPopoverTarget();
|
||||
|
||||
await buildWrapper(target, createPopoverTarget());
|
||||
|
||||
wrapper.vm.dispose(target);
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(allPopovers()).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('observe', () => {
|
||||
it('removes popover when target is removed from the document', async () => {
|
||||
const target = createPopoverTarget();
|
||||
await buildWrapper(target);
|
||||
|
||||
wrapper.vm.addPopovers([target, createPopoverTarget()]);
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
triggerMutate(document.body, {
|
||||
entry: { removedNodes: [target] },
|
||||
options: { childList: true },
|
||||
});
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(allPopovers()).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('disconnects mutation observer on beforeDestroy', async () => {
|
||||
await buildWrapper(createPopoverTarget());
|
||||
|
||||
expect(observersCount()).toBe(1);
|
||||
|
||||
wrapper.destroy();
|
||||
expect(observersCount()).toBe(0);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,104 @@
|
|||
import { initPopovers, dispose, destroy } from '~/popovers';
|
||||
|
||||
describe('popovers/index.js', () => {
|
||||
let popoversApp;
|
||||
|
||||
const createPopoverTarget = (trigger = 'hover') => {
|
||||
const target = document.createElement('button');
|
||||
const dataset = {
|
||||
title: 'default title',
|
||||
content: 'some content',
|
||||
toggle: 'popover',
|
||||
trigger,
|
||||
};
|
||||
|
||||
Object.entries(dataset).forEach(([key, value]) => {
|
||||
target.dataset[key] = value;
|
||||
});
|
||||
|
||||
document.body.appendChild(target);
|
||||
|
||||
return target;
|
||||
};
|
||||
|
||||
const buildPopoversApp = () => {
|
||||
popoversApp = initPopovers('[data-toggle="popover"]');
|
||||
};
|
||||
|
||||
const triggerEvent = (target, eventName = 'mouseenter') => {
|
||||
const event = new Event(eventName);
|
||||
|
||||
target.dispatchEvent(event);
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
destroy();
|
||||
});
|
||||
|
||||
describe('initPopover', () => {
|
||||
it('attaches a GlPopover for the elements specified in the selector', async () => {
|
||||
const target = createPopoverTarget();
|
||||
|
||||
buildPopoversApp();
|
||||
|
||||
triggerEvent(target);
|
||||
|
||||
await popoversApp.$nextTick();
|
||||
const html = document.querySelector('.gl-popover').innerHTML;
|
||||
|
||||
expect(document.querySelector('.gl-popover')).not.toBe(null);
|
||||
expect(html).toContain('default title');
|
||||
expect(html).toContain('some content');
|
||||
});
|
||||
|
||||
it('supports triggering a popover via custom events', async () => {
|
||||
const trigger = 'click';
|
||||
const target = createPopoverTarget(trigger);
|
||||
|
||||
buildPopoversApp();
|
||||
triggerEvent(target, trigger);
|
||||
|
||||
await popoversApp.$nextTick();
|
||||
|
||||
expect(document.querySelector('.gl-popover')).not.toBe(null);
|
||||
expect(document.querySelector('.gl-popover').innerHTML).toContain('default title');
|
||||
});
|
||||
|
||||
it('inits popovers on targets added after content load', async () => {
|
||||
buildPopoversApp();
|
||||
|
||||
expect(document.querySelector('.gl-popover')).toBe(null);
|
||||
|
||||
const trigger = 'click';
|
||||
const target = createPopoverTarget(trigger);
|
||||
triggerEvent(target, trigger);
|
||||
await popoversApp.$nextTick();
|
||||
|
||||
expect(document.querySelector('.gl-popover')).not.toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dispose', () => {
|
||||
it('removes popovers that target the elements specified', async () => {
|
||||
const fakeTarget = createPopoverTarget();
|
||||
const target = createPopoverTarget();
|
||||
buildPopoversApp();
|
||||
triggerEvent(target);
|
||||
triggerEvent(createPopoverTarget());
|
||||
await popoversApp.$nextTick();
|
||||
|
||||
expect(document.querySelectorAll('.gl-popover')).toHaveLength(2);
|
||||
|
||||
dispose([fakeTarget]);
|
||||
await popoversApp.$nextTick();
|
||||
|
||||
expect(document.querySelectorAll('.gl-popover')).toHaveLength(2);
|
||||
|
||||
dispose([target]);
|
||||
await popoversApp.$nextTick();
|
||||
|
||||
expect(document.querySelectorAll('.gl-popover')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,13 +1,7 @@
|
|||
import Vue from 'vue';
|
||||
import Mousetrap from 'mousetrap';
|
||||
import mountComponent from 'helpers/vue_mount_component_helper';
|
||||
import headerComponent from '~/vue_merge_request_widget/components/mr_widget_header.vue';
|
||||
|
||||
jest.mock('mousetrap', () => ({
|
||||
bind: jest.fn(),
|
||||
unbind: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('MRWidgetHeader', () => {
|
||||
let vm;
|
||||
let Component;
|
||||
|
@ -136,35 +130,6 @@ describe('MRWidgetHeader', () => {
|
|||
it('renders target branch', () => {
|
||||
expect(vm.$el.querySelector('.js-target-branch').textContent.trim()).toEqual('master');
|
||||
});
|
||||
|
||||
describe('keyboard shortcuts', () => {
|
||||
it('binds a keyboard shortcut handler to the "b" key', () => {
|
||||
expect(Mousetrap.bind).toHaveBeenCalledWith('b', expect.any(Function));
|
||||
});
|
||||
|
||||
it('triggers a click on the "copy to clipboard" button when the handler is executed', () => {
|
||||
const testClickHandler = jest.fn();
|
||||
vm.$refs.copyBranchNameButton.$el.addEventListener('click', testClickHandler);
|
||||
|
||||
// Get a reference to the function that was assigned to the "b" shortcut key.
|
||||
const shortcutHandler = Mousetrap.bind.mock.calls[0][1];
|
||||
|
||||
expect(testClickHandler).not.toHaveBeenCalled();
|
||||
|
||||
// Simulate Mousetrap calling the function.
|
||||
shortcutHandler();
|
||||
|
||||
expect(testClickHandler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('unbinds the keyboard shortcut when the component is destroyed', () => {
|
||||
expect(Mousetrap.unbind).not.toHaveBeenCalled();
|
||||
|
||||
vm.$destroy();
|
||||
|
||||
expect(Mousetrap.unbind).toHaveBeenCalledWith('b');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an open merge request', () => {
|
||||
|
|
|
@ -82,16 +82,6 @@ RSpec.describe PageLayoutHelper do
|
|||
expect(helper.page_image).to match_asset_path 'assets/gitlab_logo.png'
|
||||
end
|
||||
end
|
||||
|
||||
context 'if avatar_with_host is disabled' do
|
||||
it "#{type.titlecase} does not generate avatar full url" do
|
||||
stub_feature_flags(avatar_with_host: false)
|
||||
|
||||
assign(type, object)
|
||||
|
||||
expect(helper.page_image).to eq object.avatar_url(only_path: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require Rails.root.join('db', 'post_migrate', '20190924152703_migrate_issue_trackers_data.rb')
|
||||
require_migration!('migrate_issue_trackers_data')
|
||||
|
||||
RSpec.describe MigrateIssueTrackersData do
|
||||
let(:services) { table(:services) }
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require Rails.root.join('db', 'post_migrate', '20191015154408_drop_merge_requests_require_code_owner_approval_from_projects.rb')
|
||||
require_migration!('drop_merge_requests_require_code_owner_approval_from_projects')
|
||||
|
||||
RSpec.describe DropMergeRequestsRequireCodeOwnerApprovalFromProjects do
|
||||
let(:projects_table) { table(:projects) }
|
||||
|
|
|
@ -13,7 +13,8 @@ RSpec.describe 'Robots.txt Requests', :aggregate_failures do
|
|||
|
||||
it 'allows the requests' do
|
||||
requests = [
|
||||
'/users/sign_in'
|
||||
'/users/sign_in',
|
||||
'/namespace/subnamespace/design.gitlab.com'
|
||||
]
|
||||
|
||||
requests.each do |request|
|
||||
|
@ -60,7 +61,8 @@ RSpec.describe 'Robots.txt Requests', :aggregate_failures do
|
|||
'/foo/bar/protected_branches',
|
||||
'/foo/bar/uploads/foo',
|
||||
'/foo/bar/project_members',
|
||||
'/foo/bar/settings'
|
||||
'/foo/bar/settings',
|
||||
'/namespace/subnamespace/design.gitlab.com/settings'
|
||||
]
|
||||
|
||||
requests.each do |request|
|
||||
|
|
|
@ -3,23 +3,42 @@
|
|||
require 'find'
|
||||
|
||||
class RequireMigration
|
||||
MIGRATION_FOLDERS = %w(db/migrate db/post_migrate ee/db/geo/migrate ee/db/geo/post_migrate).freeze
|
||||
class AutoLoadError < RuntimeError
|
||||
MESSAGE = "Can not find any migration file for `%{file_name}`!\n" \
|
||||
"You can try to provide the migration file name manually."
|
||||
|
||||
def initialize(file_name)
|
||||
message = format(MESSAGE, file_name: file_name)
|
||||
|
||||
super(message)
|
||||
end
|
||||
end
|
||||
|
||||
FOSS_MIGRATION_FOLDERS = %w[db/migrate db/post_migrate].freeze
|
||||
ALL_MIGRATION_FOLDERS = (FOSS_MIGRATION_FOLDERS + %w[ee/db/geo/migrate ee/db/geo/post_migrate]).freeze
|
||||
SPEC_FILE_PATTERN = /.+\/(?<file_name>.+)_spec\.rb/.freeze
|
||||
|
||||
class << self
|
||||
def require_migration!(file_name)
|
||||
file_paths = search_migration_file(file_name)
|
||||
raise AutoLoadError.new(file_name) unless file_paths.first
|
||||
|
||||
require file_paths.first
|
||||
end
|
||||
|
||||
def search_migration_file(file_name)
|
||||
MIGRATION_FOLDERS.flat_map do |path|
|
||||
migration_folders.flat_map do |path|
|
||||
migration_path = Rails.root.join(path).to_s
|
||||
|
||||
Find.find(migration_path).grep(/\d+_#{file_name}\.rb/)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def migration_folders
|
||||
Gitlab.ee? ? ALL_MIGRATION_FOLDERS : FOSS_MIGRATION_FOLDERS
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,72 +1,245 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'variable list' do
|
||||
it 'shows list of variables' do
|
||||
page.within('.js-ci-variable-list-section') do
|
||||
expect(first('.js-ci-variable-input-key').value).to eq(variable.key)
|
||||
it 'shows a list of variables' do
|
||||
page.within('.ci-variable-table') do
|
||||
expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq(variable.key)
|
||||
end
|
||||
end
|
||||
|
||||
it 'adds new CI variable' do
|
||||
page.within('.js-ci-variable-list-section .js-row:last-child') do
|
||||
find('.js-ci-variable-input-key').set('key')
|
||||
find('.js-ci-variable-input-value').set('key_value')
|
||||
it 'adds a new CI variable' do
|
||||
click_button('Add Variable')
|
||||
|
||||
fill_variable('key', 'key_value') do
|
||||
click_button('Add variable')
|
||||
end
|
||||
|
||||
click_button('Save variables')
|
||||
wait_for_requests
|
||||
|
||||
visit page_path
|
||||
|
||||
# We check the first row because it re-sorts to alphabetical order on refresh
|
||||
page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do
|
||||
expect(find('.js-ci-variable-input-key').value).to eq('key')
|
||||
expect(find('.js-ci-variable-input-value', visible: false).value).to eq('key_value')
|
||||
page.within('.ci-variable-table') do
|
||||
expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq('key')
|
||||
end
|
||||
end
|
||||
|
||||
it 'adds a new protected variable' do
|
||||
page.within('.js-ci-variable-list-section .js-row:last-child') do
|
||||
find('.js-ci-variable-input-key').set('key')
|
||||
find('.js-ci-variable-input-value').set('key_value')
|
||||
click_button('Add Variable')
|
||||
|
||||
expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true')
|
||||
fill_variable('key', 'key_value') do
|
||||
click_button('Add variable')
|
||||
end
|
||||
|
||||
click_button('Save variables')
|
||||
wait_for_requests
|
||||
|
||||
visit page_path
|
||||
|
||||
# We check the first row because it re-sorts to alphabetical order on refresh
|
||||
page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do
|
||||
expect(find('.js-ci-variable-input-key').value).to eq('key')
|
||||
expect(find('.js-ci-variable-input-value', visible: false).value).to eq('key_value')
|
||||
expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true')
|
||||
page.within('.ci-variable-table') do
|
||||
expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq('key')
|
||||
expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Protected"] svg[data-testid="mobile-issue-close-icon"]')).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
it 'defaults to unmasked' do
|
||||
page.within('.js-ci-variable-list-section .js-row:last-child') do
|
||||
find('.js-ci-variable-input-key').set('key')
|
||||
find('.js-ci-variable-input-value').set('key_value')
|
||||
click_button('Add Variable')
|
||||
|
||||
expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false')
|
||||
fill_variable('key', 'key_value') do
|
||||
click_button('Add variable')
|
||||
end
|
||||
|
||||
click_button('Save variables')
|
||||
wait_for_requests
|
||||
|
||||
visit page_path
|
||||
|
||||
# We check the first row because it re-sorts to alphabetical order on refresh
|
||||
page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do
|
||||
expect(find('.js-ci-variable-input-key').value).to eq('key')
|
||||
expect(find('.js-ci-variable-input-value', visible: false).value).to eq('key_value')
|
||||
expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false')
|
||||
page.within('.ci-variable-table') do
|
||||
expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq('key')
|
||||
expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Masked"] svg[data-testid="close-icon"]')).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
it 'reveals and hides variables' do
|
||||
page.within('.ci-variable-table') do
|
||||
expect(first('.js-ci-variable-row td[data-label="Key"]').text).to eq(variable.key)
|
||||
expect(page).to have_content('*' * 17)
|
||||
|
||||
click_button('Reveal value')
|
||||
|
||||
expect(first('.js-ci-variable-row td[data-label="Key"]').text).to eq(variable.key)
|
||||
expect(first('.js-ci-variable-row td[data-label="Value"]').text).to eq(variable.value)
|
||||
expect(page).not_to have_content('*' * 17)
|
||||
|
||||
click_button('Hide value')
|
||||
|
||||
expect(first('.js-ci-variable-row td[data-label="Key"]').text).to eq(variable.key)
|
||||
expect(page).to have_content('*' * 17)
|
||||
end
|
||||
end
|
||||
|
||||
it 'deletes a variable' do
|
||||
expect(page).to have_selector('.js-ci-variable-row', count: 1)
|
||||
|
||||
page.within('.ci-variable-table') do
|
||||
click_button('Edit')
|
||||
end
|
||||
|
||||
page.within('#add-ci-variable') do
|
||||
click_button('Delete variable')
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(first('.js-ci-variable-row').text).to eq('There are no variables yet.')
|
||||
end
|
||||
|
||||
it 'edits a variable' do
|
||||
page.within('.ci-variable-table') do
|
||||
click_button('Edit')
|
||||
end
|
||||
|
||||
page.within('#add-ci-variable') do
|
||||
find('[data-qa-selector="ci_variable_key_field"] input').set('new_key')
|
||||
|
||||
click_button('Update variable')
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(first('.js-ci-variable-row td[data-label="Key"]').text).to eq('new_key')
|
||||
end
|
||||
|
||||
it 'edits a variable to be unmasked' do
|
||||
page.within('.ci-variable-table') do
|
||||
click_button('Edit')
|
||||
end
|
||||
|
||||
page.within('#add-ci-variable') do
|
||||
find('[data-testid="ci-variable-protected-checkbox"]').click
|
||||
find('[data-testid="ci-variable-masked-checkbox"]').click
|
||||
|
||||
click_button('Update variable')
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
||||
page.within('.ci-variable-table') do
|
||||
expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Masked"] svg[data-testid="close-icon"]')).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
it 'edits a variable to be masked' do
|
||||
page.within('.ci-variable-table') do
|
||||
click_button('Edit')
|
||||
end
|
||||
|
||||
page.within('#add-ci-variable') do
|
||||
find('[data-testid="ci-variable-masked-checkbox"]').click
|
||||
|
||||
click_button('Update variable')
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
||||
page.within('.ci-variable-table') do
|
||||
click_button('Edit')
|
||||
end
|
||||
|
||||
page.within('#add-ci-variable') do
|
||||
find('[data-testid="ci-variable-masked-checkbox"]').click
|
||||
|
||||
click_button('Update variable')
|
||||
end
|
||||
|
||||
page.within('.ci-variable-table') do
|
||||
expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Masked"] svg[data-testid="mobile-issue-close-icon"]')).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
it 'shows a validation error box about duplicate keys' do
|
||||
click_button('Add Variable')
|
||||
|
||||
fill_variable('key', 'key_value') do
|
||||
click_button('Add variable')
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
||||
click_button('Add Variable')
|
||||
|
||||
fill_variable('key', 'key_value') do
|
||||
click_button('Add variable')
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(find('.flash-container')).to be_present
|
||||
expect(find('.flash-text').text).to have_content('Variables key (key) has already been taken')
|
||||
end
|
||||
|
||||
it 'prevents a variable to be added if no values are provided when a variable is set to masked' do
|
||||
click_button('Add Variable')
|
||||
|
||||
page.within('#add-ci-variable') do
|
||||
find('[data-qa-selector="ci_variable_key_field"] input').set('empty_mask_key')
|
||||
find('[data-testid="ci-variable-protected-checkbox"]').click
|
||||
find('[data-testid="ci-variable-masked-checkbox"]').click
|
||||
|
||||
expect(find_button('Add variable', disabled: true)).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
it 'shows validation error box about unmaskable values' do
|
||||
click_button('Add Variable')
|
||||
|
||||
fill_variable('empty_mask_key', '???', protected: true, masked: true) do
|
||||
expect(page).to have_content('This variable can not be masked')
|
||||
expect(find_button('Add variable', disabled: true)).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
it 'handles multiple edits and a deletion' do
|
||||
# Create two variables
|
||||
click_button('Add Variable')
|
||||
|
||||
fill_variable('akey', 'akeyvalue') do
|
||||
click_button('Add variable')
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
||||
click_button('Add Variable')
|
||||
|
||||
fill_variable('zkey', 'zkeyvalue') do
|
||||
click_button('Add variable')
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_selector('.js-ci-variable-row', count: 3)
|
||||
|
||||
# Remove the `akey` variable
|
||||
page.within('.ci-variable-table') do
|
||||
page.within('.js-ci-variable-row:first-child') do
|
||||
click_button('Edit')
|
||||
end
|
||||
end
|
||||
|
||||
page.within('#add-ci-variable') do
|
||||
click_button('Delete variable')
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
||||
# Add another variable
|
||||
click_button('Add Variable')
|
||||
|
||||
fill_variable('ckey', 'ckeyvalue') do
|
||||
click_button('Add variable')
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
||||
# expect to find 3 rows of variables in alphabetical order
|
||||
expect(page).to have_selector('.js-ci-variable-row', count: 3)
|
||||
rows = all('.js-ci-variable-row')
|
||||
expect(rows[0].find('td[data-label="Key"]').text).to eq('ckey')
|
||||
expect(rows[1].find('td[data-label="Key"]').text).to eq('test_key')
|
||||
expect(rows[2].find('td[data-label="Key"]').text).to eq('zkey')
|
||||
end
|
||||
|
||||
context 'defaults to the application setting' do
|
||||
context 'application setting is true' do
|
||||
before do
|
||||
|
@ -76,13 +249,11 @@ RSpec.shared_examples 'variable list' do
|
|||
end
|
||||
|
||||
it 'defaults to protected' do
|
||||
page.within('.js-ci-variable-list-section .js-row:last-child') do
|
||||
find('.js-ci-variable-input-key').set('key')
|
||||
click_button('Add Variable')
|
||||
|
||||
page.within('#add-ci-variable') do
|
||||
expect(find('[data-testid="ci-variable-protected-checkbox"]')).to be_checked
|
||||
end
|
||||
|
||||
values = all('.js-ci-variable-input-protected', visible: false).map(&:value)
|
||||
|
||||
expect(values).to eq %w(false true true)
|
||||
end
|
||||
|
||||
it 'shows a message regarding the changed default' do
|
||||
|
@ -98,13 +269,11 @@ RSpec.shared_examples 'variable list' do
|
|||
end
|
||||
|
||||
it 'defaults to unprotected' do
|
||||
page.within('.js-ci-variable-list-section .js-row:last-child') do
|
||||
find('.js-ci-variable-input-key').set('key')
|
||||
click_button('Add Variable')
|
||||
|
||||
page.within('#add-ci-variable') do
|
||||
expect(find('[data-testid="ci-variable-protected-checkbox"]')).not_to be_checked
|
||||
end
|
||||
|
||||
values = all('.js-ci-variable-input-protected', visible: false).map(&:value)
|
||||
|
||||
expect(values).to eq %w(false false false)
|
||||
end
|
||||
|
||||
it 'does not show a message regarding the default' do
|
||||
|
@ -113,275 +282,14 @@ RSpec.shared_examples 'variable list' do
|
|||
end
|
||||
end
|
||||
|
||||
it 'reveals and hides variables' do
|
||||
page.within('.js-ci-variable-list-section') do
|
||||
expect(first('.js-ci-variable-input-key').value).to eq(variable.key)
|
||||
expect(first('.js-ci-variable-input-value', visible: false).value).to eq(variable.value)
|
||||
expect(page).to have_content('*' * 17)
|
||||
|
||||
click_button('Reveal value')
|
||||
|
||||
expect(first('.js-ci-variable-input-key').value).to eq(variable.key)
|
||||
expect(first('.js-ci-variable-input-value').value).to eq(variable.value)
|
||||
expect(page).not_to have_content('*' * 17)
|
||||
|
||||
click_button('Hide value')
|
||||
|
||||
expect(first('.js-ci-variable-input-key').value).to eq(variable.key)
|
||||
expect(first('.js-ci-variable-input-value', visible: false).value).to eq(variable.value)
|
||||
expect(page).to have_content('*' * 17)
|
||||
end
|
||||
end
|
||||
|
||||
it 'deletes variable' do
|
||||
page.within('.js-ci-variable-list-section') do
|
||||
expect(page).to have_selector('.js-row', count: 2)
|
||||
|
||||
first('.js-row-remove-button').click
|
||||
|
||||
click_button('Save variables')
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_selector('.js-row', count: 1)
|
||||
end
|
||||
end
|
||||
|
||||
it 'edits variable' do
|
||||
page.within('.js-ci-variable-list-section') do
|
||||
click_button('Reveal value')
|
||||
|
||||
page.within('.js-row:nth-child(2)') do
|
||||
find('.js-ci-variable-input-key').set('new_key')
|
||||
find('.js-ci-variable-input-value').set('new_value')
|
||||
end
|
||||
|
||||
click_button('Save variables')
|
||||
wait_for_requests
|
||||
|
||||
visit page_path
|
||||
|
||||
page.within('.js-row:nth-child(2)') do
|
||||
expect(find('.js-ci-variable-input-key').value).to eq('new_key')
|
||||
expect(find('.js-ci-variable-input-value', visible: false).value).to eq('new_value')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'edits variable to be protected' do
|
||||
# Create the unprotected variable
|
||||
page.within('.js-ci-variable-list-section .js-row:last-child') do
|
||||
find('.js-ci-variable-input-key').set('unprotected_key')
|
||||
find('.js-ci-variable-input-value').set('unprotected_value')
|
||||
find('.ci-variable-protected-item .js-project-feature-toggle').click
|
||||
|
||||
expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('false')
|
||||
end
|
||||
|
||||
click_button('Save variables')
|
||||
wait_for_requests
|
||||
|
||||
visit page_path
|
||||
|
||||
# We check the first row because it re-sorts to alphabetical order on refresh
|
||||
page.within('.js-ci-variable-list-section .js-row:nth-child(3)') do
|
||||
find('.ci-variable-protected-item .js-project-feature-toggle').click
|
||||
|
||||
expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true')
|
||||
end
|
||||
|
||||
click_button('Save variables')
|
||||
wait_for_requests
|
||||
|
||||
visit page_path
|
||||
|
||||
# We check the first row because it re-sorts to alphabetical order on refresh
|
||||
page.within('.js-ci-variable-list-section .js-row:nth-child(3)') do
|
||||
expect(find('.js-ci-variable-input-key').value).to eq('unprotected_key')
|
||||
expect(find('.js-ci-variable-input-value', visible: false).value).to eq('unprotected_value')
|
||||
expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true')
|
||||
end
|
||||
end
|
||||
|
||||
it 'edits variable to be unprotected' do
|
||||
# Create the protected variable
|
||||
page.within('.js-ci-variable-list-section .js-row:last-child') do
|
||||
find('.js-ci-variable-input-key').set('protected_key')
|
||||
find('.js-ci-variable-input-value').set('protected_value')
|
||||
|
||||
expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true')
|
||||
end
|
||||
|
||||
click_button('Save variables')
|
||||
wait_for_requests
|
||||
|
||||
visit page_path
|
||||
|
||||
page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do
|
||||
find('.ci-variable-protected-item .js-project-feature-toggle').click
|
||||
|
||||
expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('false')
|
||||
end
|
||||
|
||||
click_button('Save variables')
|
||||
wait_for_requests
|
||||
|
||||
visit page_path
|
||||
|
||||
page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do
|
||||
expect(find('.js-ci-variable-input-key').value).to eq('protected_key')
|
||||
expect(find('.js-ci-variable-input-value', visible: false).value).to eq('protected_value')
|
||||
expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('false')
|
||||
end
|
||||
end
|
||||
|
||||
it 'edits variable to be unmasked' do
|
||||
page.within('.js-ci-variable-list-section .js-row:last-child') do
|
||||
find('.js-ci-variable-input-key').set('unmasked_key')
|
||||
find('.js-ci-variable-input-value').set('unmasked_value')
|
||||
expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false')
|
||||
|
||||
find('.ci-variable-masked-item .js-project-feature-toggle').click
|
||||
|
||||
expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true')
|
||||
end
|
||||
|
||||
click_button('Save variables')
|
||||
wait_for_requests
|
||||
|
||||
visit page_path
|
||||
|
||||
page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do
|
||||
expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true')
|
||||
|
||||
find('.ci-variable-masked-item .js-project-feature-toggle').click
|
||||
|
||||
expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false')
|
||||
end
|
||||
|
||||
click_button('Save variables')
|
||||
wait_for_requests
|
||||
|
||||
visit page_path
|
||||
|
||||
page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do
|
||||
expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false')
|
||||
end
|
||||
end
|
||||
|
||||
it 'edits variable to be masked' do
|
||||
page.within('.js-ci-variable-list-section .js-row:last-child') do
|
||||
find('.js-ci-variable-input-key').set('masked_key')
|
||||
find('.js-ci-variable-input-value').set('masked_value')
|
||||
expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false')
|
||||
|
||||
find('.ci-variable-masked-item .js-project-feature-toggle').click
|
||||
|
||||
expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true')
|
||||
end
|
||||
|
||||
click_button('Save variables')
|
||||
wait_for_requests
|
||||
|
||||
visit page_path
|
||||
|
||||
page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do
|
||||
expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true')
|
||||
end
|
||||
end
|
||||
|
||||
it 'handles multiple edits and deletion in the middle' do
|
||||
page.within('.js-ci-variable-list-section') do
|
||||
# Create 2 variables
|
||||
page.within('.js-row:last-child') do
|
||||
find('.js-ci-variable-input-key').set('akey')
|
||||
find('.js-ci-variable-input-value').set('akeyvalue')
|
||||
end
|
||||
page.within('.js-row:last-child') do
|
||||
find('.js-ci-variable-input-key').set('zkey')
|
||||
find('.js-ci-variable-input-value').set('zkeyvalue')
|
||||
end
|
||||
|
||||
click_button('Save variables')
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_selector('.js-row', count: 4)
|
||||
|
||||
# Remove the `akey` variable
|
||||
page.within('.js-row:nth-child(3)') do
|
||||
first('.js-row-remove-button').click
|
||||
end
|
||||
|
||||
# Add another variable
|
||||
page.within('.js-row:last-child') do
|
||||
find('.js-ci-variable-input-key').set('ckey')
|
||||
find('.js-ci-variable-input-value').set('ckeyvalue')
|
||||
end
|
||||
|
||||
click_button('Save variables')
|
||||
wait_for_requests
|
||||
|
||||
visit page_path
|
||||
|
||||
# Expect to find 3 variables(4 rows) in alphbetical order
|
||||
expect(page).to have_selector('.js-row', count: 4)
|
||||
row_keys = all('.js-ci-variable-input-key')
|
||||
expect(row_keys[0].value).to eq('ckey')
|
||||
expect(row_keys[1].value).to eq('test_key')
|
||||
expect(row_keys[2].value).to eq('zkey')
|
||||
expect(row_keys[3].value).to eq('')
|
||||
end
|
||||
end
|
||||
|
||||
it 'shows validation error box about duplicate keys' do
|
||||
page.within('.js-ci-variable-list-section .js-row:last-child') do
|
||||
find('.js-ci-variable-input-key').set('samekey')
|
||||
find('.js-ci-variable-input-value').set('value123')
|
||||
end
|
||||
page.within('.js-ci-variable-list-section .js-row:last-child') do
|
||||
find('.js-ci-variable-input-key').set('samekey')
|
||||
find('.js-ci-variable-input-value').set('value456')
|
||||
end
|
||||
|
||||
click_button('Save variables')
|
||||
wait_for_requests
|
||||
|
||||
expect(all('.js-ci-variable-list-section .js-ci-variable-error-box ul li').count).to eq(1)
|
||||
|
||||
# We check the first row because it re-sorts to alphabetical order on refresh
|
||||
page.within('.js-ci-variable-list-section') do
|
||||
expect(find('.js-ci-variable-error-box')).to have_content(/Validation failed Variables have duplicate values \(.+\)/)
|
||||
end
|
||||
end
|
||||
|
||||
it 'shows validation error box about masking empty values' do
|
||||
page.within('.js-ci-variable-list-section .js-row:last-child') do
|
||||
find('.js-ci-variable-input-key').set('empty_value')
|
||||
find('.js-ci-variable-input-value').set('')
|
||||
find('.ci-variable-masked-item .js-project-feature-toggle').click
|
||||
end
|
||||
|
||||
click_button('Save variables')
|
||||
wait_for_requests
|
||||
|
||||
page.within('.js-ci-variable-list-section') do
|
||||
expect(all('.js-ci-variable-error-box ul li').count).to eq(1)
|
||||
expect(find('.js-ci-variable-error-box')).to have_content(/Validation failed Variables value is invalid/)
|
||||
end
|
||||
end
|
||||
|
||||
it 'shows validation error box about unmaskable values' do
|
||||
page.within('.js-ci-variable-list-section .js-row:last-child') do
|
||||
find('.js-ci-variable-input-key').set('unmaskable_value')
|
||||
find('.js-ci-variable-input-value').set('???')
|
||||
find('.ci-variable-masked-item .js-project-feature-toggle').click
|
||||
end
|
||||
|
||||
click_button('Save variables')
|
||||
wait_for_requests
|
||||
|
||||
page.within('.js-ci-variable-list-section') do
|
||||
expect(all('.js-ci-variable-error-box ul li').count).to eq(1)
|
||||
expect(find('.js-ci-variable-error-box')).to have_content(/Validation failed Variables value is invalid/)
|
||||
def fill_variable(key, value, protected: false, masked: false)
|
||||
page.within('#add-ci-variable') do
|
||||
find('[data-qa-selector="ci_variable_key_field"] input').set(key)
|
||||
find('[data-qa-selector="ci_variable_value_field"]').set(value) if value.present?
|
||||
find('[data-testid="ci-variable-protected-checkbox"]').click if protected
|
||||
find('[data-testid="ci-variable-masked-checkbox"]').click if masked
|
||||
|
||||
yield
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue