# frozen_string_literal: true require 'spec_helper' RSpec.describe IssuablesHelper, feature_category: :team_planning do let(:label) { build_stubbed(:label) } let(:label2) { build_stubbed(:label) } describe '#users_dropdown_label' do let(:user) { build_stubbed(:user) } let(:user2) { build_stubbed(:user) } it 'returns unassigned' do expect(users_dropdown_label([])).to eq('Unassigned') end it 'returns selected user\'s name' do expect(users_dropdown_label([user])).to eq(user.name) end it 'returns selected user\'s name and counter' do expect(users_dropdown_label([user, user2])).to eq("#{user.name} + 1 more") end end describe '#group_dropdown_label' do let(:group) { create(:group) } let(:default) { 'default label' } it 'returns default group label when group_id is nil' do expect(group_dropdown_label(nil, default)).to eq('default label') end it 'returns "any group" when group_id is 0' do expect(group_dropdown_label('0', default)).to eq('Any group') end it 'returns group full path when a group was found for the provided id' do expect(group_dropdown_label(group.id, default)).to eq(group.full_name) end it 'returns default label when a group was not found for the provided id' do expect(group_dropdown_label(non_existing_record_id, default)).to eq('default label') end end describe '#assignees_label' do let(:issuable) { build(:merge_request) } let(:assignee1) { build_stubbed(:user, name: 'Jane Doe') } let(:assignee2) { build_stubbed(:user, name: 'John Doe') } before do allow(issuable).to receive(:assignees).and_return(assignees) end context 'when multiple assignees exist' do let(:assignees) { [assignee1, assignee2] } it 'returns assignee label with assignee names' do expect(helper.assignees_label(issuable)).to eq("Assignees: Jane Doe and John Doe") end it 'returns assignee label only with include_value: false' do expect(helper.assignees_label(issuable, include_value: false)).to eq("Assignees") end context 'when the name contains a URL' do let(:assignees) { [build_stubbed(:user, name: 'www.gitlab.com')] } it 'returns sanitized name' do expect(helper.assignees_label(issuable)).to eq("Assignee: www_gitlab_com") end end end context 'when one assignee exists' do let(:assignees) { [assignee1] } it 'returns assignee label with no names' do expect(helper.assignees_label(issuable)).to eq("Assignee: Jane Doe") end it 'returns assignee label only with include_value: false' do expect(helper.assignees_label(issuable, include_value: false)).to eq("Assignee") end end context 'when no assignees exist' do let(:assignees) { [] } it 'returns assignee label with no names' do expect(helper.assignees_label(issuable)).to eq("Assignees: ") end it 'returns assignee label only with include_value: false' do expect(helper.assignees_label(issuable, include_value: false)).to eq("Assignees") end end end describe '#assigned_issuables_count', feature_category: :team_planning do context 'when issuable is issues' do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project).tap { |p| p.add_developer(user) } } subject { helper.assigned_issuables_count(:issues) } before do allow(helper).to receive(:current_user).and_return(user) end context 'when assigned issues count is over 100' do let_it_be(:issues) { create_list(:issue, 101, project: project, assignees: [user]) } it { is_expected.to eq 100 } end end end describe '#assigned_open_issues_count_text', feature_category: :team_planning do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project).tap { |p| p.add_developer(user) } } subject { helper.assigned_open_issues_count_text } before do allow(helper).to receive(:current_user).and_return(user) end context 'when assigned issues count is over 99' do let_it_be(:issues) { create_list(:issue, 100, project: project, assignees: [user]) } it { is_expected.to eq '99+' } end end describe '#issuables_state_counter_text' do let_it_be(:user) { create(:user) } describe 'state text' do context 'when number of issuables can be generated' do before do allow(helper).to receive(:issuables_count_for_state).and_return(42) end it 'returns navigation with badges' do expect(helper.issuables_state_counter_text(:issues, :opened, true)) .to eq('Open 42') expect(helper.issuables_state_counter_text(:issues, :closed, true)) .to eq('Closed 42') expect(helper.issuables_state_counter_text(:merge_requests, :merged, true)) .to eq('Merged 42') expect(helper.issuables_state_counter_text(:merge_requests, :all, true)) .to eq('All 42') end end context 'when count cannot be generated' do before do allow(helper).to receive(:issuables_count_for_state).and_return(-1) end it 'returns navigation without badges' do expect(helper.issuables_state_counter_text(:issues, :opened, true)) .to eq('Open') expect(helper.issuables_state_counter_text(:issues, :closed, true)) .to eq('Closed') expect(helper.issuables_state_counter_text(:merge_requests, :merged, true)) .to eq('Merged') expect(helper.issuables_state_counter_text(:merge_requests, :all, true)) .to eq('All') end end context 'when count is over the threshold' do let_it_be(:group) { create(:group) } before do allow(helper).to receive(:issuables_count_for_state).and_return(1100) allow(helper).to receive(:parent).and_return(group) stub_const("Gitlab::IssuablesCountForState::THRESHOLD", 1000) end it 'returns truncated count' do expect(helper.issuables_state_counter_text(:issues, :opened, true)) .to eq('Open 1.1k') end end end end describe '#issuable_reference' do let(:project_namespace) { build_stubbed(:project_namespace) } let(:project) { build_stubbed(:project, project_namespace: project_namespace) } context 'when show_full_reference truthy' do it 'display issuable full reference' do assign(:show_full_reference, true) issue = build_stubbed(:issue, project: project) expect(helper.issuable_reference(issue)).to eql(issue.to_reference(full: true)) end end context 'when show_full_reference falsey' do context 'when @group present' do it 'display issuable reference to @group' do assign(:show_full_reference, nil) assign(:group, project.namespace) issue = build_stubbed(:issue, project: project) expect(helper.issuable_reference(issue)).to eql(issue.to_reference(project.namespace)) end end context 'when @project present' do it 'display issuable reference to @project' do assign(:show_full_reference, nil) assign(:group, nil) assign(:project, project) issue = build_stubbed(:issue, project: project) expect(helper.issuable_reference(issue)).to eql(issue.to_reference(project)) end end end end describe '#issuable_project_reference' do let(:project_namespace) { build_stubbed(:project_namespace) } let(:project) { build_stubbed(:project, project_namespace: project_namespace) } it 'display project name and simple reference with `#` to an issue' do issue = build_stubbed(:issue, project: project) expect(helper.issuable_project_reference(issue)).to eq("#{issue.project.full_name} ##{issue.iid}") end it 'display project name and simple reference with `!` to an MR' do merge_request = build_stubbed(:merge_request) expect(helper.issuable_project_reference(merge_request)).to eq("#{merge_request.project.full_name} !#{merge_request.iid}") end end describe '#updated_at_by' do let(:user) { create(:user) } let(:unedited_issuable) { create(:issue) } let(:edited_issuable) { create(:issue, last_edited_by: user, created_at: 3.days.ago, updated_at: 1.day.ago, last_edited_at: 2.days.ago) } let(:edited_updated_at_by) do { updatedAt: edited_issuable.last_edited_at.to_time.iso8601, updatedBy: { name: user.name, path: user_path(user) } } end it { expect(helper.updated_at_by(unedited_issuable)).to eq({}) } it { expect(helper.updated_at_by(edited_issuable)).to eq(edited_updated_at_by) } context 'when updated by a deleted user' do let(:edited_updated_at_by) do { updatedAt: edited_issuable.last_edited_at.to_time.iso8601, updatedBy: { name: User.ghost.name, path: user_path(User.ghost) } } end before do user.destroy! end it 'returns "Ghost user" as edited_by' do expect(helper.updated_at_by(edited_issuable.reload)).to eq(edited_updated_at_by) end end end describe '#issuable_initial_data' do let(:user) { create(:user) } before do allow(helper).to receive(:current_user).and_return(user) allow(helper).to receive(:can?).and_return(true) stub_commonmark_sourcepos_disabled end it 'returns the correct data for an issue' do issue = create(:issue, author: user, description: 'issue text') @project = issue.project expected_data = { endpoint: "/#{@project.full_path}/-/issues/#{issue.iid}", updateEndpoint: "/#{@project.full_path}/-/issues/#{issue.iid}.json", canUpdate: true, canDestroy: true, issuableRef: "##{issue.iid}", markdownPreviewPath: "/#{@project.full_path}/preview_markdown?target_id=#{issue.iid}&target_type=Issue", markdownDocsPath: '/help/user/markdown', lockVersion: issue.lock_version, projectPath: @project.path, projectId: @project.id, projectNamespace: @project.namespace.path, state: issue.state, initialTitleHtml: issue.title, initialTitleText: issue.title, initialDescriptionHtml: '
issue text
', initialDescriptionText: 'issue text', initialTaskCompletionStatus: { completed_count: 0, count: 0 }, issueType: 'issue', iid: issue.iid.to_s, isHidden: false } expect(helper.issuable_initial_data(issue)).to match(hash_including(expected_data)) end context 'for incident tab' do let(:incident) { create(:incident) } let(:params) do ActionController::Parameters.new({ controller: "projects/incidents", action: "show", namespace_id: "foo", project_id: "bar", id: incident.iid, incident_tab: 'timeline' }).permit! end it 'includes incident attributes' do @project = incident.project allow(helper).to receive(:safe_params).and_return(params) expected_data = { issueType: 'incident', hasLinkedAlerts: false, canUpdateTimelineEvent: true, currentPath: "/foo/bar/-/issues/incident/#{incident.iid}/timeline", currentTab: 'timeline' } expect(helper.issuable_initial_data(incident)).to match(hash_including(expected_data)) end end describe '#sentryIssueIdentifier' do let(:issue) { create(:issue, author: user) } before do assign(:project, issue.project) end it 'sets sentryIssueIdentifier to nil with no sentry issue' do expect(helper.issuable_initial_data(issue)[:sentryIssueIdentifier]) .to be_nil end it 'sets sentryIssueIdentifier to sentry_issue_identifier' do sentry_issue = create(:sentry_issue, issue: issue) expect(helper.issuable_initial_data(issue)[:sentryIssueIdentifier]) .to eq(sentry_issue.sentry_issue_identifier) end end describe '#zoomMeetingUrl in issue' do let(:issue) { create(:issue, author: user) } before do assign(:project, issue.project) end shared_examples 'sets zoomMeetingUrl to nil' do specify do expect(helper.issuable_initial_data(issue)[:zoomMeetingUrl]) .to be_nil end end context 'with no "added" zoom mettings' do it_behaves_like 'sets zoomMeetingUrl to nil' context 'with multiple removed meetings' do before do create(:zoom_meeting, issue: issue, issue_status: :removed) create(:zoom_meeting, issue: issue, issue_status: :removed) end it_behaves_like 'sets zoomMeetingUrl to nil' end end context 'with "added" zoom meeting' do before do create(:zoom_meeting, issue: issue) end shared_examples 'sets zoomMeetingUrl to canonical meeting url' do specify do expect(helper.issuable_initial_data(issue)) .to include(zoomMeetingUrl: 'https://zoom.us/j/123456789') end end it_behaves_like 'sets zoomMeetingUrl to canonical meeting url' context 'with muliple "removed" zoom meetings' do before do create(:zoom_meeting, issue: issue, issue_status: :removed) create(:zoom_meeting, issue: issue, issue_status: :removed) end it_behaves_like 'sets zoomMeetingUrl to canonical meeting url' end end end describe '#duplicatedToIssueUrl' do let(:issue) { create(:issue, author: user) } before do assign(:project, issue.project) end context 'when issue is duplicated' do before do allow(issue).to receive(:duplicated?).and_return(true) allow(issue).to receive(:duplicated_to).and_return(issue) end it 'returns url' do expect(helper.issuable_initial_data(issue)[:duplicatedToIssueUrl]).to be_truthy end end context 'when issue is not duplicated' do before do allow(issue).to receive(:duplicated?).and_return(false) end it 'returns nil' do expect(helper.issuable_initial_data(issue)[:duplicatedToIssueUrl]).to be_nil end end end describe '#movedToIssueUrl' do let(:issue) { create(:issue, author: user) } before do assign(:project, issue.project) end context 'when issue is moved' do before do allow(issue).to receive(:moved?).and_return(true) allow(issue).to receive(:moved_to).and_return(issue) end it 'returns url' do expect(helper.issuable_initial_data(issue)[:movedToIssueUrl]).to be_truthy end end context 'when issue is not moved' do before do allow(issue).to receive(:moved?).and_return(false) end it 'returns nil' do expect(helper.issuable_initial_data(issue)[:movedToIssueUrl]).to be_nil end end end end describe '#assignee_sidebar_data' do let(:user) { create(:user) } let(:merge_request) { nil } subject { helper.assignee_sidebar_data(user, merge_request: merge_request) } it 'returns hash of assignee data' do is_expected.to eql({ avatar_url: user.avatar_url, name: user.name, username: user.username }) end context 'with merge_request' do let(:merge_request) { build_stubbed(:merge_request) } where(can_merge: [true, false]) with_them do before do allow(merge_request).to receive(:can_be_merged_by?).and_return(can_merge) end it { is_expected.to include({ can_merge: can_merge }) } end end end describe '#issuable_squash_option?' do using RSpec::Parameterized::TableSyntax where(:issuable_persisted, :squash, :squash_enabled_by_default, :expectation) do true | true | true | true true | false | true | false false | false | false | false false | false | true | true false | true | false | false false | true | true | true end with_them do it 'returns the correct value' do project = double( squash_enabled_by_default?: squash_enabled_by_default ) issuable = double(persisted?: issuable_persisted, squash: squash) expect(helper.issuable_squash_option?(issuable, project)).to eq(expectation) end end end describe '#state_name_with_icon' do let_it_be(:project) { create(:project, :repository) } context 'for an issue' do let_it_be(:issue) { create(:issue, project: project) } let_it_be(:issue_closed) { create(:issue, :closed, project: project) } it 'returns the correct state name and icon when issue is open' do expect(helper.state_name_with_icon(issue)).to match_array([_('Open'), 'issues']) end it 'returns the correct state name and icon when issue is closed' do expect(helper.state_name_with_icon(issue_closed)).to match_array([_('Closed'), 'issue-closed']) end end context 'for a merge request' do let_it_be(:merge_request) { create(:merge_request, source_project: project) } let_it_be(:merge_request_merged) { create(:merge_request, :merged, source_project: project) } let_it_be(:merge_request_closed) { create(:merge_request, :closed, source_project: project) } it 'returns the correct state name and icon when merge request is open' do expect(helper.state_name_with_icon(merge_request)).to match_array([_('Open'), 'merge-request-open']) end it 'returns the correct state name and icon when merge request is merged' do expect(helper.state_name_with_icon(merge_request_merged)).to match_array([_('Merged'), 'merge']) end it 'returns the correct state name and icon when merge request is closed' do expect(helper.state_name_with_icon(merge_request_closed)).to match_array([_('Closed'), 'merge-request-close']) end end end describe '#issuable_display_type' do using RSpec::Parameterized::TableSyntax where(:issuable_type, :issuable_display_type) do :issue | 'issue' :incident | 'incident' :merge_request | 'merge request' end with_them do let(:issuable) { build_stubbed(issuable_type) } subject { helper.issuable_display_type(issuable) } it { is_expected.to eq(issuable_display_type) } end end describe '#issuable_type_selector_data' do using RSpec::Parameterized::TableSyntax let_it_be(:project) { create(:project) } where(:issuable_type, :issuable_display_type, :is_issue_allowed, :is_incident_allowed) do :issue | 'issue' | true | false :incident | 'incident' | false | true end with_them do let(:issuable) { build_stubbed(issuable_type) } before do allow(helper).to receive(:create_issue_type_allowed?).with(project, :issue).and_return(is_issue_allowed) allow(helper).to receive(:create_issue_type_allowed?).with(project, :incident).and_return(is_incident_allowed) assign(:project, project) end it 'returns the correct data for the issuable type selector' do expected_data = { selected_type: issuable_display_type, is_issue_allowed: is_issue_allowed.to_s, is_incident_allowed: is_incident_allowed.to_s, issue_path: new_project_issue_path(project), incident_path: new_project_issue_path(project, { issuable_template: 'incident', issue: { issue_type: 'incident' } }) } expect(helper.issuable_type_selector_data(issuable)).to match(expected_data) end end end describe '#issuable_label_selector_data' do let_it_be(:project) { create(:project, :repository) } context 'with a new issuable' do let_it_be(:issuable) { build(:issue, project: project) } it 'returns the expected data' do expect(helper.issuable_label_selector_data(project, issuable)).to match({ field_name: "#{issuable.class.model_name.param_key}[label_ids][]", full_path: project.full_path, initial_labels: '[]', issuable_type: issuable.issuable_type, labels_filter_base_path: project_issues_path(project), labels_manage_path: project_labels_path(project) }) end end context 'with an existing issuable' do let_it_be(:label) { create(:label, name: 'Bug') } let_it_be(:label2) { create(:label, name: 'Community contribution') } let_it_be(:issuable) do create(:merge_request, source_project: project, target_project: project, labels: [label, label2]) end it 'returns the expected data' do initial_labels = [ { __typename: "Label", id: label.id, title: label.title, description: label.description, color: label.color, text_color: label.text_color }, { __typename: "Label", id: label2.id, title: label2.title, description: label2.description, color: label2.color, text_color: label2.text_color } ] expect(helper.issuable_label_selector_data(project, issuable)).to match({ field_name: "#{issuable.class.model_name.param_key}[label_ids][]", full_path: project.full_path, initial_labels: initial_labels.to_json, issuable_type: issuable.issuable_type, labels_filter_base_path: project_merge_requests_path(project), labels_manage_path: project_labels_path(project) }) end end end end