Commit 9de8a305 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 6b36a033 019fecec
......@@ -114,6 +114,12 @@ class SafeMathRenderer {
throwOnError: true,
maxSize: 20,
maxExpand: 20,
trust: (context) =>
// this config option restores the KaTeX pre-v0.11.0
// behavior of allowing certain commands and protocols
// eslint-disable-next-line @gitlab/require-i18n-strings
['\\url', '\\href'].includes(context.command) &&
['http', 'https', 'mailto', '_relative'].includes(context.protocol),
});
} catch (e) {
// Don't show a flash for now because it would override an existing flash message
......
<script>
export default {
props: {
job: {
type: Object,
required: true,
},
},
};
</script>
<template>
<div></div>
</template>
<script>
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import timeagoMixin from '~/vue_shared/mixins/timeago';
export default {
iconSize: 12,
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
GlIcon,
},
mixins: [timeagoMixin],
props: {
job: {
type: Object,
required: true,
},
},
computed: {
finishedTime() {
return this.job?.finishedAt;
},
duration() {
return this.job?.duration;
},
},
};
</script>
<template>
<div>
<div v-if="duration" data-testid="job-duration">
<gl-icon name="timer" :size="$options.iconSize" data-testid="duration-icon" />
{{ durationTimeFormatted(duration) }}
</div>
<div v-if="finishedTime" data-testid="job-finished-time">
<gl-icon name="calendar" :size="$options.iconSize" data-testid="finished-time-icon" />
<time
v-gl-tooltip
:title="tooltipTitle(finishedTime)"
data-placement="top"
data-container="body"
>
{{ timeFormatted(finishedTime) }}
</time>
</div>
</div>
</template>
<script>
import { GlBadge, GlIcon, GlLink } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { SUCCESS_STATUS } from '../../../constants';
export default {
iconSize: 12,
badgeSize: 'sm',
components: {
GlBadge,
GlIcon,
GlLink,
},
props: {
job: {
type: Object,
required: true,
},
},
computed: {
jobId() {
const id = getIdFromGraphQLId(this.job.id);
return `#${id}`;
},
jobPath() {
return this.job.detailedStatus?.detailsPath;
},
jobRef() {
return this.job?.refName;
},
jobRefPath() {
return this.job?.refPath;
},
jobTags() {
return this.job.tags;
},
createdByTag() {
return this.job.createdByTag;
},
triggered() {
return this.job.triggered;
},
isManualJob() {
return this.job.manualJob;
},
successfulJob() {
return this.job.status === SUCCESS_STATUS;
},
showAllowedToFailBadge() {
return this.job.allowFailure && !this.successfulJob;
},
isScheduledJob() {
return Boolean(this.job.scheduledAt);
},
},
};
</script>
<template>
<div>
<div class="gl-text-truncate">
<gl-link class="gl-text-gray-500!" :href="jobPath" data-testid="job-id">{{ jobId }}</gl-link>
<div class="gl-display-flex gl-align-items-center">
<div v-if="jobRef" class="gl-max-w-15 gl-text-truncate">
<gl-icon
v-if="createdByTag"
name="label"
:size="$options.iconSize"
data-testid="label-icon"
/>
<gl-icon v-else name="fork" :size="$options.iconSize" data-testid="fork-icon" />
<gl-link
class="gl-font-weight-bold gl-text-gray-500!"
:href="job.refPath"
data-testid="job-ref"
>{{ job.refName }}</gl-link
>
</div>
<span v-else>{{ __('none') }}</span>
<gl-icon class="gl-mx-2" name="commit" :size="$options.iconSize" />
<gl-link :href="job.commitPath" data-testid="job-sha">{{ job.shortSha }}</gl-link>
</div>
</div>
<div>
<gl-badge
v-for="tag in jobTags"
:key="tag"
variant="info"
:size="$options.badgeSize"
data-testid="job-tag-badge"
>
{{ tag }}
</gl-badge>
<gl-badge
v-if="triggered"
variant="info"
:size="$options.badgeSize"
data-testid="triggered-job-badge"
>{{ s__('Job|triggered') }}
</gl-badge>
<gl-badge
v-if="showAllowedToFailBadge"
variant="warning"
:size="$options.badgeSize"
data-testid="fail-job-badge"
>{{ s__('Job|allowed to fail') }}
</gl-badge>
<gl-badge
v-if="isScheduledJob"
variant="info"
:size="$options.badgeSize"
data-testid="delayed-job-badge"
>{{ s__('Job|delayed') }}
</gl-badge>
<gl-badge
v-if="isManualJob"
variant="info"
:size="$options.badgeSize"
data-testid="manual-job-badge"
>
{{ s__('Job|manual') }}
</gl-badge>
</div>
</div>
</template>
<script>
import { GlAvatar, GlLink } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
export default {
components: {
GlAvatar,
GlLink,
},
props: {
job: {
type: Object,
required: true,
},
},
computed: {
pipelineId() {
const id = getIdFromGraphQLId(this.job.pipeline.id);
return `#${id}`;
},
pipelinePath() {
return this.job.pipeline?.path;
},
pipelineUserAvatar() {
return this.job.pipeline?.user?.avatarUrl;
},
userPath() {
return this.job.pipeline?.user?.webPath;
},
showAvatar() {
return this.job.pipeline?.user;
},
},
};
</script>
<template>
<div class="gl-text-truncate">
<gl-link class="gl-text-gray-500!" :href="pipelinePath" data-testid="pipeline-id">
{{ pipelineId }}
</gl-link>
<div>
<span>{{ __('created by') }}</span>
<gl-link v-if="showAvatar" :href="userPath" data-testid="pipeline-user-link">
<gl-avatar :src="pipelineUserAvatar" :size="16" />
</gl-link>
<span v-else>{{ __('API') }}</span>
</div>
</div>
</template>
......@@ -8,7 +8,20 @@ query getJobs($fullPath: ID!, $statuses: [CiJobStatus!]) {
startCursor
}
nodes {
artifacts {
nodes {
downloadPath
}
}
allowFailure
status
scheduledAt
manualJob
triggered
createdByTag
detailedStatus {
detailsPath
group
icon
label
text
......
<script>
import { GlTable } from '@gitlab/ui';
import { __ } from '~/locale';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
import ActionsCell from './cells/actions_cell.vue';
import DurationCell from './cells/duration_cell.vue';
import JobCell from './cells/job_cell.vue';
import PipelineCell from './cells/pipeline_cell.vue';
const defaultTableClasses = {
tdClass: 'gl-p-5!',
......@@ -13,45 +18,58 @@ export default {
key: 'status',
label: __('Status'),
...defaultTableClasses,
columnClass: 'gl-w-10p',
},
{
key: 'job',
label: __('Job'),
...defaultTableClasses,
columnClass: 'gl-w-20p',
},
{
key: 'pipeline',
label: __('Pipeline'),
...defaultTableClasses,
columnClass: 'gl-w-10p',
},
{
key: 'stage',
label: __('Stage'),
...defaultTableClasses,
columnClass: 'gl-w-10p',
},
{
key: 'name',
label: __('Name'),
...defaultTableClasses,
columnClass: 'gl-w-15p',
},
{
key: 'duration',
label: __('Duration'),
...defaultTableClasses,
columnClass: 'gl-w-15p',
},
{
key: 'coverage',
label: __('Coverage'),
...defaultTableClasses,
columnClass: 'gl-w-10p',
},
{
key: 'actions',
label: '',
...defaultTableClasses,
columnClass: 'gl-w-10p',
},
],
components: {
ActionsCell,
CiBadge,
DurationCell,
GlTable,
JobCell,
PipelineCell,
},
props: {
jobs: {
......@@ -59,9 +77,62 @@ export default {
required: true,
},
},
methods: {
formatCoverage(coverage) {
return coverage ? `${coverage}%` : '';
},
},
};
</script>
<template>
<gl-table :items="jobs" :fields="$options.fields" />
<gl-table
:items="jobs"
:fields="$options.fields"
:tbody-tr-attr="{ 'data-testid': 'jobs-table-row' }"
stacked="lg"
fixed
>
<template #table-colgroup="{ fields }">
<col v-for="field in fields" :key="field.key" :class="field.columnClass" />
</template>
<template #cell(status)="{ item }">
<ci-badge :status="item.detailedStatus" />
</template>
<template #cell(job)="{ item }">
<job-cell :job="item" />
</template>
<template #cell(pipeline)="{ item }">
<pipeline-cell :job="item" />
</template>
<template #cell(stage)="{ item }">
<div class="gl-text-truncate">
<span data-testid="job-stage-name">{{ item.stage.name }}</span>
</div>
</template>
<template #cell(name)="{ item }">
<div class="gl-text-truncate">
<span data-testid="job-name">{{ item.name }}</span>
</div>
</template>
<template #cell(duration)="{ item }">
<duration-cell :job="item" />
</template>
<template #cell(coverage)="{ item }">
<span v-if="item.coverage" data-testid="job-coverage">{{
formatCoverage(item.coverage)
}}</span>
</template>
<template #cell(actions)="{ item }">
<actions-cell :job="item" />
</template>
</gl-table>
</template>
......@@ -50,7 +50,7 @@ export default {
</script>
<template>
<gl-tabs>
<gl-tabs content-class="gl-pb-0">
<gl-tab
v-for="tab in tabs"
:key="tab.text"
......
......@@ -22,3 +22,5 @@ export const JOB_RETRY_FORWARD_DEPLOYMENT_MODAL = {
primaryText: __('Retry job'),
title: s__('Jobs|Are you sure you want to retry this job?'),
};
export const SUCCESS_STATUS = 'SUCCESS';
......@@ -14,5 +14,25 @@ export default {
tooltipTitle(time) {
return formatDate(time);
},
durationTimeFormatted(duration) {
const date = new Date(duration * 1000);
let hh = date.getUTCHours();
let mm = date.getUTCMinutes();
let ss = date.getSeconds();
if (hh < 10) {
hh = `0${hh}`;
}
if (mm < 10) {
mm = `0${mm}`;
}
if (ss < 10) {
ss = `0${ss}`;
}
return `${hh}:${mm}:${ss}`;
},
},
};
......@@ -9,7 +9,7 @@
= button_tag class: 'toggle-mobile-nav', type: 'button' do
%span.sr-only= _("Open sidebar")
= sprite_icon('hamburger', size: 18)
.breadcrumbs-links.overflow-auto{ data: { testid: 'breadcrumb-links', qa_selector: 'breadcrumb_links_content' } }
.breadcrumbs-links{ data: { testid: 'breadcrumb-links', qa_selector: 'breadcrumb_links_content' } }
%ul.list-unstyled.breadcrumbs-list.js-breadcrumbs-list
- unless hide_top_links
= header_title
......
---
title: Fix overflow in breadcrumbs list mainly on mobile
merge_request: 59552
author: Takuya Noguchi
type: other
---
title: Drop Jira proxy setting columns
merge_request: 60123
author:
type: other
---
title: Update KaTeX integration to v0.13.0
merge_request: 60071
author:
type: other
# frozen_string_literal: true
class RemoveProxySettingsToJiraTrackerData < ActiveRecord::Migration[6.0]
def change
remove_column :jira_tracker_data, :encrypted_proxy_address, :text
remove_column :jira_tracker_data, :encrypted_proxy_address_iv, :text
remove_column :jira_tracker_data, :encrypted_proxy_port, :text
remove_column :jira_tracker_data, :encrypted_proxy_port_iv, :text
remove_column :jira_tracker_data, :encrypted_proxy_username, :text
remove_column :jira_tracker_data, :encrypted_proxy_username_iv, :text
remove_column :jira_tracker_data, :encrypted_proxy_password, :text
remove_column :jira_tracker_data, :encrypted_proxy_password_iv, :text
end
end
6b508f1a48402aa2db3862e2e31ee4ccb851f535ed59f9b949ac1bad0ff2f0e1
\ No newline at end of file
......@@ -14016,14 +14016,6 @@ CREATE TABLE jira_tracker_data (
deployment_type smallint DEFAULT 0 NOT NULL,
vulnerabilities_issuetype text,
vulnerabilities_enabled boolean DEFAULT false NOT NULL,
encrypted_proxy_address text,
encrypted_proxy_address_iv text,
encrypted_proxy_port text,
encrypted_proxy_port_iv text,
encrypted_proxy_username text,
encrypted_proxy_username_iv text,
encrypted_proxy_password text,
encrypted_proxy_password_iv text,
jira_issue_transition_automatic boolean DEFAULT false NOT NULL,
CONSTRAINT check_0bf84b76e9 CHECK ((char_length(vulnerabilities_issuetype) <= 255)),
CONSTRAINT check_214cf6a48b CHECK ((char_length(project_key) <= 255))
......@@ -658,6 +658,7 @@ DAST can be [configured](#customizing-the-dast-settings) using CI/CD variables.
| `DAST_AUTO_UPDATE_ADDONS` | boolean | ZAP add-ons are pinned to specific versions in the DAST Docker image. Set to `true` to download the latest versions when the scan starts. Default: `false` |
| `DAST_API_HOST_OVERRIDE` | string | Used to override domains defined in API specification files. Only supported when importing the API specification from a URL. Example: `example.com:8080` |
| `DAST_EXCLUDE_RULES` | string | Set to a comma-separated list of Vulnerability Rule IDs to exclude them from running during the scan. Rule IDs are numbers and can be found from the DAST log or on the [ZAP project](https://github.com/zaproxy/zaproxy/blob/develop/docs/scanners.md). For example, `HTTP Parameter Override` has a rule ID of `10026`. **Note:** In earlier versions of GitLab the excluded rules were executed but alerts they generated were suppressed. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/118641) in GitLab 12.10. |
| `DAST_ONLY_INCLUDE_RULES` | string | Set to a comma-separated list of Vulnerability Rule IDs to configure the scan to run only them. Rule IDs are numbers and can be found from the DAST log or on the [ZAP project](https://github.com/zaproxy/zaproxy/blob/develop/docs/scanners.md). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/250651) in GitLab 13.12. |
| `DAST_REQUEST_HEADERS` | string | Set to a comma-separated list of request header names and values. Headers are added to every request made by DAST. For example, `Cache-control: no-cache,User-Agent: DAST/1.0` |
| `DAST_DEBUG` | boolean | Enable debug message output. Default: `false`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12652) in GitLab 13.1. |
| `DAST_SPIDER_MINS` | number | The maximum duration of the spider scan in minutes. Set to `0` for unlimited. Default: One minute, or unlimited when the scan is a full scan. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12652) in GitLab 13.1. |
......
......@@ -3,6 +3,7 @@ stage: none
group: Development
info: "See the Technical Writers assigned to Development Guidelines: https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments-to-development-guidelines"
description: "Understand what 'GitLab features deployed behind flags' means."
layout: 'feature_flags'
---
# GitLab functionality may be limited by feature flags
......
......@@ -1487,6 +1487,9 @@ msgstr ""
msgid "ACTION REQUIRED: Something went wrong while obtaining the Let's Encrypt certificate for GitLab Pages domain '%{domain}'"
msgstr ""
msgid "API"
msgstr ""
msgid "API Fuzzing"
msgstr ""
......@@ -18543,12 +18546,24 @@ msgstr ""
msgid "Job|This job is stuck because you don't have any active runners that can run this job."
msgstr ""
msgid "Job|allowed to fail"
msgstr ""
msgid "Job|delayed"
msgstr ""
msgid "Job|for"
msgstr ""
msgid "Job|into"
msgstr ""
msgid "Job|manual"
msgstr ""
msgid "Job|triggered"
msgstr ""
msgid "Job|with"
msgstr ""
......@@ -37584,6 +37599,9 @@ msgstr ""
msgid "created %{timeAgo}"
msgstr ""
msgid "created by"
msgstr ""
msgid "data"
msgstr ""
......
......@@ -109,7 +109,7 @@
"js-yaml": "^3.13.1",
"jszip": "^3.1.3",
"jszip-utils": "^0.0.2",
"katex": "^0.10.0",
"katex": "^0.13.2",
"lodash": "^4.17.20",
"marked": "^0.3.12",
"mathjax": "3",
......
......@@ -13,14 +13,24 @@ RSpec.describe 'Math rendering', :js do
```math
a^2+b^2=c^2
```
This math is aligned
```math
\\begin{align*}
a&=b+c \\\\
d+e&=f
\\end{align*}
```
MATH
issue = create(:issue, project: project, description: description)
visit project_issue_path(project, issue)
expect(page).to have_selector('.katex .mord.mathdefault', text: 'b')
expect(page).to have_selector('.katex-display .mord.mathdefault', text: 'b')
expect(page).to have_selector('.katex .mord.mathnormal', text: 'b')
expect(page).to have_selector('.katex-display .mord.mathnormal', text: 'b')
expect(page).to have_selector('.katex-display .mtable .col-align-l .mord.mathnormal', text: 'f')
end
it 'only renders non XSS links' do
......@@ -35,7 +45,9 @@ RSpec.describe 'Math rendering', :js do
visit project_issue_path(project, issue)
page.within '.description > .md' do
expect(page).to have_selector('.katex-error')
# unfortunately there is no class selector for KaTeX's "unsupported command"
# formatting so we must match the style attribute
expect(page).to have_selector('.katex-html .mord[style*="color:"][style*="#cc0000"]', text: '\href')
expect(page).to have_selector('.katex-html a', text: 'Gitlab')
end
end
......
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import DurationCell from '~/jobs/components/table/cells/duration_cell.vue';
describe('Duration Cell', () => {
let wrapper;
const findJobDuration = () => wrapper.findByTestId('job-duration');
const findJobFinishedTime = () => wrapper.findByTestId('job-finished-time');
const findDurationIcon = () => wrapper.findByTestId('duration-icon');
const findFinishedTimeIcon = () => wrapper.findByTestId('finished-time-icon');
const createComponent = (props) => {
wrapper = extendedWrapper(
shallowMount(DurationCell, {
propsData: {
job: {
...props,
},
},
}),
);
};
afterEach(() => {
wrapper.destroy();
});
it('does not display duration or finished time when no properties are present', () => {
createComponent();
expect(findJobDuration().exists()).toBe(false);
expect(findJobFinishedTime().exists()).toBe(false);
});
it('displays duration and finished time when both properties are present', () => {
const props = {
duration: 7,
finishedAt: '2021-04-26T13:37:52Z',
};
createComponent(props);
expect(findJobDuration().exists()).toBe(true);
expect(findJobFinishedTime().exists()).toBe(true);
});
it('displays only the duration of the job when the duration property is present', () => {
const props = {
duration: 7,
};
createComponent(props);
expect(findJobDuration().exists()).toBe(true);
expect(findJobFinishedTime().exists()).toBe(false);
});
it('displays only the finished time of the job when the finshedAt property is present', () => {
const props = {
finishedAt: '2021-04-26T13:37:52Z',
};
createComponent(props);
expect(findJobFinishedTime().exists()).toBe(true);
expect(findJobDuration().exists()).toBe(false);
});
it('displays icons for finished time and duration', () => {
const props = {
duration: 7,
finishedAt: '2021-04-26T13:37:52Z',
};
createComponent(props);
expect(findFinishedTimeIcon().props('name')).toBe('calendar');
expect(findDurationIcon().props('name')).toBe('timer');
});
});
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import JobCell from '~/jobs/components/table/cells/job_cell.vue';
import { mockJobsInTable } from '../../../mock_data';
const mockJob = mockJobsInTable[0];
const mockJobCreatedByTag = mockJobsInTable[1];
describe('Job Cell', () => {
let wrapper;
const findJobId = () => wrapper.findByTestId('job-id');
const findJobRef = () => wrapper.findByTestId('job-ref');
const findJobSha = () => wrapper.findByTestId('job-sha');
const findLabelIcon = () => wrapper.findByTestId('label-icon');
const findForkIcon = () => wrapper.findByTestId('fork-icon');
const findAllTagBadges = () => wrapper.findAllByTestId('job-tag-badge');
const findBadgeById = (id) => wrapper.findByTestId(id);
const createComponent = (jobData = mockJob) => {
wrapper = extendedWrapper(
shallowMount(JobCell, {
propsData: {
job: jobData,
},
}),
);
};
afterEach(() => {
wrapper.destroy();
});
describe('Job Id', () => {
beforeEach(() => {
createComponent();
});
it('displays the job id and links to the job', () => {
const expectedJobId = `#${getIdFromGraphQLId(mockJob.id)}`;
expect(findJobId().text()).toBe(expectedJobId);
expect(findJobId().attributes('href')).toBe(mockJob.detailedStatus.detailsPath);
});
});
describe('Ref of the job', () => {
it('displays the ref name and links to the ref', () => {
createComponent();
expect(findJobRef().text()).toBe(mockJob.refName);
expect(findJobRef().attributes('href')).toBe(mockJob.refPath);
});
it('displays fork icon when job is not created by tag', () => {
createComponent();
expect(findForkIcon().exists()).toBe(true);
expect(findLabelIcon().exists()).toBe(false);
});
it('displays label icon when job is created by a tag', () => {
createComponent(mockJobCreatedByTag);
expect(findLabelIcon().exists()).toBe(true);
expect(findForkIcon().exists()).toBe(false);
});
});
describe('Commit of the job', () => {
beforeEach(() => {
createComponent();
});
it('displays the sha and links to the commit', () => {
expect(findJobSha().text()).toBe(mockJob.shortSha);
expect(findJobSha().attributes('href')).toBe(mockJob.commitPath);
});
});
describe('Job badges', () => {
it('displays tags of the job', () => {
const mockJobWithTags = {
tags: ['tag-1', 'tag-2', 'tag-3'],
};
createComponent(mockJobWithTags);
expect(findAllTagBadges()).toHaveLength(mockJobWithTags.tags.length);
});
it.each`
testId | text
${'manual-job-badge'} | ${'manual'}
${'triggered-job-badge'} | ${'triggered'}
${'fail-job-badge'} | ${'allowed to fail'}
${'delayed-job-badge'} | ${'delayed'}
`('displays the static $text badge', ({ testId, text }) => {
createComponent({
manualJob: true,
triggered: true,
allowFailure: true,
scheduledAt: '2021-03-09T14:58:50+00:00',
});
expect(findBadgeById(testId).exists()).toBe(true);
expect(findBadgeById(testId).text()).toBe(text);
});
});
});
import { GlAvatar } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import PipelineCell from '~/jobs/components/table/cells/pipeline_cell.vue';
const mockJobWithoutUser = {
id: 'gid://gitlab/Ci::Build/2264',
pipeline: {
id: 'gid://gitlab/Ci::Pipeline/460',
path: '/root/ci-project/-/pipelines/460',
},
};
const mockJobWithUser = {
id: 'gid://gitlab/Ci::Build/2264',
pipeline: {
id: 'gid://gitlab/Ci::Pipeline/460',
path: '/root/ci-project/-/pipelines/460',
user: {
avatarUrl:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
webPath: '/root',
},
},
};
describe('Pipeline Cell', () => {
let wrapper;
const findPipelineId = () => wrapper.findByTestId('pipeline-id');
const findPipelineUserLink = () => wrapper.findByTestId('pipeline-user-link');
const findUserAvatar = () => wrapper.findComponent(GlAvatar);
const createComponent = (props = mockJobWithUser) => {
wrapper = extendedWrapper(
shallowMount(PipelineCell, {
propsData: {
job: props,
},
}),
);
};
afterEach(() => {
wrapper.destroy();
});
describe('Pipeline Id', () => {
beforeEach(() => {
createComponent();
});
it('displays the pipeline id and links to the pipeline', () => {
const expectedPipelineId = `#${getIdFromGraphQLId(mockJobWithUser.pipeline.id)}`;
expect(findPipelineId().text()).toBe(expectedPipelineId);
expect(findPipelineId().attributes('href')).toBe(mockJobWithUser.pipeline.path);
});
});
describe('Pipeline created by', () => {
const apiWrapperText = 'API';
it('shows and links to the pipeline user', () => {
createComponent();
expect(findPipelineUserLink().exists()).toBe(true);
expect(findPipelineUserLink().attributes('href')).toBe(mockJobWithUser.pipeline.user.webPath);
expect(findUserAvatar().attributes('src')).toBe(mockJobWithUser.pipeline.user.avatarUrl);
expect(wrapper.text()).not.toContain(apiWrapperText);
});
it('shows pipeline was created by the API', () => {
createComponent(mockJobWithoutUser);
expect(findPipelineUserLink().exists()).toBe(false);
expect(findUserAvatar().exists()).toBe(false);
expect(wrapper.text()).toContain(apiWrapperText);
});
});
});
import { GlTable } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import JobsTable from '~/jobs/components/table/jobs_table.vue';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
import { mockJobsInTable } from '../../mock_data';
describe('Jobs Table', () => {
let wrapper;
const findTable = () => wrapper.findComponent(GlTable);
const findStatusBadge = () => wrapper.findComponent(CiBadge);
const findTableRows = () => wrapper.findAllByTestId('jobs-table-row');
const findJobStage = () => wrapper.findByTestId('job-stage-name');
const findJobName = () => wrapper.findByTestId('job-name');
const findAllCoverageJobs = () => wrapper.findAllByTestId('job-coverage');
const createComponent = (props = {}) => {
wrapper = shallowMount(JobsTable, {
propsData: {
jobs: mockJobsInTable,
...props,
},
});
wrapper = extendedWrapper(
mount(JobsTable, {
propsData: {
jobs: mockJobsInTable,
...props,
},
}),
);
};
beforeEach(() => {
......@@ -25,7 +34,31 @@ describe('Jobs Table', () => {
wrapper.destroy();
});
it('displays a table', () => {
it('displays the jobs table', () => {
expect(findTable().exists()).toBe(true);
});
it('displays correct number of job rows', () => {
expect(findTableRows()).toHaveLength(mockJobsInTable.length);
});
it('displays job status', () => {
expect(findStatusBadge().exists()).toBe(true);
});
it('displays the job stage and name', () => {
const firstJob = mockJobsInTable[0];
expect(findJobStage().text()).toBe(firstJob.stage.name);
expect(findJobName().text()).toBe(firstJob.name);
});
it('displays the coverage for only jobs that have coverage', () => {
const jobsThatHaveCoverage = mockJobsInTable.filter((job) => job.coverage !== null);
jobsThatHaveCoverage.forEach((job, index) => {
expect(findAllCoverageJobs().at(index).text()).toBe(`${job.coverage}%`);
});
expect(findAllCoverageJobs()).toHaveLength(jobsThatHaveCoverage.length);
});
});
......@@ -1292,6 +1292,7 @@ export const mockJobsInTable = [
title: 'Play',
__typename: 'StatusAction',
},
detailsPath: '/root/ci-project/-/jobs/2004',
__typename: 'DetailedStatus',
},
id: 'gid://gitlab/Ci::Build/2004',
......@@ -1316,6 +1317,7 @@ export const mockJobsInTable = [
duration: null,
finishedAt: null,
coverage: null,
createdByTag: false,
retryable: false,
playable: true,
cancelable: false,
......@@ -1353,6 +1355,7 @@ export const mockJobsInTable = [
duration: null,
finishedAt: null,
coverage: null,
createdByTag: true,
retryable: false,
playable: false,
cancelable: false,
......@@ -1396,7 +1399,8 @@ export const mockJobsInTable = [
name: 'artifact_job',
duration: 2,
finishedAt: '2021-04-01T17:36:18Z',
coverage: null,
coverage: 82.71,
createdByTag: false,
retryable: true,
playable: false,
cancelable: false,
......
......@@ -3132,7 +3132,7 @@ commander@2, commander@^2.10.0, commander@^2.18.0, commander@^2.19.0, commander@
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==
commander@^6.2.0:
commander@^6.0.0, commander@^6.2.0:
version "6.2.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
......@@ -7459,12 +7459,12 @@ karma@^4.2.0:
tmp "0.0.33"
useragent "2.3.0"
katex@^0.10.0:
version "0.10.2"
resolved "https://registry.yarnpkg.com/katex/-/katex-0.10.2.tgz#39973edbb65eda5b6f9e7f41648781e557dd4932"
integrity sha512-cQOmyIRoMloCoSIOZ1+gEwsksdJZ1EW4SWm3QzxSza/QsnZr6D4U1V9S4q+B/OLm2OQ8TCBecQ8MaIfnScI7cw==
katex@^0.13.2:
version "0.13.2"
resolved "https://registry.yarnpkg.com/katex/-/katex-0.13.2.tgz#4075b9144e6af992ec9a4b772fa3754763be5f26"
integrity sha512-u/KhjFDhyPr+70aiBn9SL/9w/QlLagIXBi2NZSbNnBUp2tR8dCjQplyEMkEzniem5gOeSCBjlBUg4VaiWs1JJg==
dependencies:
commander "^2.19.0"
commander "^6.0.0"
keyv@^3.0.0:
version "3.1.0"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment