提交 fc53ce8e 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 8e22ef10
......@@ -35,10 +35,10 @@ export default {
title() {
const timeago = getTimeago();
const { timeDifference, standardDateFormat } = this;
const formatedDate = standardDateFormat;
const formattedDate = standardDateFormat;
if (timeDifference >= -1 && timeDifference < 7) {
return `${timeago.format(this.issueDueDate)} (${formatedDate})`;
return `${timeago.format(this.issueDueDate)} (${formattedDate})`;
}
return timeago.format(this.issueDueDate);
......
......@@ -159,7 +159,7 @@ export default {
<div role="rowheader" class="table-mobile-header">{{ __('Created') }}</div>
<div class="table-mobile-content text-secondary key-created-at">
<span v-tooltip :title="tooltipTitle(deployKey.created_at)">
<icon name="calendar" /> <span>{{ timeFormated(deployKey.created_at) }}</span>
<icon name="calendar" /> <span>{{ timeFormatted(deployKey.created_at) }}</span>
</span>
</div>
</div>
......
......@@ -177,6 +177,7 @@ export default {
projectPath: this.projectPath,
dismissEndpoint: this.dismissEndpoint,
showSuggestPopover: this.showSuggestPopover,
useSingleDiffStyle: this.glFeatures.singleMrDiffView,
});
if (this.shouldShow) {
......
......@@ -46,6 +46,7 @@ export const setBaseConfig = ({ commit }, options) => {
projectPath,
dismissEndpoint,
showSuggestPopover,
useSingleDiffStyle,
} = options;
commit(types.SET_BASE_CONFIG, {
endpoint,
......@@ -54,11 +55,15 @@ export const setBaseConfig = ({ commit }, options) => {
projectPath,
dismissEndpoint,
showSuggestPopover,
useSingleDiffStyle,
});
};
export const fetchDiffFiles = ({ state, commit }) => {
const worker = new TreeWorker();
const urlParams = {
w: state.showWhitespace ? '0' : '1',
};
commit(types.SET_LOADING, true);
......@@ -69,9 +74,10 @@ export const fetchDiffFiles = ({ state, commit }) => {
});
return axios
.get(mergeUrlParams({ w: state.showWhitespace ? '0' : '1' }, state.endpoint))
.get(mergeUrlParams(urlParams, state.endpoint))
.then(res => {
commit(types.SET_LOADING, false);
commit(types.SET_MERGE_REQUEST_DIFFS, res.data.merge_request_diffs || []);
commit(types.SET_DIFF_DATA, res.data);
......
......@@ -31,4 +31,5 @@ export default () => ({
fileFinderVisible: false,
dismissEndpoint: '',
showSuggestPopover: true,
useSingleDiffStyle: false,
});
......@@ -19,6 +19,7 @@ export default {
projectPath,
dismissEndpoint,
showSuggestPopover,
useSingleDiffStyle,
} = options;
Object.assign(state, {
endpoint,
......@@ -27,6 +28,7 @@ export default {
projectPath,
dismissEndpoint,
showSuggestPopover,
useSingleDiffStyle,
});
},
......
......@@ -56,7 +56,7 @@ export default {
__('Reported %{timeAgo} by %{reportedBy}'),
{
reportedBy: `<strong>${this.error.culprit}</strong>`,
timeAgo: this.timeFormated(this.stacktraceData.date_received),
timeAgo: this.timeFormatted(this.stacktraceData.date_received),
},
false,
);
......@@ -107,7 +107,7 @@ export default {
this.$refs.sentryIssueForm.submit();
},
formatDate(date) {
return `${this.timeFormated(date)} (${dateFormat(date, 'UTC:yyyy-mm-dd h:MM:ssTT Z')})`;
return `${this.timeFormatted(date)} (${dateFormat(date, 'UTC:yyyy-mm-dd h:MM:ssTT Z')})`;
},
},
};
......
......@@ -22,7 +22,7 @@ export default {
mixins: [timeAgoMixin],
data() {
return {
lastCommitFormatedAge: null,
lastCommitFormattedAge: null,
};
},
computed: {
......@@ -62,7 +62,7 @@ export default {
},
commitAgeUpdate() {
if (this.lastCommit) {
this.lastCommitFormatedAge = this.timeFormated(this.lastCommit.committed_date);
this.lastCommitFormattedAge = this.timeFormatted(this.lastCommit.committed_date);
}
},
getCommitPath(shortSha) {
......@@ -118,7 +118,7 @@ export default {
:title="tooltipTitle(lastCommit.committed_date)"
data-placement="top"
data-container="body"
>{{ lastCommitFormatedAge }}</time
>{{ lastCommitFormattedAge }}</time
>
</div>
<ide-status-list class="ml-auto" />
......
......@@ -91,7 +91,7 @@ export default {
/>
<gl-tooltip :target="() => $refs.state" placement="bottom">
<span class="d-block">
<span class="bold"> {{ stateTitle }} </span> {{ timeFormated(closedOrCreatedDate) }}
<span class="bold"> {{ stateTitle }} </span> {{ timeFormatted(closedOrCreatedDate) }}
</span>
<span class="text-tertiary">{{ tooltipTitle(closedOrCreatedDate) }}</span>
</gl-tooltip>
......
......@@ -168,13 +168,13 @@ export default {
/>
<detail-row
v-if="job.finished_at"
:value="timeFormated(job.finished_at)"
:value="timeFormatted(job.finished_at)"
class="js-job-finished"
title="Finished"
/>
<detail-row
v-if="job.erased_at"
:value="timeFormated(job.erased_at)"
:value="timeFormatted(job.erased_at)"
class="js-job-erased"
title="Erased"
/>
......
......@@ -114,7 +114,7 @@ export const logLinesParser = (lines = [], accumulator = []) =>
acc.push(parseHeaderLine(line, lineNumber));
} else if (isCollapsibleSection(acc, last, line)) {
// if the object belongs to a nested section, we append it to the new `lines` array of the
// previously formated header
// previously formatted header
last.lines.push(parseLine(line, lineNumber));
} else if (line.section_duration) {
// if the line has section_duration, we look for the correct header to add it
......
......@@ -5,7 +5,7 @@
import $ from 'jquery';
import axios from './axios_utils';
import { getLocationHash } from './url_utility';
import { convertToCamelCase } from './text_utility';
import { convertToCamelCase, convertToSnakeCase } from './text_utility';
import { isObject } from './type_utility';
import breakpointInstance from '../../breakpoints';
......@@ -697,6 +697,22 @@ export const convertObjectPropsToCamelCase = (obj = {}, options = {}) => {
}, initial);
};
/**
* Converts all the object keys to snake case
*
* @param {Object} obj Object to transform
* @returns {Object}
*/
// Follow up to add additional options param:
// https://gitlab.com/gitlab-org/gitlab/issues/39173
export const convertObjectPropsToSnakeCase = (obj = {}) =>
obj
? Object.entries(obj).reduce(
(acc, [key, value]) => ({ ...acc, [convertToSnakeCase(key)]: value }),
{},
)
: {};
export const imagePath = imgUrl =>
`${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`;
......
......@@ -447,7 +447,7 @@ export const parsePikadayDate = dateString => {
/**
* Used `onSelect` method in pickaday
* @param {Date} date UTC format
* @return {String} Date formated in yyyy-mm-dd
* @return {String} Date formatted in yyyy-mm-dd
*/
export const pikadayToString = date => {
const day = pad(date.getDate());
......@@ -513,8 +513,8 @@ export const stringifyTime = (timeObject, fullNameFormat = false) => {
if (fullNameFormat && isNonZero) {
// Remove traling 's' if unit value is singular
const formatedUnitName = unitValue > 1 ? unitName : unitName.replace(/s$/, '');
return `${memo} ${unitValue} ${formatedUnitName}`;
const formattedUnitName = unitValue > 1 ? unitName : unitName.replace(/s$/, '');
return `${memo} ${unitValue} ${formattedUnitName}`;
}
return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo;
......
......@@ -45,7 +45,7 @@ export default {
return this.mergeRequest.headPipeline && this.mergeRequest.headPipeline.detailedStatus;
},
formattedTime() {
return this.timeFormated(this.mergeRequest.createdAt);
return this.timeFormatted(this.mergeRequest.createdAt);
},
statusBoxClass() {
switch (this.mergeRequest.state) {
......
......@@ -31,7 +31,7 @@ export default {
hasFinishedTime() {
return this.finishedTime !== '';
},
durationFormated() {
durationFormatted() {
const date = new Date(this.duration * 1000);
let hh = date.getUTCHours();
......@@ -59,7 +59,7 @@ export default {
<div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Duration') }}</div>
<div class="table-mobile-content">
<p v-if="hasDuration" class="duration">
<span v-html="iconTimerSvg"> </span> {{ durationFormated }}
<span v-html="iconTimerSvg"> </span> {{ durationFormatted }}
</p>
<p v-if="hasFinishedTime" class="finished-at d-none d-sm-none d-md-block">
......@@ -71,7 +71,7 @@ export default {
data-placement="top"
data-container="body"
>
{{ timeFormated(finishedTime) }}
{{ timeFormatted(finishedTime) }}
</time>
</p>
</div>
......
......@@ -247,7 +247,7 @@ export default {
<td>
<span v-gl-tooltip.bottom :title="tooltipTitle(item.createdAt)">{{
timeFormated(item.createdAt)
timeFormatted(item.createdAt)
}}</span>
</td>
......
......@@ -48,7 +48,7 @@ export default {
},
releasedTimeAgo() {
return sprintf(__('released %{time}'), {
time: this.timeFormated(this.release.released_at),
time: this.timeFormatted(this.release.released_at),
});
},
userImageAltDescription() {
......
......@@ -50,7 +50,7 @@ export default {
},
computed: {
releasedAtTimeAgo() {
return this.timeFormated(this.releasedAt);
return this.timeFormatted(this.releasedAt);
},
userImageAltDescription() {
return this.author && this.author.username
......
......@@ -41,7 +41,7 @@ export default {
},
computed: {
deployTimeago() {
return this.timeFormated(this.deployment.deployed_at);
return this.timeFormatted(this.deployment.deployed_at);
},
deployedText() {
return this.$options.deployedTextMap[this.computedDeploymentStatus];
......
......@@ -54,7 +54,7 @@ export default {
return timeFor(
this.milestoneDue,
sprintf(__('Expired %{expiredOn}'), {
expiredOn: this.timeFormated(this.milestoneDue),
expiredOn: this.timeFormatted(this.milestoneDue),
}),
);
}
......@@ -62,7 +62,7 @@ export default {
return sprintf(
this.isMilestoneStarted ? __('Started %{startsIn}') : __('Starts %{startsIn}'),
{
startsIn: this.timeFormated(this.milestoneStart),
startsIn: this.timeFormatted(this.milestoneStart),
},
);
}
......
......@@ -64,7 +64,7 @@ export default {
tooltipText(dateType = 'min') {
const defaultText = dateType === 'min' ? __('Start date') : __('Due date');
const date = this[`${dateType}Date`];
const timeAgo = dateType === 'min' ? this.timeFormated(date) : timeFor(date);
const timeAgo = dateType === 'min' ? this.timeFormatted(date) : timeFor(date);
const dateText = date ? [this.dateText(dateType), `(${timeAgo})`].join(' ') : '';
if (date) {
......
......@@ -35,7 +35,7 @@ export default {
v-gl-tooltip.viewport="{ placement: tooltipPlacement }"
:class="cssClass"
:title="tooltipTitle(time)"
v-text="timeFormated(time)"
v-text="timeFormatted(time)"
>
</time>
</template>
......@@ -159,7 +159,7 @@ const mixins = {
return this.displayReference.split(this.pathIdSeparator).pop();
},
createdAtInWords() {
return this.createdAt ? this.timeFormated(this.createdAt) : '';
return this.createdAt ? this.timeFormatted(this.createdAt) : '';
},
createdAtTimestamp() {
return this.createdAt ? formatDate(new Date(this.createdAt)) : '';
......@@ -168,10 +168,10 @@ const mixins = {
return this.mergedAt ? formatDate(new Date(this.mergedAt)) : '';
},
mergedAtInWords() {
return this.mergedAt ? this.timeFormated(this.mergedAt) : '';
return this.mergedAt ? this.timeFormatted(this.mergedAt) : '';
},
closedAtInWords() {
return this.closedAt ? this.timeFormated(this.closedAt) : '';
return this.closedAt ? this.timeFormatted(this.closedAt) : '';
},
closedAtTimestamp() {
return this.closedAt ? formatDate(new Date(this.closedAt)) : '';
......
......@@ -5,7 +5,7 @@ import { formatDate, getTimeago } from '../../lib/utils/datetime_utility';
*/
export default {
methods: {
timeFormated(time) {
timeFormatted(time) {
const timeago = getTimeago();
return timeago.format(time);
......
......@@ -20,6 +20,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
before_action :check_user_can_push_to_source_branch!, only: [:rebase]
before_action only: [:show] do
push_frontend_feature_flag(:diffs_batch_load, @project)
push_frontend_feature_flag(:single_mr_diff_view, @project)
end
before_action do
......
---
title: Show merge immediately dialog even if the MR's pipeline hasn't finished
merge_request: 21556
author:
type: changed
......@@ -219,6 +219,94 @@ build_latest_vulnerabilities:
The above template will work for a GitLab Docker registry running on a local installation, however, if you're using a non-GitLab Docker registry, you'll need to change the `$CI_REGISTRY` value and the `docker login` credentials to match the details of your local registry.
## Reports JSON format
CAUTION: **Caution:**
The JSON report artifacts are not a public API of Container Scanning and their format may change in the future.
The Container Scanning tool emits a JSON report file. Here is an example of the report structure with all important parts of
it highlighted:
```json-doc
{
"version": "2.3",
"vulnerabilities": [
{
"category": "container_scanning",
"message": "CVE-2019-3462 in apt",
"description": "Incorrect sanitation of the 302 redirect field in HTTP transport method of apt versions 1.4.8 and earlier can lead to content injection by a MITM attacker, potentially leading to remote code execution on the target machine.",
"cve": "debian:9:apt:CVE-2019-3462",
"severity": "High",
"confidence": "Unknown",
"solution": "Upgrade apt from 1.4.8 to 1.4.9",
"scanner": {
"id": "klar",
"name": "klar"
},
"location": {
"dependency": {
"package": {
"name": "apt"
},
"version": "1.4.8"
},
"operating_system": "debian:9",
"image": "registry.gitlab.com/gitlab-org/security-products/dast/webgoat-8.0@sha256:bc09fe2e0721dfaeee79364115aeedf2174cce0947b9ae5fe7c33312ee019a4e"
},
"identifiers": [
{
"type": "cve",
"name": "CVE-2019-3462",
"value": "CVE-2019-3462",
"url": "https://security-tracker.debian.org/tracker/CVE-2019-3462"
}
],
"links": [
{
"url": "https://security-tracker.debian.org/tracker/CVE-2019-3462"
}
]
}
],
"remediations": [
]
}
```
Here is the description of the report file structure nodes and their meaning. All fields are mandatory to be present in
the report JSON unless stated otherwise. Presence of optional fields depends on the underlying analyzers being used.
| Report JSON node | Description |
|------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `version` | Report syntax version used to generate this JSON. |
| `vulnerabilities` | Array of vulnerability objects. |
| `vulnerabilities[].category` | Where this vulnerability belongs (SAST, Container Scanning etc.). For Container Scanning, it will always be `container_scanning`. |
| `vulnerabilities[].message` | A short text that describes the vulnerability, it may include occurrence's specific information. Optional. |
| `vulnerabilities[].description` | A long text that describes the vulnerability. Optional. |
| `vulnerabilities[].cve` | A fingerprint string value that represents a concrete occurrence of the vulnerability. It's used to determine whether two vulnerability occurrences are same or different. May not be 100% accurate. **This is NOT a [CVE](https://cve.mitre.org/)**. |
| `vulnerabilities[].severity` | How much the vulnerability impacts the software. Possible values: `Undefined` (an analyzer has not provided this info), `Info`, `Unknown`, `Low`, `Medium`, `High`, `Critical`. **Note:** Our current container scanning tool based on [klar](https://github.com/optiopay/klar) only provides the following levels: `Unknown`, `Low`, `Medium`, `High`, `Critical`. |
| `vulnerabilities[].confidence` | How reliable the vulnerability's assessment is. Possible values: `Undefined` (an analyzer has not provided this info), `Ignore`, `Unknown`, `Experimental`, `Low`, `Medium`, `High`, `Confirmed`. **Note:** Our current container scanning tool based on [klar](https://github.com/optiopay/klar) does not provide a confidence level, so this value is currently hardcoded to `Unknown`. |
| `vulnerabilities[].solution` | Explanation of how to fix the vulnerability. Optional. |
| `vulnerabilities[].scanner` | A node that describes the analyzer used to find this vulnerability. |
| `vulnerabilities[].scanner.id` | Id of the scanner as a snake_case string. |
| `vulnerabilities[].scanner.name` | Name of the scanner, for display purposes. |
| `vulnerabilities[].location` | A node that tells where the vulnerability is located. |
| `vulnerabilities[].location.dependency` | A node that describes the dependency of a project where the vulnerability is located. |
| `vulnerabilities[].location.dependency.package` | A node that provides the information on the package where the vulnerability is located. |
| `vulnerabilities[].location.dependency.package.name` | Name of the package where the vulnerability is located. |
| `vulnerabilities[].location.dependency.version` | Version of the vulnerable package. Optional. |
| `vulnerabilities[].location.operating_system` | The operating system that contains the vulnerable package. |
| `vulnerabilities[].location.image` | The Docker image that was analyzed. Optional. |
| `vulnerabilities[].identifiers` | An ordered array of references that identify a vulnerability on internal or external DBs. |
| `vulnerabilities[].identifiers[].type` | Type of the identifier. Possible values: common identifier types (among `cve`, `cwe`, `osvdb`, and `usn`). |
| `vulnerabilities[].identifiers[].name` | Name of the identifier for display purpose. |
| `vulnerabilities[].identifiers[].value` | Value of the identifier for matching purpose. |
| `vulnerabilities[].identifiers[].url` | URL to identifier's documentation. Optional. |
| `vulnerabilities[].links` | An array of references to external documentation pieces or articles that describe the vulnerability further. Optional. |
| `vulnerabilities[].links[].name` | Name of the vulnerability details link. Optional. |
| `vulnerabilities[].links[].url` | URL of the vulnerability details document. Optional. |
| `remediations` | Not supported yet. |
## Troubleshooting
### docker: Error response from daemon: failed to copy xattrs
......
......@@ -5202,6 +5202,9 @@ msgstr ""
msgid "CustomCycleAnalytics|Add stage"
msgstr ""
msgid "CustomCycleAnalytics|Editing stage"
msgstr ""
msgid "CustomCycleAnalytics|Enter a name for the stage"
msgstr ""
......@@ -5235,6 +5238,9 @@ msgstr ""
msgid "CustomCycleAnalytics|Stop event label"
msgstr ""
msgid "CustomCycleAnalytics|Update stage"
msgstr ""
msgid "Customize colors"
msgstr ""
......
......@@ -45,9 +45,9 @@ describe('Environment item', () => {
});
it('should render last deployment date', () => {
const formatedDate = format(environment.last_deployment.deployed_at);
const formattedDate = format(environment.last_deployment.deployed_at);
expect(wrapper.find('.environment-created-date-timeago').text()).toContain(formatedDate);
expect(wrapper.find('.environment-created-date-timeago').text()).toContain(formattedDate);
});
describe('With user information', () => {
......
......@@ -8,7 +8,7 @@ describe('Erased block', () => {
const erasedAt = '2016-11-07T11:11:16.525Z';
const timeago = getTimeago();
const formatedDate = timeago.format(erasedAt);
const formattedDate = timeago.format(erasedAt);
const createComponent = props => {
wrapper = mount(ErasedBlock, {
......@@ -41,7 +41,7 @@ describe('Erased block', () => {
});
it('renders erasedAt', () => {
expect(wrapper.text().trim()).toContain(formatedDate);
expect(wrapper.text().trim()).toContain(formattedDate);
});
});
......@@ -57,7 +57,7 @@ describe('Erased block', () => {
});
it('renders erasedAt', () => {
expect(wrapper.text().trim()).toContain(formatedDate);
expect(wrapper.text().trim()).toContain(formattedDate);
});
});
});
......@@ -68,7 +68,7 @@ describe('table registry', () => {
expect(tds.at(2).html()).toContain(repoPropsData.list[0].shortRevision);
expect(tds.at(3).html()).toContain(repoPropsData.list[0].layers);
expect(tds.at(3).html()).toContain(repoPropsData.list[0].size);
expect(tds.at(4).html()).toContain(wrapper.vm.timeFormated(repoPropsData.list[0].createdAt));
expect(tds.at(4).html()).toContain(wrapper.vm.timeFormatted(repoPropsData.list[0].createdAt));
});
it('should have a label called Image ID', () => {
......
......@@ -8,8 +8,8 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
jest.mock('~/vue_shared/mixins/timeago', () => ({
methods: {
timeFormated() {
return '7 fortnightes ago';
timeFormatted() {
return '7 fortnights ago';
},
tooltipTitle() {
return 'February 30, 2401';
......@@ -82,7 +82,7 @@ describe('Release block footer', () => {
it('renders the author and creation time info', () => {
expect(trimText(authorDateInfoSection().text())).toBe(
`Created 7 fortnightes ago by ${releaseClone.author.username}`,
`Created 7 fortnights ago by ${releaseClone.author.username}`,
);
});
......@@ -139,7 +139,7 @@ describe('Release block footer', () => {
beforeEach(() => factory({ author: undefined }));
it('renders the release date without the author name', () => {
expect(trimText(authorDateInfoSection().text())).toBe('Created 7 fortnightes ago');
expect(trimText(authorDateInfoSection().text())).toBe('Created 7 fortnights ago');
});
});
......
......@@ -68,7 +68,7 @@ describe('Release block', () => {
});
it('renders release date', () => {
expect(wrapper.text()).toContain(timeagoMixin.methods.timeFormated(release.released_at));
expect(wrapper.text()).toContain(timeagoMixin.methods.timeFormatted(release.released_at));
});
it('renders number of assets provided', () => {
......
......@@ -90,11 +90,11 @@ describe('RelatedIssuableItem', () => {
it('renders state title', () => {
const stateTitle = tokenState.attributes('title');
const formatedCreateDate = formatDate(props.createdAt);
const formattedCreateDate = formatDate(props.createdAt);
expect(stateTitle).toContain('<span class="bold">Opened</span>');
expect(stateTitle).toContain(`<span class="text-tertiary">${formatedCreateDate}</span>`);
expect(stateTitle).toContain(`<span class="text-tertiary">${formattedCreateDate}</span>`);
});
it('renders aria label', () => {
......
......@@ -41,6 +41,7 @@ describe('diffs/components/app', () => {
changesEmptyStateIllustration: '',
dismissEndpoint: '',
showSuggestPopover: true,
useSingleDiffStyle: false,
...props,
},
store,
......
......@@ -75,6 +75,7 @@ describe('DiffsStoreActions', () => {
const projectPath = '/root/project';
const dismissEndpoint = '/-/user_callouts';
const showSuggestPopover = false;
const useSingleDiffStyle = false;
testAction(
setBaseConfig,
......@@ -85,6 +86,7 @@ describe('DiffsStoreActions', () => {
projectPath,
dismissEndpoint,
showSuggestPopover,
useSingleDiffStyle,
},
{
endpoint: '',
......@@ -93,6 +95,7 @@ describe('DiffsStoreActions', () => {
projectPath: '',
dismissEndpoint: '',
showSuggestPopover: true,
useSingleDiffStyle: true,
},
[
{
......@@ -104,6 +107,7 @@ describe('DiffsStoreActions', () => {
projectPath,
dismissEndpoint,
showSuggestPopover,
useSingleDiffStyle,
},
},
],
......
......@@ -10,11 +10,13 @@ describe('DiffsStoreMutations', () => {
const state = {};
const endpoint = '/diffs/endpoint';
const projectPath = '/root/project';
const useSingleDiffStyle = false;
mutations[types.SET_BASE_CONFIG](state, { endpoint, projectPath });
mutations[types.SET_BASE_CONFIG](state, { endpoint, projectPath, useSingleDiffStyle });
expect(state.endpoint).toEqual(endpoint);
expect(state.projectPath).toEqual(projectPath);
expect(state.useSingleDiffStyle).toEqual(useSingleDiffStyle);
});
});
......
......@@ -721,6 +721,28 @@ describe('common_utils', () => {
});
});
describe('convertObjectPropsToSnakeCase', () => {
it('converts each object key to snake case', () => {
const obj = {
some: 'some',
'cool object': 'cool object',
likeThisLongOne: 'likeThisLongOne',
};
expect(commonUtils.convertObjectPropsToSnakeCase(obj)).toEqual({
some: 'some',
cool_object: 'cool object',
like_this_long_one: 'likeThisLongOne',
});
});
it('returns an empty object if there are no keys', () => {
['', {}, [], null].forEach(badObj => {
expect(commonUtils.convertObjectPropsToSnakeCase(badObj)).toEqual({});
});
});
});
describe('with options', () => {
const objWithoutChildren = {
project_name: 'GitLab CE',
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册