Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-08-16 15:09:17 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-08-16 15:09:17 +0300
commit09dff3eec735ccbe001d165293ecebf195452071 (patch)
tree03c73077d0703edb9452145e7109835da2cd4918 /spec
parent78e911431fc575ff4f6c9b7e0f95c02b57a5e926 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/features/cycle_analytics_spec.rb54
-rw-r--r--spec/features/issues/form_spec.rb36
-rw-r--r--spec/frontend/cycle_analytics/base_spec.js37
-rw-r--r--spec/frontend/cycle_analytics/mock_data.js3
-rw-r--r--spec/frontend/cycle_analytics/store/mutations_spec.js25
-rw-r--r--spec/frontend/cycle_analytics/utils_spec.js50
-rw-r--r--spec/frontend/cycle_analytics/value_stream_metrics_spec.js128
-rw-r--r--spec/frontend/design_management/components/design_notes/design_discussion_spec.js38
-rw-r--r--spec/frontend/design_management/mock_data/apollo_mock.js37
-rw-r--r--spec/frontend/editor/source_editor_markdown_ext_spec.js327
-rw-r--r--spec/frontend/fixtures/analytics.rb17
-rw-r--r--spec/frontend/issue_show/components/fields/type_spec.js14
-rw-r--r--spec/frontend/jobs/components/log/mock_data.js9
-rw-r--r--spec/frontend/jobs/store/utils_spec.js16
-rw-r--r--spec/frontend/projects/terraform_notification/terraform_notification_spec.js27
-rw-r--r--spec/helpers/issues_helper_spec.rb74
-rw-r--r--spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/templates/Terraform/base_latest_gitlab_ci_yaml_spec.rb26
-rw-r--r--spec/lib/gitlab/ci/templates/terraform_gitlab_ci_yaml_spec.rb46
-rw-r--r--spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb6
-rw-r--r--spec/models/instance_configuration_spec.rb35
-rw-r--r--spec/models/project_feature_spec.rb2
-rw-r--r--spec/models/project_statistics_spec.rb6
-rw-r--r--spec/services/projects/update_pages_service_spec.rb7
-rw-r--r--spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb20
-rw-r--r--spec/support/shared_examples/models/update_project_statistics_shared_examples.rb110
26 files changed, 887 insertions, 267 deletions
diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb
index 704adfa568d..a13aed35768 100644
--- a/spec/features/cycle_analytics_spec.rb
+++ b/spec/features/cycle_analytics_spec.rb
@@ -7,6 +7,7 @@ RSpec.describe 'Value Stream Analytics', :js do
let_it_be(:guest) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:stage_table_selector) { '[data-testid="vsa-stage-table"]' }
+ let_it_be(:metrics_selector) { "[data-testid='vsa-time-metrics']" }
let(:issue) { create(:issue, project: project, created_at: 2.days.ago) }
let(:milestone) { create(:milestone, project: project) }
@@ -26,11 +27,13 @@ RSpec.describe 'Value Stream Analytics', :js do
wait_for_requests
end
- it 'shows pipeline summary' do
- expect(new_issues_counter).to have_content('-')
- expect(commits_counter).to have_content('-')
- expect(deploys_counter).to have_content('-')
- expect(deployment_frequency_counter).to have_content('-')
+ it 'displays metrics' do
+ aggregate_failures 'with relevant values' do
+ expect(new_issues_counter).to have_content('-')
+ expect(commits_counter).to have_content('-')
+ expect(deploys_counter).to have_content('-')
+ expect(deployment_frequency_counter).to have_content('-')
+ end
end
it 'shows active stage with empty message' do
@@ -60,11 +63,15 @@ RSpec.describe 'Value Stream Analytics', :js do
visit project_cycle_analytics_path(project)
end
- it 'shows pipeline summary' do
- expect(new_issues_counter).to have_content('1')
- expect(commits_counter).to have_content('2')
- expect(deploys_counter).to have_content('1')
- expect(deployment_frequency_counter).to have_content('0')
+ it 'displays metrics' do
+ metrics_tiles = page.find(metrics_selector)
+
+ aggregate_failures 'with relevant values' do
+ expect(metrics_tiles).to have_content('Commit')
+ expect(metrics_tiles).to have_content('Deploy')
+ expect(metrics_tiles).to have_content('Deployment Frequency')
+ expect(metrics_tiles).to have_content('New Issue')
+ end
end
it 'shows data on each stage', :sidekiq_might_not_need_inline do
@@ -96,7 +103,7 @@ RSpec.describe 'Value Stream Analytics', :js do
end
it 'shows only relevant data' do
- expect(new_issues_counter).to have_content('1')
+ expect(new_issue_counter).to have_content('1')
end
end
end
@@ -116,7 +123,7 @@ RSpec.describe 'Value Stream Analytics', :js do
end
it 'does not show the commit stats' do
- expect(page).to have_no_selector(:xpath, commits_counter_selector)
+ expect(page.find(metrics_selector)).not_to have_selector("#commits")
end
it 'needs permissions to see restricted stages' do
@@ -130,28 +137,29 @@ RSpec.describe 'Value Stream Analytics', :js do
end
end
- def new_issues_counter
- find(:xpath, "//p[contains(text(),'New Issue')]/preceding-sibling::h3")
+ def find_metric_tile(sel)
+ page.find("#{metrics_selector} #{sel}")
end
- def commits_counter_selector
- "//p[contains(text(),'Commits')]/preceding-sibling::h3"
+ # When now use proper pluralization for the metric names, which affects the id
+ def new_issue_counter
+ find_metric_tile("#new-issue")
end
- def commits_counter
- find(:xpath, commits_counter_selector)
+ def new_issues_counter
+ find_metric_tile("#new-issues")
end
- def deploys_counter
- find(:xpath, "//p[contains(text(),'Deploy')]/preceding-sibling::h3", match: :first)
+ def commits_counter
+ find_metric_tile("#commits")
end
- def deployment_frequency_counter_selector
- "//p[contains(text(),'Deployment Frequency')]/preceding-sibling::h3"
+ def deploys_counter
+ find_metric_tile("#deploys")
end
def deployment_frequency_counter
- find(:xpath, deployment_frequency_counter_selector)
+ find_metric_tile("#deployment-frequency")
end
def expect_issue_to_be_present
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index 5ca20028485..4bad67acc87 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -6,13 +6,13 @@ RSpec.describe 'New/edit issue', :js do
include ActionView::Helpers::JavaScriptHelper
include FormHelper
- let!(:project) { create(:project) }
- let!(:user) { create(:user)}
- let!(:user2) { create(:user)}
- let!(:milestone) { create(:milestone, project: project) }
- let!(:label) { create(:label, project: project) }
- let!(:label2) { create(:label, project: project) }
- let!(:issue) { create(:issue, project: project, assignees: [user], milestone: milestone) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user)}
+ let_it_be(:user2) { create(:user)}
+ let_it_be(:milestone) { create(:milestone, project: project) }
+ let_it_be(:label) { create(:label, project: project) }
+ let_it_be(:label2) { create(:label, project: project) }
+ let_it_be(:issue) { create(:issue, project: project, assignees: [user], milestone: milestone) }
before do
stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
@@ -234,6 +234,28 @@ RSpec.describe 'New/edit issue', :js do
expect(page).to have_selector('.atwho-view')
end
+ describe 'displays issue type options in the dropdown' do
+ before do
+ page.within('.issue-form') do
+ click_button 'Issue'
+ end
+ end
+
+ it 'correctly displays the Issue type option with an icon', :aggregate_failures do
+ page.within('[data-testid="issue-type-select-dropdown"]') do
+ expect(page).to have_selector('[data-testid="issue-type-issue-icon"]')
+ expect(page).to have_content('Issue')
+ end
+ end
+
+ it 'correctly displays the Incident type option with an icon', :aggregate_failures do
+ page.within('[data-testid="issue-type-select-dropdown"]') do
+ expect(page).to have_selector('[data-testid="issue-type-incident-icon"]')
+ expect(page).to have_content('Incident')
+ end
+ end
+ end
+
describe 'milestone' do
let!(:milestone) { create(:milestone, title: '">&lt;img src=x onerror=alert(document.domain)&gt;', project: project) }
diff --git a/spec/frontend/cycle_analytics/base_spec.js b/spec/frontend/cycle_analytics/base_spec.js
index 0b1a4f7ad1c..71830eed3ef 100644
--- a/spec/frontend/cycle_analytics/base_spec.js
+++ b/spec/frontend/cycle_analytics/base_spec.js
@@ -6,6 +6,7 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import BaseComponent from '~/cycle_analytics/components/base.vue';
import PathNavigation from '~/cycle_analytics/components/path_navigation.vue';
import StageTable from '~/cycle_analytics/components/stage_table.vue';
+import ValueStreamMetrics from '~/cycle_analytics/components/value_stream_metrics.vue';
import { NOT_ENOUGH_DATA_ERROR } from '~/cycle_analytics/constants';
import initState from '~/cycle_analytics/store/state';
import {
@@ -23,6 +24,7 @@ const selectedStageEvents = issueEvents.events;
const noDataSvgPath = 'path/to/no/data';
const noAccessSvgPath = 'path/to/no/access';
const selectedStageCount = stageCounts[selectedStage.id];
+const fullPath = 'full/path/to/foo';
Vue.use(Vuex);
@@ -34,6 +36,7 @@ const defaultState = {
createdBefore,
createdAfter,
stageCounts,
+ endpoints: { fullPath },
};
function createStore({ initialState = {}, initialGetters = {} }) {
@@ -45,6 +48,10 @@ function createStore({ initialState = {}, initialGetters = {} }) {
},
getters: {
pathNavigationData: () => transformedProjectStagePathData,
+ filterParams: () => ({
+ created_after: createdAfter,
+ created_before: createdBefore,
+ }),
...initialGetters,
},
});
@@ -67,11 +74,17 @@ function createComponent({ initialState, initialGetters } = {}) {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findPathNavigation = () => wrapper.findComponent(PathNavigation);
-const findOverviewMetrics = () => wrapper.findByTestId('vsa-stage-overview-metrics');
+const findOverviewMetrics = () => wrapper.findComponent(ValueStreamMetrics);
const findStageTable = () => wrapper.findComponent(StageTable);
const findStageEvents = () => findStageTable().props('stageEvents');
const findEmptyStageTitle = () => wrapper.findComponent(GlEmptyState).props('title');
+const hasMetricsRequests = (reqs) => {
+ const foundReqs = findOverviewMetrics().props('requests');
+ expect(foundReqs.length).toEqual(reqs.length);
+ expect(foundReqs.map(({ name }) => name)).toEqual(reqs);
+};
+
describe('Value stream analytics component', () => {
beforeEach(() => {
wrapper = createComponent({ initialState: { selectedStage, selectedStageEvents } });
@@ -94,6 +107,10 @@ describe('Value stream analytics component', () => {
expect(findOverviewMetrics().exists()).toBe(true);
});
+ it('passes requests prop to the metrics component', () => {
+ hasMetricsRequests(['recent activity']);
+ });
+
it('renders the stage table', () => {
expect(findStageTable().exists()).toBe(true);
});
@@ -110,6 +127,16 @@ describe('Value stream analytics component', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
+ describe('with `cycleAnalyticsForGroups=true` license', () => {
+ beforeEach(() => {
+ wrapper = createComponent({ initialState: { features: { cycleAnalyticsForGroups: true } } });
+ });
+
+ it('passes requests prop to the metrics component', () => {
+ hasMetricsRequests(['time summary', 'recent activity']);
+ });
+ });
+
describe('isLoading = true', () => {
beforeEach(() => {
wrapper = createComponent({
@@ -121,14 +148,14 @@ describe('Value stream analytics component', () => {
expect(findPathNavigation().props('loading')).toBe(true);
});
- it('does not render the overview metrics', () => {
- expect(findOverviewMetrics().exists()).toBe(false);
- });
-
it('does not render the stage table', () => {
expect(findStageTable().exists()).toBe(false);
});
+ it('renders the overview metrics', () => {
+ expect(findOverviewMetrics().exists()).toBe(true);
+ });
+
it('renders the loading icon', () => {
expect(findLoadingIcon().exists()).toBe(true);
});
diff --git a/spec/frontend/cycle_analytics/mock_data.js b/spec/frontend/cycle_analytics/mock_data.js
index 71bb9fc63ed..c29e81edf46 100644
--- a/spec/frontend/cycle_analytics/mock_data.js
+++ b/spec/frontend/cycle_analytics/mock_data.js
@@ -15,8 +15,11 @@ export const getStageByTitle = (stages, title) =>
const fixtureEndpoints = {
customizableCycleAnalyticsStagesAndEvents: 'projects/analytics/value_stream_analytics/stages',
stageEvents: (stage) => `projects/analytics/value_stream_analytics/events/${stage}`,
+ metricsData: 'projects/analytics/value_stream_analytics/summary',
};
+export const metricsData = getJSONFixture(fixtureEndpoints.metricsData);
+
export const customizableStagesAndEvents = getJSONFixture(
fixtureEndpoints.customizableCycleAnalyticsStagesAndEvents,
);
diff --git a/spec/frontend/cycle_analytics/store/mutations_spec.js b/spec/frontend/cycle_analytics/store/mutations_spec.js
index 3d4a1ac672e..7fcfef98547 100644
--- a/spec/frontend/cycle_analytics/store/mutations_spec.js
+++ b/spec/frontend/cycle_analytics/store/mutations_spec.js
@@ -6,8 +6,6 @@ import {
selectedStage,
rawIssueEvents,
issueEvents,
- rawData,
- convertedData,
selectedValueStream,
rawValueStreamStages,
valueStreamStages,
@@ -90,18 +88,17 @@ describe('Project Value Stream Analytics mutations', () => {
});
it.each`
- mutation | payload | stateKey | value
- ${types.SET_DATE_RANGE} | ${DEFAULT_DAYS_TO_DISPLAY} | ${'daysInPast'} | ${DEFAULT_DAYS_TO_DISPLAY}
- ${types.SET_DATE_RANGE} | ${DEFAULT_DAYS_TO_DISPLAY} | ${'createdAfter'} | ${mockCreatedAfter}
- ${types.SET_DATE_RANGE} | ${DEFAULT_DAYS_TO_DISPLAY} | ${'createdBefore'} | ${mockCreatedBefore}
- ${types.SET_LOADING} | ${true} | ${'isLoading'} | ${true}
- ${types.SET_LOADING} | ${false} | ${'isLoading'} | ${false}
- ${types.SET_SELECTED_VALUE_STREAM} | ${selectedValueStream} | ${'selectedValueStream'} | ${selectedValueStream}
- ${types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS} | ${rawData} | ${'summary'} | ${convertedData.summary}
- ${types.RECEIVE_VALUE_STREAMS_SUCCESS} | ${[selectedValueStream]} | ${'valueStreams'} | ${[selectedValueStream]}
- ${types.RECEIVE_VALUE_STREAM_STAGES_SUCCESS} | ${{ stages: rawValueStreamStages }} | ${'stages'} | ${valueStreamStages}
- ${types.RECEIVE_STAGE_MEDIANS_SUCCESS} | ${rawStageMedians} | ${'medians'} | ${formattedStageMedians}
- ${types.RECEIVE_STAGE_COUNTS_SUCCESS} | ${rawStageCounts} | ${'stageCounts'} | ${stageCounts}
+ mutation | payload | stateKey | value
+ ${types.SET_DATE_RANGE} | ${DEFAULT_DAYS_TO_DISPLAY} | ${'daysInPast'} | ${DEFAULT_DAYS_TO_DISPLAY}
+ ${types.SET_DATE_RANGE} | ${DEFAULT_DAYS_TO_DISPLAY} | ${'createdAfter'} | ${mockCreatedAfter}
+ ${types.SET_DATE_RANGE} | ${DEFAULT_DAYS_TO_DISPLAY} | ${'createdBefore'} | ${mockCreatedBefore}
+ ${types.SET_LOADING} | ${true} | ${'isLoading'} | ${true}
+ ${types.SET_LOADING} | ${false} | ${'isLoading'} | ${false}
+ ${types.SET_SELECTED_VALUE_STREAM} | ${selectedValueStream} | ${'selectedValueStream'} | ${selectedValueStream}
+ ${types.RECEIVE_VALUE_STREAMS_SUCCESS} | ${[selectedValueStream]} | ${'valueStreams'} | ${[selectedValueStream]}
+ ${types.RECEIVE_VALUE_STREAM_STAGES_SUCCESS} | ${{ stages: rawValueStreamStages }} | ${'stages'} | ${valueStreamStages}
+ ${types.RECEIVE_STAGE_MEDIANS_SUCCESS} | ${rawStageMedians} | ${'medians'} | ${formattedStageMedians}
+ ${types.RECEIVE_STAGE_COUNTS_SUCCESS} | ${rawStageCounts} | ${'stageCounts'} | ${stageCounts}
`(
'$mutation with $payload will set $stateKey to $value',
({ mutation, payload, stateKey, value }) => {
diff --git a/spec/frontend/cycle_analytics/utils_spec.js b/spec/frontend/cycle_analytics/utils_spec.js
index b8bc2bf512a..69fed879fd8 100644
--- a/spec/frontend/cycle_analytics/utils_spec.js
+++ b/spec/frontend/cycle_analytics/utils_spec.js
@@ -1,40 +1,24 @@
import { useFakeDate } from 'helpers/fake_date';
import {
- decorateData,
transformStagesForPathNavigation,
timeSummaryForPathNavigation,
medianTimeToParsedSeconds,
formatMedianValues,
filterStagesByHiddenStatus,
calculateFormattedDayInPast,
+ prepareTimeMetricsData,
} from '~/cycle_analytics/utils';
+import { slugify } from '~/lib/utils/text_utility';
import {
selectedStage,
- rawData,
- convertedData,
allowedStages,
stageMedians,
pathNavIssueMetric,
rawStageMedians,
+ metricsData,
} from './mock_data';
describe('Value stream analytics utils', () => {
- describe('decorateData', () => {
- const result = decorateData(rawData);
- it('returns the summary data', () => {
- expect(result.summary).toEqual(convertedData.summary);
- });
-
- it('returns `-` for summary data that has no value', () => {
- const singleSummaryResult = decorateData({
- stats: [],
- permissions: { issue: true },
- summary: [{ value: null, title: 'Commits' }],
- });
- expect(singleSummaryResult.summary).toEqual([{ value: '-', title: 'Commits' }]);
- });
- });
-
describe('transformStagesForPathNavigation', () => {
const stages = allowedStages;
const response = transformStagesForPathNavigation({
@@ -129,4 +113,32 @@ describe('Value stream analytics utils', () => {
expect(calculateFormattedDayInPast(5)).toEqual({ now: '1815-12-10', past: '1815-12-05' });
});
});
+
+ describe('prepareTimeMetricsData', () => {
+ let prepared;
+ const [first, second] = metricsData;
+ const firstKey = slugify(first.title);
+ const secondKey = slugify(second.title);
+
+ beforeEach(() => {
+ prepared = prepareTimeMetricsData([first, second], {
+ [firstKey]: { description: 'Is a value that is good' },
+ });
+ });
+
+ it('will add a `key` based on the title', () => {
+ expect(prepared).toMatchObject([{ key: firstKey }, { key: secondKey }]);
+ });
+
+ it('will add a `label` key', () => {
+ expect(prepared).toMatchObject([{ label: 'New Issues' }, { label: 'Commits' }]);
+ });
+
+ it('will add a popover description using the key if it is provided', () => {
+ expect(prepared).toMatchObject([
+ { description: 'Is a value that is good' },
+ { description: '' },
+ ]);
+ });
+ });
});
diff --git a/spec/frontend/cycle_analytics/value_stream_metrics_spec.js b/spec/frontend/cycle_analytics/value_stream_metrics_spec.js
new file mode 100644
index 00000000000..ffdb49a828c
--- /dev/null
+++ b/spec/frontend/cycle_analytics/value_stream_metrics_spec.js
@@ -0,0 +1,128 @@
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
+import { GlSingleStat } from '@gitlab/ui/dist/charts';
+import { shallowMount } from '@vue/test-utils';
+import waitForPromises from 'helpers/wait_for_promises';
+import { METRIC_TYPE_SUMMARY } from '~/api/analytics_api';
+import ValueStreamMetrics from '~/cycle_analytics/components/value_stream_metrics.vue';
+import createFlash from '~/flash';
+import { group, metricsData } from './mock_data';
+
+jest.mock('~/flash');
+
+describe('ValueStreamMetrics', () => {
+ let wrapper;
+ let mockGetValueStreamSummaryMetrics;
+
+ const { full_path: requestPath } = group;
+ const fakeReqName = 'Mock metrics';
+ const metricsRequestFactory = () => ({
+ request: mockGetValueStreamSummaryMetrics,
+ endpoint: METRIC_TYPE_SUMMARY,
+ name: fakeReqName,
+ });
+
+ const createComponent = ({ requestParams = {} } = {}) => {
+ return shallowMount(ValueStreamMetrics, {
+ propsData: {
+ requestPath,
+ requestParams,
+ requests: [metricsRequestFactory()],
+ },
+ });
+ };
+
+ const findMetrics = () => wrapper.findAllComponents(GlSingleStat);
+
+ const expectToHaveRequest = (fields) => {
+ expect(mockGetValueStreamSummaryMetrics).toHaveBeenCalledWith({
+ endpoint: METRIC_TYPE_SUMMARY,
+ requestPath,
+ ...fields,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('with successful requests', () => {
+ beforeEach(() => {
+ mockGetValueStreamSummaryMetrics = jest.fn().mockResolvedValue({ data: metricsData });
+ wrapper = createComponent();
+ });
+
+ it('will display a loader with pending requests', async () => {
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.find(GlSkeletonLoading).exists()).toBe(true);
+ });
+
+ describe('with data loaded', () => {
+ beforeEach(async () => {
+ await waitForPromises();
+ });
+
+ it('fetches data from the value stream analytics endpoint', () => {
+ expectToHaveRequest({ params: {} });
+ });
+
+ it.each`
+ index | value | title | unit
+ ${0} | ${metricsData[0].value} | ${metricsData[0].title} | ${metricsData[0].unit}
+ ${1} | ${metricsData[1].value} | ${metricsData[1].title} | ${metricsData[1].unit}
+ ${2} | ${metricsData[2].value} | ${metricsData[2].title} | ${metricsData[2].unit}
+ ${3} | ${metricsData[3].value} | ${metricsData[3].title} | ${metricsData[3].unit}
+ `(
+ 'renders a single stat component for the $title with value and unit',
+ ({ index, value, title, unit }) => {
+ const metric = findMetrics().at(index);
+ expect(metric.props()).toMatchObject({ value, title, unit: unit ?? '' });
+ },
+ );
+
+ it('will not display a loading icon', () => {
+ expect(wrapper.find(GlSkeletonLoading).exists()).toBe(false);
+ });
+
+ describe('with additional params', () => {
+ beforeEach(async () => {
+ wrapper = createComponent({
+ requestParams: {
+ 'project_ids[]': [1],
+ created_after: '2020-01-01',
+ created_before: '2020-02-01',
+ },
+ });
+
+ await waitForPromises();
+ });
+
+ it('fetches data for the `getValueStreamSummaryMetrics` request', () => {
+ expectToHaveRequest({
+ params: {
+ 'project_ids[]': [1],
+ created_after: '2020-01-01',
+ created_before: '2020-02-01',
+ },
+ });
+ });
+ });
+ });
+ });
+
+ describe('with a request failing', () => {
+ beforeEach(async () => {
+ mockGetValueStreamSummaryMetrics = jest.fn().mockRejectedValue();
+ wrapper = createComponent();
+
+ await waitForPromises();
+ });
+
+ it('it should render an error message', () => {
+ expect(createFlash).toHaveBeenCalledWith({
+ message: `There was an error while fetching value stream analytics ${fakeReqName} data.`,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
index efadb9b717d..9335d800a16 100644
--- a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
@@ -17,6 +17,8 @@ const defaultMockDiscussion = {
notes,
};
+const DEFAULT_TODO_COUNT = 2;
+
describe('Design discussions component', () => {
let wrapper;
@@ -41,8 +43,14 @@ describe('Design discussions component', () => {
},
};
const mutate = jest.fn().mockResolvedValue({ data: { createNote: { errors: [] } } });
+ const readQuery = jest.fn().mockReturnValue({
+ project: {
+ issue: { designCollection: { designs: { nodes: [{ currentUserTodos: { nodes: [] } }] } } },
+ },
+ });
const $apollo = {
mutate,
+ provider: { clients: { defaultClient: { readQuery } } },
};
function createComponent(props = {}, data = {}) {
@@ -69,6 +77,12 @@ describe('Design discussions component', () => {
$apollo,
$route: {
hash: '#note_1',
+ params: {
+ id: 1,
+ },
+ query: {
+ version: null,
+ },
},
},
});
@@ -138,7 +152,13 @@ describe('Design discussions component', () => {
});
describe('when discussion is resolved', () => {
+ let dispatchEventSpy;
+
beforeEach(() => {
+ dispatchEventSpy = jest.spyOn(document, 'dispatchEvent');
+ jest.spyOn(document, 'querySelector').mockReturnValue({
+ innerText: DEFAULT_TODO_COUNT,
+ });
createComponent({
discussion: {
...defaultMockDiscussion,
@@ -174,6 +194,24 @@ describe('Design discussions component', () => {
expect(findResolveIcon().props('name')).toBe('check-circle-filled');
});
+ it('emit todo:toggle when discussion is resolved', async () => {
+ createComponent(
+ { discussionWithOpenForm: defaultMockDiscussion.id },
+ { discussionComment: 'test', isFormRendered: true },
+ );
+ findResolveButton().trigger('click');
+ findReplyForm().vm.$emit('submitForm');
+
+ await mutate();
+ await wrapper.vm.$nextTick();
+
+ const dispatchedEvent = dispatchEventSpy.mock.calls[0][0];
+
+ expect(dispatchEventSpy).toHaveBeenCalledTimes(1);
+ expect(dispatchedEvent.detail).toEqual({ count: DEFAULT_TODO_COUNT });
+ expect(dispatchedEvent.type).toBe('todo:toggle');
+ });
+
describe('when replies are expanded', () => {
beforeEach(() => {
findRepliesWidget().vm.$emit('toggle');
diff --git a/spec/frontend/design_management/mock_data/apollo_mock.js b/spec/frontend/design_management/mock_data/apollo_mock.js
index e53ad2e6afe..cdd07a16e90 100644
--- a/spec/frontend/design_management/mock_data/apollo_mock.js
+++ b/spec/frontend/design_management/mock_data/apollo_mock.js
@@ -172,3 +172,40 @@ export const moveDesignMutationResponseWithErrors = {
},
},
};
+
+export const resolveCommentMutationResponse = {
+ discussionToggleResolve: {
+ discussion: {
+ noteable: {
+ id: 'gid://gitlab/DesignManagement::Design/1',
+ currentUserTodos: {
+ nodes: [],
+ __typename: 'TodoConnection',
+ },
+ __typename: 'Design',
+ },
+ __typename: 'Discussion',
+ },
+ errors: [],
+ __typename: 'DiscussionToggleResolvePayload',
+ },
+};
+
+export const getDesignQueryResponse = {
+ project: {
+ issue: {
+ designCollection: {
+ designs: {
+ nodes: [
+ {
+ id: 'gid://gitlab/DesignManagement::Design/1',
+ currentUserTodos: {
+ nodes: [{ id: 'gid://gitlab/Todo::1' }],
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+};
diff --git a/spec/frontend/editor/source_editor_markdown_ext_spec.js b/spec/frontend/editor/source_editor_markdown_ext_spec.js
index 943e21250b4..d9a6b65f986 100644
--- a/spec/frontend/editor/source_editor_markdown_ext_spec.js
+++ b/spec/frontend/editor/source_editor_markdown_ext_spec.js
@@ -1,16 +1,34 @@
+import MockAdapter from 'axios-mock-adapter';
import { Range, Position } from 'monaco-editor';
+import waitForPromises from 'helpers/wait_for_promises';
+import {
+ EXTENSION_MARKDOWN_PREVIEW_PANEL_CLASS,
+ EXTENSION_MARKDOWN_PREVIEW_ACTION_ID,
+ EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH,
+ EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS,
+} from '~/editor/constants';
import { EditorMarkdownExtension } from '~/editor/extensions/source_editor_markdown_ext';
import SourceEditor from '~/editor/source_editor';
+import createFlash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import syntaxHighlight from '~/syntax_highlight';
+
+jest.mock('~/syntax_highlight');
+jest.mock('~/flash');
describe('Markdown Extension for Source Editor', () => {
let editor;
let instance;
let editorEl;
+ let panelSpy;
+ let mockAxios;
+ const projectPath = 'fooGroup/barProj';
const firstLine = 'This is a';
const secondLine = 'multiline';
const thirdLine = 'string with some **markup**';
const text = `${firstLine}\n${secondLine}\n${thirdLine}`;
const filePath = 'foo.md';
+ const responseData = '<div>FooBar</div>';
const setSelection = (startLineNumber = 1, startColumn = 1, endLineNumber = 1, endColumn = 1) => {
const selection = new Range(startLineNumber, startColumn, endLineNumber, endColumn);
@@ -22,7 +40,13 @@ describe('Markdown Extension for Source Editor', () => {
const selectionToString = () => instance.getSelection().toString();
const positionToString = () => instance.getPosition().toString();
+ const togglePreview = async () => {
+ instance.togglePreview();
+ await waitForPromises();
+ };
+
beforeEach(() => {
+ mockAxios = new MockAdapter(axios);
setFixtures('<div id="editor" data-editor-loading></div>');
editorEl = document.getElementById('editor');
editor = new SourceEditor();
@@ -31,12 +55,313 @@ describe('Markdown Extension for Source Editor', () => {
blobPath: filePath,
blobContent: text,
});
- editor.use(new EditorMarkdownExtension());
+ editor.use(new EditorMarkdownExtension({ instance, projectPath }));
+ panelSpy = jest.spyOn(EditorMarkdownExtension, 'togglePreviewPanel');
});
afterEach(() => {
instance.dispose();
editorEl.remove();
+ mockAxios.restore();
+ });
+
+ it('sets up the instance', () => {
+ expect(instance.preview).toEqual({
+ el: undefined,
+ action: expect.any(Object),
+ shown: false,
+ });
+ expect(instance.projectPath).toBe(projectPath);
+ });
+
+ describe('cleanup', () => {
+ beforeEach(async () => {
+ mockAxios.onPost().reply(200, { body: responseData });
+ await togglePreview();
+ });
+
+ it('removes the contextual menu action', () => {
+ expect(instance.getAction(EXTENSION_MARKDOWN_PREVIEW_ACTION_ID)).toBeDefined();
+
+ instance.cleanup();
+
+ expect(instance.getAction(EXTENSION_MARKDOWN_PREVIEW_ACTION_ID)).toBe(null);
+ });
+
+ it('toggles the `shown` flag', () => {
+ expect(instance.preview.shown).toBe(true);
+ instance.cleanup();
+ expect(instance.preview.shown).toBe(false);
+ });
+
+ it('toggles the panel only if the preview is visible', () => {
+ const { el: previewEl } = instance.preview;
+ const parentEl = previewEl.parentElement;
+
+ expect(previewEl).toBeVisible();
+ expect(parentEl.classList.contains(EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS)).toBe(true);
+
+ instance.cleanup();
+ expect(previewEl).toBeHidden();
+ expect(parentEl.classList.contains(EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS)).toBe(
+ false,
+ );
+
+ instance.cleanup();
+ expect(previewEl).toBeHidden();
+ expect(parentEl.classList.contains(EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS)).toBe(
+ false,
+ );
+ });
+
+ it('toggles the layout only if the preview is visible', () => {
+ const { width } = instance.getLayoutInfo();
+
+ expect(instance.preview.shown).toBe(true);
+
+ instance.cleanup();
+
+ const { width: newWidth } = instance.getLayoutInfo();
+ expect(newWidth === width / EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH).toBe(true);
+
+ instance.cleanup();
+ expect(newWidth === width / EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH).toBe(true);
+ });
+ });
+
+ describe('fetchPreview', () => {
+ const group = 'foo';
+ const project = 'bar';
+ const setData = (path, g, p) => {
+ instance.projectPath = path;
+ document.body.setAttribute('data-group', g);
+ document.body.setAttribute('data-project', p);
+ };
+ const fetchPreview = async () => {
+ instance.fetchPreview();
+ await waitForPromises();
+ };
+
+ beforeEach(() => {
+ mockAxios.onPost().reply(200, { body: responseData });
+ });
+
+ it('correctly fetches preview based on projectPath', async () => {
+ setData(projectPath, group, project);
+ await fetchPreview();
+ expect(mockAxios.history.post[0].url).toBe(`/${projectPath}/preview_markdown`);
+ expect(mockAxios.history.post[0].data).toEqual(JSON.stringify({ text }));
+ });
+
+ it('correctly fetches preview based on group and project data attributes', async () => {
+ setData(undefined, group, project);
+ await fetchPreview();
+ expect(mockAxios.history.post[0].url).toBe(`/${group}/${project}/preview_markdown`);
+ expect(mockAxios.history.post[0].data).toEqual(JSON.stringify({ text }));
+ });
+
+ it('puts the fetched content into the preview DOM element', async () => {
+ instance.preview.el = editorEl.parentElement;
+ await fetchPreview();
+ expect(instance.preview.el.innerHTML).toEqual(responseData);
+ });
+
+ it('applies syntax highlighting to the preview content', async () => {
+ instance.preview.el = editorEl.parentElement;
+ await fetchPreview();
+ expect(syntaxHighlight).toHaveBeenCalled();
+ });
+
+ it('catches the errors when fetching the preview', async () => {
+ mockAxios.onPost().reply(500);
+
+ await fetchPreview();
+ expect(createFlash).toHaveBeenCalled();
+ });
+ });
+
+ describe('setupPreviewAction', () => {
+ it('adds the contextual menu action', () => {
+ expect(instance.getAction(EXTENSION_MARKDOWN_PREVIEW_ACTION_ID)).toBeDefined();
+ });
+
+ it('does not set up action if one already exists', () => {
+ jest.spyOn(instance, 'addAction').mockImplementation();
+
+ instance.setupPreviewAction();
+ expect(instance.addAction).not.toHaveBeenCalled();
+ });
+
+ it('toggles preview when the action is triggered', () => {
+ jest.spyOn(instance, 'togglePreview').mockImplementation();
+
+ expect(instance.togglePreview).not.toHaveBeenCalled();
+
+ const action = instance.getAction(EXTENSION_MARKDOWN_PREVIEW_ACTION_ID);
+ action.run();
+
+ expect(instance.togglePreview).toHaveBeenCalled();
+ });
+ });
+
+ describe('togglePreview', () => {
+ beforeEach(() => {
+ mockAxios.onPost().reply(200, { body: responseData });
+ });
+
+ it('toggles preview flag on instance', () => {
+ expect(instance.preview.shown).toBe(false);
+
+ instance.togglePreview();
+ expect(instance.preview.shown).toBe(true);
+
+ instance.togglePreview();
+ expect(instance.preview.shown).toBe(false);
+ });
+
+ describe('model language changes', () => {
+ const plaintextPath = 'foo.txt';
+ const markdownPath = 'foo.md';
+ let cleanupSpy;
+ let actionSpy;
+
+ beforeEach(() => {
+ cleanupSpy = jest.spyOn(instance, 'cleanup');
+ actionSpy = jest.spyOn(instance, 'setupPreviewAction');
+ instance.togglePreview();
+ });
+
+ it('cleans up when switching away from markdown', async () => {
+ expect(instance.cleanup).not.toHaveBeenCalled();
+ expect(instance.setupPreviewAction).not.toHaveBeenCalled();
+
+ instance.updateModelLanguage(plaintextPath);
+
+ expect(cleanupSpy).toHaveBeenCalled();
+ expect(actionSpy).not.toHaveBeenCalled();
+ });
+
+ it('re-enables the action when switching back to markdown', () => {
+ instance.updateModelLanguage(plaintextPath);
+
+ jest.clearAllMocks();
+
+ instance.updateModelLanguage(markdownPath);
+
+ expect(cleanupSpy).not.toHaveBeenCalled();
+ expect(actionSpy).toHaveBeenCalled();
+ });
+
+ it('does not re-enable the action if we do not change the language', () => {
+ instance.updateModelLanguage(markdownPath);
+
+ expect(cleanupSpy).not.toHaveBeenCalled();
+ expect(actionSpy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('panel DOM element set up', () => {
+ it('sets up an element to contain the preview and stores it on instance', () => {
+ expect(instance.preview.el).toBeUndefined();
+
+ instance.togglePreview();
+
+ expect(instance.preview.el).toBeDefined();
+ expect(instance.preview.el.classList.contains(EXTENSION_MARKDOWN_PREVIEW_PANEL_CLASS)).toBe(
+ true,
+ );
+ });
+
+ it('re-uses existing preview DOM element on repeated calls', () => {
+ instance.togglePreview();
+ const origPreviewEl = instance.preview.el;
+ instance.togglePreview();
+
+ expect(instance.preview.el).toBe(origPreviewEl);
+ });
+
+ it('hides the preview DOM element by default', () => {
+ panelSpy.mockImplementation();
+ instance.togglePreview();
+ expect(instance.preview.el.style.display).toBe('none');
+ });
+ });
+
+ describe('preview layout setup', () => {
+ it('sets correct preview layout', () => {
+ jest.spyOn(instance, 'layout');
+ const { width, height } = instance.getLayoutInfo();
+
+ instance.togglePreview();
+
+ expect(instance.layout).toHaveBeenCalledWith({
+ width: width * EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH,
+ height,
+ });
+ });
+ });
+
+ describe('preview panel', () => {
+ it('toggles preview CSS class on the editor', () => {
+ expect(editorEl.classList.contains(EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS)).toBe(
+ false,
+ );
+ instance.togglePreview();
+ expect(editorEl.classList.contains(EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS)).toBe(
+ true,
+ );
+ instance.togglePreview();
+ expect(editorEl.classList.contains(EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS)).toBe(
+ false,
+ );
+ });
+
+ it('toggles visibility of the preview DOM element', async () => {
+ await togglePreview();
+ expect(instance.preview.el.style.display).toBe('block');
+ await togglePreview();
+ expect(instance.preview.el.style.display).toBe('none');
+ });
+
+ describe('hidden preview DOM element', () => {
+ it('listens to model changes and re-fetches preview', async () => {
+ expect(mockAxios.history.post).toHaveLength(0);
+ await togglePreview();
+ expect(mockAxios.history.post).toHaveLength(1);
+
+ instance.setValue('New Value');
+ await waitForPromises();
+ expect(mockAxios.history.post).toHaveLength(2);
+ });
+
+ it('stores disposable listener for model changes', async () => {
+ expect(instance.modelChangeListener).toBeUndefined();
+ await togglePreview();
+ expect(instance.modelChangeListener).toBeDefined();
+ });
+ });
+
+ describe('already visible preview', () => {
+ beforeEach(async () => {
+ await togglePreview();
+ mockAxios.resetHistory();
+ });
+
+ it('does not re-fetch the preview', () => {
+ instance.togglePreview();
+ expect(mockAxios.history.post).toHaveLength(0);
+ });
+
+ it('disposes the model change event listener', () => {
+ const disposeSpy = jest.fn();
+ instance.modelChangeListener = {
+ dispose: disposeSpy,
+ };
+ instance.togglePreview();
+ expect(disposeSpy).toHaveBeenCalled();
+ });
+ });
+ });
});
describe('getSelectedText', () => {
diff --git a/spec/frontend/fixtures/analytics.rb b/spec/frontend/fixtures/analytics.rb
index 87cfbb72382..6d106dce166 100644
--- a/spec/frontend/fixtures/analytics.rb
+++ b/spec/frontend/fixtures/analytics.rb
@@ -51,4 +51,21 @@ RSpec.describe 'Analytics (JavaScript fixtures)', :sidekiq_inline do
end
end
end
+
+ describe Projects::Analytics::CycleAnalytics::SummaryController, type: :controller do
+ render_views
+ let(:params) { { namespace_id: group, project_id: project, value_stream_id: value_stream_id } }
+
+ before do
+ project.add_developer(user)
+
+ sign_in(user)
+ end
+
+ it "projects/analytics/value_stream_analytics/summary" do
+ get(:show, params: params, format: :json)
+
+ expect(response).to be_successful
+ end
+ end
end
diff --git a/spec/frontend/issue_show/components/fields/type_spec.js b/spec/frontend/issue_show/components/fields/type_spec.js
index 0c8af60d50d..fac745716d7 100644
--- a/spec/frontend/issue_show/components/fields/type_spec.js
+++ b/spec/frontend/issue_show/components/fields/type_spec.js
@@ -1,4 +1,4 @@
-import { GlFormGroup, GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlFormGroup, GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -35,6 +35,9 @@ describe('Issue type field component', () => {
const findTypeFromGroup = () => wrapper.findComponent(GlFormGroup);
const findTypeFromDropDown = () => wrapper.findComponent(GlDropdown);
const findTypeFromDropDownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findTypeFromDropDownItemAt = (at) => findTypeFromDropDownItems().at(at);
+ const findTypeFromDropDownItemIconAt = (at) =>
+ findTypeFromDropDownItems().at(at).findComponent(GlIcon);
const createComponent = ({ data } = {}) => {
fakeApollo = createMockApollo([], mockResolvers);
@@ -60,6 +63,15 @@ describe('Issue type field component', () => {
wrapper.destroy();
});
+ it.each`
+ at | text | icon
+ ${0} | ${IssuableTypes[0].text} | ${IssuableTypes[0].icon}
+ ${1} | ${IssuableTypes[1].text} | ${IssuableTypes[1].icon}
+ `(`renders the issue type $text with an icon in the dropdown`, ({ at, text, icon }) => {
+ expect(findTypeFromDropDownItemIconAt(at).attributes('name')).toBe(icon);
+ expect(findTypeFromDropDownItemAt(at).text()).toBe(text);
+ });
+
it('renders a form group with the correct label', () => {
expect(findTypeFromGroup().attributes('label')).toBe(i18n.label);
});
diff --git a/spec/frontend/jobs/components/log/mock_data.js b/spec/frontend/jobs/components/log/mock_data.js
index 76c35703106..3ff0bd73581 100644
--- a/spec/frontend/jobs/components/log/mock_data.js
+++ b/spec/frontend/jobs/components/log/mock_data.js
@@ -123,6 +123,15 @@ export const multipleCollapsibleSectionsMockData = [
},
];
+export const backwardsCompatibilityTrace = [
+ {
+ offset: 2365,
+ content: [],
+ section: 'download-artifacts',
+ section_duration: '00:01',
+ },
+];
+
export const originalTrace = [
{
offset: 1,
diff --git a/spec/frontend/jobs/store/utils_spec.js b/spec/frontend/jobs/store/utils_spec.js
index 35ac2945ab5..0c5fa150002 100644
--- a/spec/frontend/jobs/store/utils_spec.js
+++ b/spec/frontend/jobs/store/utils_spec.js
@@ -19,6 +19,7 @@ import {
collapsibleTrace,
collapsibleTraceIncremental,
multipleCollapsibleSectionsMockData,
+ backwardsCompatibilityTrace,
} from '../components/log/mock_data';
describe('Jobs Store Utils', () => {
@@ -297,6 +298,21 @@ describe('Jobs Store Utils', () => {
expect(result.parsedLines[1].lines).toEqual(expect.arrayContaining(innerSection));
});
});
+
+ describe('backwards compatibility', () => {
+ beforeEach(() => {
+ result = logLinesParser(backwardsCompatibilityTrace);
+ });
+
+ it('should return an object with a parsedLines prop', () => {
+ expect(result).toEqual(
+ expect.objectContaining({
+ parsedLines: expect.any(Array),
+ }),
+ );
+ expect(result.parsedLines).toHaveLength(1);
+ });
+ });
});
describe('findOffsetAndRemove', () => {
diff --git a/spec/frontend/projects/terraform_notification/terraform_notification_spec.js b/spec/frontend/projects/terraform_notification/terraform_notification_spec.js
index be34b207c4b..71c22998b08 100644
--- a/spec/frontend/projects/terraform_notification/terraform_notification_spec.js
+++ b/spec/frontend/projects/terraform_notification/terraform_notification_spec.js
@@ -5,19 +5,21 @@ import TerraformNotification from '~/projects/terraform_notification/components/
jest.mock('~/lib/utils/common_utils');
-const bannerDissmisedKey = 'terraform_notification_dismissed_for_project_1';
+const terraformImagePath = '/path/to/image';
+const bannerDismissedKey = 'terraform_notification_dismissed';
describe('TerraformNotificationBanner', () => {
let wrapper;
- const propsData = {
- projectId: 1,
+ const provideData = {
+ terraformImagePath,
+ bannerDismissedKey,
};
const findBanner = () => wrapper.findComponent(GlBanner);
beforeEach(() => {
wrapper = shallowMount(TerraformNotification, {
- propsData,
+ provide: provideData,
stubs: { GlBanner },
});
});
@@ -27,19 +29,6 @@ describe('TerraformNotificationBanner', () => {
parseBoolean.mockReturnValue(false);
});
- describe('when the dismiss cookie is set', () => {
- beforeEach(() => {
- parseBoolean.mockReturnValue(true);
- wrapper = shallowMount(TerraformNotification, {
- propsData,
- });
- });
-
- it('should not render the banner', () => {
- expect(findBanner().exists()).toBe(false);
- });
- });
-
describe('when the dismiss cookie is not set', () => {
it('should render the banner', () => {
expect(findBanner().exists()).toBe(true);
@@ -51,8 +40,8 @@ describe('TerraformNotificationBanner', () => {
await findBanner().vm.$emit('close');
});
- it('should set the cookie with the bannerDissmisedKey', () => {
- expect(setCookie).toHaveBeenCalledWith(bannerDissmisedKey, true);
+ it('should set the cookie with the bannerDismissedKey', () => {
+ expect(setCookie).toHaveBeenCalledWith(bannerDismissedKey, true);
});
it('should remove the banner', () => {
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index 96aba312ba3..f2222a6affe 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -1,14 +1,26 @@
# frozen_string_literal: true
-require "spec_helper"
+require 'spec_helper'
RSpec.describe IssuesHelper do
let(:project) { create(:project) }
let(:issue) { create :issue, project: project }
let(:ext_project) { create :redmine_project }
+ describe '#work_item_type_icon' do
+ it 'returns icon of all standard base types' do
+ WorkItem::Type.base_types.each do |type|
+ expect(work_item_type_icon(type[0])).to eq "issue-type-#{type[0].to_s.dasherize}"
+ end
+ end
+
+ it 'defaults to issue icon if type is unknown' do
+ expect(work_item_type_icon('invalid')).to eq 'issue-type-issue'
+ end
+ end
+
describe '#award_user_list' do
- it "returns a comma-separated list of the first X users" do
+ it 'returns a comma-separated list of the first X users' do
user = build_stubbed(:user, name: 'Joe')
awards = Array.new(3, build_stubbed(:award_emoji, user: user))
@@ -24,7 +36,7 @@ RSpec.describe IssuesHelper do
expect(award_user_list([award], nil)).to eq 'Joe'
end
- it "truncates lists" do
+ it 'truncates lists' do
user = build_stubbed(:user, name: 'Jane')
awards = Array.new(5, build_stubbed(:award_emoji, user: user))
@@ -32,14 +44,14 @@ RSpec.describe IssuesHelper do
.to eq('Jane, Jane, Jane, and 2 more.')
end
- it "displays the current user in front of other users" do
+ it 'displays the current user in front of other users' do
current_user = build_stubbed(:user)
my_award = build_stubbed(:award_emoji, user: current_user)
award = build_stubbed(:award_emoji, user: build_stubbed(:user, name: 'Jane'))
awards = Array.new(5, award).push(my_award)
expect(award_user_list(awards, current_user, limit: 2))
- .to eq("You, Jane, and 4 more.")
+ .to eq('You, Jane, and 4 more.')
end
end
@@ -54,19 +66,19 @@ RSpec.describe IssuesHelper do
end
end
- it "returns disabled string for unauthenticated user" do
- expect(helper.award_state_class(awardable, AwardEmoji.all, nil)).to eq("disabled")
+ it 'returns disabled string for unauthenticated user' do
+ expect(helper.award_state_class(awardable, AwardEmoji.all, nil)).to eq('disabled')
end
- it "returns disabled for a user that does not have access to the awardable" do
- expect(helper.award_state_class(awardable, AwardEmoji.all, build(:user))).to eq("disabled")
+ it 'returns disabled for a user that does not have access to the awardable' do
+ expect(helper.award_state_class(awardable, AwardEmoji.all, build(:user))).to eq('disabled')
end
- it "returns active string for author" do
- expect(helper.award_state_class(awardable, AwardEmoji.all, upvote.user)).to eq("active")
+ it 'returns active string for author' do
+ expect(helper.award_state_class(awardable, AwardEmoji.all, upvote.user)).to eq('active')
end
- it "is blank for a user that has access to the awardable" do
+ it 'is blank for a user that has access to the awardable' do
user = build(:user)
expect(helper).to receive(:can?).with(user, :award_emoji, awardable).and_return(true)
@@ -74,40 +86,40 @@ RSpec.describe IssuesHelper do
end
end
- describe "awards_sort" do
- it "sorts a hash so thumbsup and thumbsdown are always on top" do
- data = { "thumbsdown" => "some value", "lifter" => "some value", "thumbsup" => "some value" }
+ describe 'awards_sort' do
+ it 'sorts a hash so thumbsup and thumbsdown are always on top' do
+ data = { 'thumbsdown' => 'some value', 'lifter' => 'some value', 'thumbsup' => 'some value' }
expect(awards_sort(data).keys).to eq(%w(thumbsup thumbsdown lifter))
end
end
- describe "#link_to_discussions_to_resolve" do
- describe "passing only a merge request" do
+ describe '#link_to_discussions_to_resolve' do
+ describe 'passing only a merge request' do
let(:merge_request) { create(:merge_request) }
- it "links just the merge request" do
+ it 'links just the merge request' do
expected_path = project_merge_request_path(merge_request.project, merge_request)
expect(link_to_discussions_to_resolve(merge_request, nil)).to include(expected_path)
end
- it "contains the reference to the merge request" do
+ it 'contains the reference to the merge request' do
expect(link_to_discussions_to_resolve(merge_request, nil)).to include(merge_request.to_reference)
end
end
- describe "when passing a discussion" do
+ describe 'when passing a discussion' do
let(:diff_note) { create(:diff_note_on_merge_request) }
let(:merge_request) { diff_note.noteable }
let(:discussion) { diff_note.to_discussion }
- it "links to the merge request with first note if a single discussion was passed" do
+ it 'links to the merge request with first note if a single discussion was passed' do
expected_path = Gitlab::UrlBuilder.build(diff_note)
expect(link_to_discussions_to_resolve(merge_request, discussion)).to include(expected_path)
end
- it "contains both the reference to the merge request and a mention of the discussion" do
+ it 'contains both the reference to the merge request and a mention of the discussion' do
expect(link_to_discussions_to_resolve(merge_request, discussion)).to include("#{merge_request.to_reference} (discussion #{diff_note.id})")
end
end
@@ -235,13 +247,13 @@ RSpec.describe IssuesHelper do
end
describe '#use_startup_call' do
- it "returns false when a query param is present" do
+ it 'returns false when a query param is present' do
allow(controller.request).to receive(:query_parameters).and_return({ foo: 'bar' })
expect(helper.use_startup_call?).to eq(false)
end
- it "returns false when user has stored sort preference" do
+ it 'returns false when user has stored sort preference' do
controller.instance_variable_set(:@sort, 'updated_asc')
expect(helper.use_startup_call?).to eq(false)
@@ -265,13 +277,13 @@ RSpec.describe IssuesHelper do
it 'returns expected result' do
expected = {
- can_create_issue: "true",
- can_reopen_issue: "true",
- can_report_spam: "false",
- can_update_issue: "true",
+ can_create_issue: 'true',
+ can_reopen_issue: 'true',
+ can_report_spam: 'false',
+ can_update_issue: 'true',
iid: issue.iid,
- is_issue_author: "false",
- issue_type: "issue",
+ is_issue_author: 'false',
+ issue_type: 'issue',
new_issue_path: new_project_issue_path(project),
project_path: project.full_path,
report_abuse_path: new_abuse_report_path(user_id: issue.author.id, ref_url: issue_url(issue)),
@@ -345,7 +357,7 @@ RSpec.describe IssuesHelper do
end
it 'returns manual ordering class' do
- expect(helper.issue_manual_ordering_class).to eq("manual-ordering")
+ expect(helper.issue_manual_ordering_class).to eq('manual-ordering')
end
context 'when manual sorting disabled' do
diff --git a/spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb
index f0b305d944a..4685d843ce0 100644
--- a/spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb
@@ -2,8 +2,8 @@
require 'spec_helper'
-RSpec.describe 'Terraform/Base.latest.gitlab-ci.yml' do
- subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Terraform/Base.latest') }
+RSpec.describe 'Terraform/Base.gitlab-ci.yml' do
+ subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Terraform/Base') }
describe 'the created pipeline' do
let(:default_branch) { 'master' }
diff --git a/spec/lib/gitlab/ci/templates/Terraform/base_latest_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Terraform/base_latest_gitlab_ci_yaml_spec.rb
new file mode 100644
index 00000000000..e35f2eabe8e
--- /dev/null
+++ b/spec/lib/gitlab/ci/templates/Terraform/base_latest_gitlab_ci_yaml_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Terraform/Base.latest.gitlab-ci.yml' do
+ subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Terraform/Base.latest') }
+
+ describe 'the created pipeline' do
+ let(:default_branch) { 'master' }
+ let(:pipeline_branch) { default_branch }
+ let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) }
+ let(:user) { project.owner }
+ let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
+ let(:pipeline) { service.execute!(:push).payload }
+ let(:build_names) { pipeline.builds.pluck(:name) }
+
+ before do
+ stub_ci_pipeline_yaml_file(template.content)
+ allow(project).to receive(:default_branch).and_return(default_branch)
+ end
+
+ it 'does not create any jobs' do
+ expect(build_names).to be_empty
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/templates/terraform_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/terraform_gitlab_ci_yaml_spec.rb
new file mode 100644
index 00000000000..936cd6ac8aa
--- /dev/null
+++ b/spec/lib/gitlab/ci/templates/terraform_gitlab_ci_yaml_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Terraform.gitlab-ci.yml' do
+ before do
+ allow(Gitlab::Template::GitlabCiYmlTemplate).to receive(:excluded_patterns).and_return([])
+ end
+
+ subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Terraform') }
+
+ describe 'the created pipeline' do
+ let(:default_branch) { project.default_branch_or_main }
+ let(:pipeline_branch) { default_branch }
+ let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) }
+ let(:user) { project.owner }
+ let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
+ let(:pipeline) { service.execute!(:push).payload }
+ let(:build_names) { pipeline.builds.pluck(:name) }
+
+ before do
+ stub_ci_pipeline_yaml_file(template.content)
+ allow(project).to receive(:default_branch).and_return(default_branch)
+ end
+
+ context 'on master branch' do
+ it 'creates init, validate and build jobs', :aggregate_failures do
+ expect(pipeline.errors).to be_empty
+ expect(build_names).to include('init', 'validate', 'build', 'deploy')
+ end
+ end
+
+ context 'outside the master branch' do
+ let(:pipeline_branch) { 'patch-1' }
+
+ before do
+ project.repository.create_branch(pipeline_branch, default_branch)
+ end
+
+ it 'does not creates a deploy and a test job', :aggregate_failures do
+ expect(pipeline.errors).to be_empty
+ expect(build_names).not_to include('deploy')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb
index dec26284b41..3d1306e82a5 100644
--- a/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb
@@ -25,7 +25,8 @@ RSpec.describe 'Terraform.latest.gitlab-ci.yml' do
end
context 'on master branch' do
- it 'creates init, validate and build jobs' do
+ it 'creates init, validate and build jobs', :aggregate_failures do
+ expect(pipeline.errors).to be_empty
expect(build_names).to include('init', 'validate', 'build', 'deploy')
end
end
@@ -37,7 +38,8 @@ RSpec.describe 'Terraform.latest.gitlab-ci.yml' do
project.repository.create_branch(pipeline_branch, default_branch)
end
- it 'does not creates a deploy and a test job' do
+ it 'does not creates a deploy and a test job', :aggregate_failures do
+ expect(pipeline.errors).to be_empty
expect(build_names).not_to include('deploy')
end
end
diff --git a/spec/models/instance_configuration_spec.rb b/spec/models/instance_configuration_spec.rb
index 23068d5b76b..9544f0fe6ec 100644
--- a/spec/models/instance_configuration_spec.rb
+++ b/spec/models/instance_configuration_spec.rb
@@ -97,6 +97,41 @@ RSpec.describe InstanceConfiguration do
end
end
+ describe '#package_file_size_limits' do
+ let_it_be(:plan1) { create(:plan, name: 'plan1', title: 'Plan 1') }
+ let_it_be(:plan2) { create(:plan, name: 'plan2', title: 'Plan 2') }
+
+ before do
+ create(:plan_limits,
+ plan: plan1,
+ conan_max_file_size: 1001,
+ maven_max_file_size: 1002,
+ npm_max_file_size: 1003,
+ nuget_max_file_size: 1004,
+ pypi_max_file_size: 1005,
+ terraform_module_max_file_size: 1006,
+ generic_packages_max_file_size: 1007
+ )
+ create(:plan_limits,
+ plan: plan2,
+ conan_max_file_size: 1101,
+ maven_max_file_size: 1102,
+ npm_max_file_size: 1103,
+ nuget_max_file_size: 1104,
+ pypi_max_file_size: 1105,
+ terraform_module_max_file_size: 1106,
+ generic_packages_max_file_size: 1107
+ )
+ end
+
+ it 'returns package file size limits' do
+ file_size_limits = subject.settings[:package_file_size_limits]
+
+ expect(file_size_limits[:Plan1]).to eq({ conan: 1001, maven: 1002, npm: 1003, nuget: 1004, pypi: 1005, terraform_module: 1006, generic: 1007 })
+ expect(file_size_limits[:Plan2]).to eq({ conan: 1101, maven: 1102, npm: 1103, nuget: 1104, pypi: 1105, terraform_module: 1106, generic: 1107 })
+ end
+ end
+
describe '#rate_limits' do
before do
Gitlab::CurrentSettings.current_application_settings.update!(
diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb
index 0e276191af1..5f720f8c4f8 100644
--- a/spec/models/project_feature_spec.rb
+++ b/spec/models/project_feature_spec.rb
@@ -8,6 +8,8 @@ RSpec.describe ProjectFeature do
let(:project) { create(:project) }
let(:user) { create(:user) }
+ it { is_expected.to belong_to(:project) }
+
describe 'PRIVATE_FEATURES_MIN_ACCESS_LEVEL_FOR_PRIVATE_PROJECT' do
it 'has higher level than that of PRIVATE_FEATURES_MIN_ACCESS_LEVEL' do
described_class::PRIVATE_FEATURES_MIN_ACCESS_LEVEL_FOR_PRIVATE_PROJECT.each do |feature, level|
diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb
index cb1baa02e96..ba769e830fd 100644
--- a/spec/models/project_statistics_spec.rb
+++ b/spec/models/project_statistics_spec.rb
@@ -401,12 +401,6 @@ RSpec.describe ProjectStatistics do
let(:stat) { :build_artifacts_size }
it_behaves_like 'a statistic that increases storage_size asynchronously'
-
- it_behaves_like 'a statistic that increases storage_size' do
- before do
- stub_feature_flags(efficient_counter_attribute: false)
- end
- end
end
context 'when adjusting :pipeline_artifacts_size' do
diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb
index aef8244c3d3..0f21736eda0 100644
--- a/spec/services/projects/update_pages_service_spec.rb
+++ b/spec/services/projects/update_pages_service_spec.rb
@@ -156,13 +156,6 @@ RSpec.describe Projects::UpdatePagesService do
expect(GenericCommitStatus.last.description).to eq("pages site contains 3 file entries, while limit is set to 2")
end
- it 'does not limit pages file count if feature is disabled' do
- stub_feature_flags(pages_limit_entries_count: false)
- create(:plan_limits, :default_plan, pages_file_entries: 2)
-
- expect(execute).to eq(:success)
- end
-
it 'removes pages after destroy' do
expect(PagesWorker).to receive(:perform_in)
expect(project.pages_deployed?).to be_falsey
diff --git a/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb
index 99a09993900..f92ed3d7396 100644
--- a/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb
@@ -62,26 +62,6 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes|
.to raise_error(ActiveModel::MissingAttributeError)
end
end
-
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(efficient_counter_attribute: false)
- end
-
- it 'delegates to ActiveRecord update!' do
- expect { subject }
- .to change { model.reset.read_attribute(attribute) }.by(increment)
- end
-
- it 'does not increment the counter in Redis' do
- subject
-
- Gitlab::Redis::SharedState.with do |redis|
- counter = redis.get(model.counter_key(attribute))
- expect(counter).to be_nil
- end
- end
- end
end
end
end
diff --git a/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb b/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb
index 7b591ad84d1..2e01de2ea84 100644
--- a/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb
+++ b/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb
@@ -22,116 +22,6 @@ RSpec.shared_examples 'UpdateProjectStatistics' do |with_counter_attribute|
it { is_expected.to be_new_record }
- context 'when feature flag efficient_counter_attribute is disabled' do
- before do
- stub_feature_flags(efficient_counter_attribute: false)
- end
-
- context 'when creating' do
- it 'updates the project statistics' do
- delta0 = reload_stat
-
- subject.save!
-
- delta1 = reload_stat
-
- expect(delta1).to eq(delta0 + read_attribute)
- expect(delta1).to be > delta0
- end
-
- it 'schedules a namespace statistics worker' do
- expect(Namespaces::ScheduleAggregationWorker)
- .to receive(:perform_async).once
-
- subject.save!
- end
- end
-
- context 'when updating' do
- let(:delta) { 42 }
-
- before do
- subject.save!
- end
-
- it 'updates project statistics' do
- expect(ProjectStatistics)
- .to receive(:increment_statistic)
- .and_call_original
-
- subject.write_attribute(statistic_attribute, read_attribute + delta)
-
- expect { subject.save! }
- .to change { reload_stat }
- .by(delta)
- end
-
- it 'schedules a namespace statistics worker' do
- expect(Namespaces::ScheduleAggregationWorker)
- .to receive(:perform_async).once
-
- subject.write_attribute(statistic_attribute, read_attribute + delta)
- subject.save!
- end
-
- it 'avoids N + 1 queries' do
- subject.write_attribute(statistic_attribute, read_attribute + delta)
-
- control_count = ActiveRecord::QueryRecorder.new do
- subject.save!
- end
-
- subject.write_attribute(statistic_attribute, read_attribute + delta)
-
- expect do
- subject.save!
- end.not_to exceed_query_limit(control_count)
- end
- end
-
- context 'when destroying' do
- before do
- subject.save!
- end
-
- it 'updates the project statistics' do
- delta0 = reload_stat
-
- subject.destroy!
-
- delta1 = reload_stat
-
- expect(delta1).to eq(delta0 - read_attribute)
- expect(delta1).to be < delta0
- end
-
- it 'schedules a namespace statistics worker' do
- expect(Namespaces::ScheduleAggregationWorker)
- .to receive(:perform_async).once
-
- subject.destroy!
- end
-
- context 'when it is destroyed from the project level' do
- it 'does not update the project statistics' do
- expect(ProjectStatistics)
- .not_to receive(:increment_statistic)
-
- project.update!(pending_delete: true)
- project.destroy!
- end
-
- it 'does not schedule a namespace statistics worker' do
- expect(Namespaces::ScheduleAggregationWorker)
- .not_to receive(:perform_async)
-
- project.update!(pending_delete: true)
- project.destroy!
- end
- end
- end
- end
-
def expect_flush_counter_increments_worker_performed
expect(FlushCounterIncrementsWorker)
.to receive(:perform_in)