BigW Consortium Gitlab

Commit 012c126d by Kushal Pandya

Merge branch '9-3-stable-rc5' into '9-3-stable'

Prepare 9.3 RC5 See merge request !12283
parents a23e9766 feeba2ec
......@@ -80,7 +80,18 @@ import initSettingsPanels from './settings_panels';
path = page.split(':');
shortcut_handler = null;
new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup();
$('.js-gfm-input').each((i, el) => {
const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
const enableGFM = gl.utils.convertPermissionToBoolean(el.dataset.supportsAutocomplete);
gfm.setup($(el), {
emojis: true,
members: enableGFM,
issues: enableGFM,
milestones: enableGFM,
mergeRequests: enableGFM,
labels: enableGFM,
});
});
function initBlob() {
new LineHighlighter();
......@@ -180,7 +191,7 @@ import initSettingsPanels from './settings_panels';
case 'groups:milestones:update':
new ZenMode();
new gl.DueDateSelectors();
new gl.GLForm($('.milestone-form'));
new gl.GLForm($('.milestone-form'), true);
break;
case 'projects:compare:show':
new gl.Diff();
......@@ -192,7 +203,7 @@ import initSettingsPanels from './settings_panels';
case 'projects:issues:new':
case 'projects:issues:edit':
shortcut_handler = new ShortcutsNavigation();
new gl.GLForm($('.issue-form'));
new gl.GLForm($('.issue-form'), true);
new IssuableForm($('.issue-form'));
new LabelsSelect();
new MilestoneSelect();
......@@ -203,7 +214,7 @@ import initSettingsPanels from './settings_panels';
case 'projects:merge_requests:edit':
new gl.Diff();
shortcut_handler = new ShortcutsNavigation();
new gl.GLForm($('.merge-request-form'));
new gl.GLForm($('.merge-request-form'), true);
new IssuableForm($('.merge-request-form'));
new LabelsSelect();
new MilestoneSelect();
......@@ -212,22 +223,24 @@ import initSettingsPanels from './settings_panels';
break;
case 'projects:tags:new':
new ZenMode();
new gl.GLForm($('.tag-form'));
new gl.GLForm($('.tag-form'), true);
new RefSelectDropdown($('.js-branch-select'), window.gl.availableRefs);
break;
case 'projects:snippets:new':
case 'projects:snippets:edit':
case 'projects:snippets:create':
case 'projects:snippets:update':
new gl.GLForm($('.snippet-form'), true);
break;
case 'snippets:new':
case 'snippets:edit':
case 'snippets:create':
case 'snippets:update':
new gl.GLForm($('.snippet-form'));
new gl.GLForm($('.snippet-form'), false);
break;
case 'projects:releases:edit':
new ZenMode();
new gl.GLForm($('.release-form'));
new gl.GLForm($('.release-form'), true);
break;
case 'projects:merge_requests:show':
new gl.Diff();
......@@ -475,7 +488,7 @@ import initSettingsPanels from './settings_panels';
new gl.Wikis();
shortcut_handler = new ShortcutsWiki();
new ZenMode();
new gl.GLForm($('.wiki-form'));
new gl.GLForm($('.wiki-form'), true);
break;
case 'snippets':
shortcut_handler = new ShortcutsNavigation();
......
......@@ -287,6 +287,10 @@ window.DropzoneInput = (function() {
$uploadingErrorMessage.html(message);
};
closeAlertMessage = function() {
return form.find('.div-dropzone-alert').alert('close');
};
form.find('.markdown-selector').click(function(e) {
e.preventDefault();
$(this).closest('.gfm-form').find('.div-dropzone').click();
......
......@@ -51,6 +51,11 @@ export default {
required: false,
default: '',
},
initialTaskStatus: {
type: String,
required: false,
default: '',
},
updatedAt: {
type: String,
required: false,
......@@ -105,6 +110,7 @@ export default {
updatedAt: this.updatedAt,
updatedByName: this.updatedByName,
updatedByPath: this.updatedByPath,
taskStatus: this.initialTaskStatus,
});
return {
......
......@@ -37,23 +37,12 @@
});
},
taskStatus() {
const taskRegexMatches = this.taskStatus.match(/(\d+) of (\d+)/);
const $issuableHeader = $('.issuable-meta');
const $tasks = $('#task_status', $issuableHeader);
const $tasksShort = $('#task_status_short', $issuableHeader);
if (taskRegexMatches) {
$tasks.text(this.taskStatus);
$tasksShort.text(`${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? 's' : ''}`);
} else {
$tasks.text('');
$tasksShort.text('');
}
this.updateTaskStatusText();
},
},
methods: {
renderGFM() {
$(this.$refs['gfm-entry-content']).renderGFM();
$(this.$refs['gfm-content']).renderGFM();
if (this.canUpdate) {
// eslint-disable-next-line no-new
......@@ -64,9 +53,24 @@
});
}
},
updateTaskStatusText() {
const taskRegexMatches = this.taskStatus.match(/(\d+) of ((?!0)\d+)/);
const $issuableHeader = $('.issuable-meta');
const $tasks = $('#task_status', $issuableHeader);
const $tasksShort = $('#task_status_short', $issuableHeader);
if (taskRegexMatches) {
$tasks.text(this.taskStatus);
$tasksShort.text(`${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? 's' : ''}`);
} else {
$tasks.text('');
$tasksShort.text('');
}
},
},
mounted() {
this.renderGFM();
this.updateTaskStatusText();
},
};
</script>
......
......@@ -45,6 +45,7 @@ document.addEventListener('DOMContentLoaded', () => {
updatedAt: this.updatedAt,
updatedByName: this.updatedByName,
updatedByPath: this.updatedByPath,
initialTaskStatus: this.initialTaskStatus,
},
});
},
......
export default class Store {
constructor({
titleHtml,
titleText,
descriptionHtml,
descriptionText,
updatedAt,
updatedByName,
updatedByPath,
}) {
this.state = {
titleHtml,
titleText,
descriptionHtml,
descriptionText,
taskStatus: '',
updatedAt,
updatedByName,
updatedByPath,
};
constructor(initialState) {
this.state = initialState;
this.formState = {
title: '',
confidential: false,
......
......@@ -322,7 +322,9 @@ const normalizeNewlines = function(str) {
Notes.updateNoteTargetSelector = function($note) {
const hash = gl.utils.getLocationHash();
$note.toggleClass('target', hash && $note.filter(`#${hash}`).length > 0);
// Needs to be an explicit true/false for the jQuery `toggleClass(force)`
const addTargetClass = Boolean(hash && $note.filter(`#${hash}`).length > 0);
$note.toggleClass('target', addTargetClass);
};
/*
......@@ -1267,7 +1269,6 @@ const normalizeNewlines = function(str) {
*/
Notes.prototype.createPlaceholderNote = function ({ formContent, uniqueId, isDiscussionNote, currentUsername, currentUserFullname, currentUserAvatar }) {
const discussionClass = isDiscussionNote ? 'discussion' : '';
const escapedFormContent = _.escape(formContent);
const $tempNote = $(
`<li id="${uniqueId}" class="note being-posted fade-in-half timeline-entry">
<div class="timeline-entry-inner">
......@@ -1287,7 +1288,7 @@ const normalizeNewlines = function(str) {
</div>
<div class="note-body">
<div class="note-text">
<p>${escapedFormContent}</p>
<p>${formContent}</p>
</div>
</div>
</div>
......
export default {
name: 'MRWidgetRelatedLinks',
props: {
isMerged: { type: Boolean, required: true },
relatedLinks: { type: Object, required: true },
},
computed: {
// TODO: the following should be handled by i18n
closingText() {
if (this.isMerged) {
return `Closed ${this.issueLabel('closing')}`;
}
return `Closes ${this.issueLabel('closing')}`;
},
hasLinks() {
const { closing, mentioned, assignToMe } = this.relatedLinks;
return closing || mentioned || assignToMe;
},
// TODO: the following should be handled by i18n
mentionedText() {
if (this.isMerged) {
if (this.hasMultipleIssues(this.relatedLinks.mentioned)) {
return 'are mentioned but were not closed';
}
return 'is mentioned but was not closed';
}
if (this.hasMultipleIssues(this.relatedLinks.mentioned)) {
return 'are mentioned but will not be closed';
}
return 'is mentioned but will not be closed';
},
},
methods: {
hasMultipleIssues(text) {
return !text ? false : text.match(/<\/a> and <a/);
return /<\/a>,? and <a/.test(text);
},
// TODO: the following should be handled by i18n
issueLabel(field) {
return this.hasMultipleIssues(this.relatedLinks[field]) ? 'issues' : 'issue';
},
verbLabel(field) {
return this.hasMultipleIssues(this.relatedLinks[field]) ? 'are' : 'is';
},
},
template: `
<section
v-if="hasLinks"
class="mr-info-list mr-links">
<div v-if="hasLinks">
<div class="legend"></div>
<p v-if="relatedLinks.closing">
Closes {{issueLabel('closing')}}
{{closingText}}
<span v-html="relatedLinks.closing"></span>.
</p>
<p v-if="relatedLinks.mentioned">
<span class="capitalize">{{issueLabel('mentioned')}}</span>
<span v-html="relatedLinks.mentioned"></span>
{{verbLabel('mentioned')}} mentioned but will not be closed.
{{mentionedText}}
</p>
<p v-if="relatedLinks.assignToMe">
<span v-html="relatedLinks.assignToMe"></span>
</p>
</section>
</div>
`,
};
/* global Flash */
import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
import mrWidgetRelatedLinks from '../../components/mr_widget_related_links';
import eventHub from '../../event_hub';
import '../../../flash';
export default {
name: 'MRWidgetMerged',
......@@ -11,6 +13,7 @@ export default {
},
components: {
'mr-widget-author-and-time': mrWidgetAuthorTime,
'mr-widget-related-links': mrWidgetRelatedLinks,
},
data() {
return {
......@@ -18,6 +21,9 @@ export default {
};
},
computed: {
shouldRenderRelatedLinks() {
return this.mr.relatedLinks && this.mr.isMerged;
},
shouldShowRemoveSourceBranch() {
const { sourceBranchRemoved, isRemovingSourceBranch, canRemoveSourceBranch } = this.mr;
......@@ -86,6 +92,10 @@ export default {
aria-hidden="true" />
The source branch is being removed.
</p>
<mr-widget-related-links
v-if="shouldRenderRelatedLinks"
:is-merged="mr.isMerged()"
:related-links="mr.relatedLinks" />
</section>
<div
v-if="shouldShowMergedButtons"
......
......@@ -48,7 +48,7 @@ export default {
return stateMaps.stateToComponentMap[this.mr.state];
},
shouldRenderMergeHelp() {
return stateMaps.statesToShowHelpWidget.indexOf(this.mr.state) > -1;
return !this.mr.isMerged;
},
shouldRenderPipelines() {
return Object.keys(this.mr.pipeline).length || this.mr.hasCI;
......@@ -238,9 +238,14 @@ export default {
:is="componentName"
:mr="mr"
:service="service" />
<mr-widget-related-links
<section
v-if="shouldRenderRelatedLinks"
:related-links="mr.relatedLinks" />
class="mr-info-list mr-links">
<div class="legend"></div>
<mr-widget-related-links
:is-merged="mr.isMerged"
:related-links="mr.relatedLinks" />
</section>
<mr-widget-merge-help v-if="shouldRenderMergeHelp" />
</div>
`,
......
import Timeago from 'timeago.js';
import { getStateKey } from '../dependencies';
const unmergedStates = [
'locked',
'conflicts',
'workInProgress',
'readyToMerge',
'checking',
'unresolvedDiscussions',
'pipelineFailed',
'pipelineBlocked',
'autoMergeFailed',
];
export default class MergeRequestStore {
constructor(data) {
......@@ -65,6 +77,7 @@ export default class MergeRequestStore {
this.mergeActionsContentPath = data.commit_change_content_path;
this.isRemovingSourceBranch = this.isRemovingSourceBranch || false;
this.isOpen = data.state === 'opened' || data.state === 'reopened' || false;
this.isMerged = unmergedStates.indexOf(data.state) === -1;
this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false;
this.canRemoveSourceBranch = currentUser.can_remove_source_branch || false;
this.canMerge = !!data.merge_path;
......
......@@ -19,19 +19,6 @@ const stateToComponentMap = {
shaMismatch: 'mr-widget-sha-mismatch',
};
const statesToShowHelpWidget = [
'locked',
'conflicts',
'workInProgress',
'readyToMerge',
'checking',
'unresolvedDiscussions',
'pipelineFailed',
'pipelineBlocked',
'autoMergeFailed',
];
export default {
stateToComponentMap,
statesToShowHelpWidget,
};
......@@ -372,6 +372,10 @@
margin-left: 12px;
}
&.mr-state-locked + .mr-info-list.mr-links {
margin-top: -16px;
}
&.empty-state {
.artwork {
margin-bottom: $gl-padding;
......
......@@ -509,11 +509,6 @@ ul.notes {
display: inline;
line-height: 20px;
@include notes-media('min', $screen-sm-min) {
margin-left: 10px;
line-height: 24px;
}
.fa {
color: $gray-darkest;
position: relative;
......
......@@ -66,12 +66,12 @@ module DiffHelper
discussions_left = discussions_right = nil
if left && (left.unchanged? || left.discussable?)
if left && left.discussable? && (left.unchanged? || left.removed?)
line_code = diff_file.line_code(left)
discussions_left = @grouped_diff_discussions[line_code]
end
if right&.discussable?
if right && right.discussable? && right.added?
line_code = diff_file.line_code(right)
discussions_right = @grouped_diff_discussions[line_code]
end
......
......@@ -138,8 +138,8 @@ module IssuablesHelper
end
output << "&ensp;".html_safe
output << content_tag(:span, issuable.task_status, id: "task_status", class: "hidden-xs hidden-sm")
output << content_tag(:span, issuable.task_status_short, id: "task_status_short", class: "hidden-md hidden-lg")
output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "hidden-xs hidden-sm")
output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "hidden-md hidden-lg")
output
end
......@@ -216,7 +216,8 @@ module IssuablesHelper
initialTitleHtml: markdown_field(issuable, :title),
initialTitleText: issuable.title,
initialDescriptionHtml: markdown_field(issuable, :description),
initialDescriptionText: issuable.description
initialDescriptionText: issuable.description,
initialTaskStatus: issuable.task_status
}
data.merge!(updated_at_by(issuable))
......
......@@ -42,7 +42,7 @@ class LegacyDiffNote < Note
end
def for_line?(line)
!line.meta? && diff_file.line_code(line) == self.line_code
line.discussable? && diff_file.line_code(line) == self.line_code
end
def original_line_code
......
......@@ -34,7 +34,7 @@ module Users
# Keep trying until we obtain the lease. If we don't do so we may end up
# not updating the list of authorized projects properly. To prevent
# hammering Redis too much we'll wait for a bit between retries.
sleep(1)
sleep(0.1)
end
begin
......
- @gfm_form = true
- current_text ||= nil
- supports_autocomplete = local_assigns.fetch(:supports_autocomplete, true)
- supports_slash_commands = local_assigns.fetch(:supports_slash_commands, false)
.zen-backdrop
- classes << ' js-gfm-input js-autosize markdown-area'
- if defined?(f) && f
= f.text_area attr, class: classes, placeholder: placeholder, data: { supports_slash_commands: supports_slash_commands }
= f.text_area attr,
class: classes,
placeholder: placeholder,
data: { supports_slash_commands: supports_slash_commands,
supports_autocomplete: supports_autocomplete }
- else
= text_area_tag attr, current_text, class: classes, placeholder: placeholder
%a.zen-control.zen-control-leave.js-zen-leave{ href: "#" }
......
- supports_autocomplete = local_assigns.fetch(:supports_autocomplete, true)
- supports_slash_commands = note_supports_slash_commands?(@note)
- if supports_slash_commands
- preview_url = preview_markdown_path(@project, slash_commands_target_type: @note.noteable_type, slash_commands_target_id: @note.noteable_id)
......@@ -27,7 +28,8 @@
attr: :note,
classes: 'note-textarea js-note-text',
placeholder: "Write a comment or drag your files here...",
supports_slash_commands: supports_slash_commands
supports_slash_commands: supports_slash_commands,
supports_autocomplete: supports_autocomplete
= render 'shared/notes/hints', supports_slash_commands: supports_slash_commands
.error-alert
......
......@@ -12,7 +12,7 @@
%a.author_link{ href: user_path(current_user) }
= image_tag avatar_icon(current_user), alt: current_user.to_reference, class: 'avatar s40'
.timeline-content.timeline-content-form
= render "shared/notes/form", view: diff_view
= render "shared/notes/form", view: diff_view, supports_autocomplete: autocomplete
- elsif !current_user
.disabled-comment.text-center.prepend-top-default
Please
......
---
title: Fix for cut & pasted images not working
merge_request:
author:
---
title: Reduce time spent waiting for certain Sidekiq jobs to complete
merge_request:
author:
......@@ -46,7 +46,10 @@ To disable artifacts site-wide, follow the steps below.
After a successful job, GitLab Runner uploads an archive containing the job
artifacts to GitLab.
To change the location where the artifacts are stored, follow the steps below.
### Using local storage
To change the location where the artifacts are stored locally, follow the steps
below.
---
......@@ -82,6 +85,13 @@ _The artifacts are stored by default in
1. Save the file and [restart GitLab][] for the changes to take effect.
### Using object storage
In [GitLab Enterprise Edition Premium][eep] you can use an object storage like
AWS S3 to store the artifacts.
[Learn how to use the object storage option.][ee-os]
## Expiring artifacts
If an expiry date is used for the artifacts, they are marked for deletion
......@@ -148,3 +158,5 @@ memory and disk I/O.
[reconfigure gitlab]: restart_gitlab.md "How to restart GitLab"
[restart gitlab]: restart_gitlab.md "How to restart GitLab"
[gitlab workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse "GitLab Workhorse repository"
[ee-os]: https://docs.gitlab.com/ee/administration/job_artifacts.html#using-object-storage
[eep]: https://about.gitlab.com/gitlab-ee/ "GitLab Enterprise Edition Premium"
......@@ -89,6 +89,7 @@ group.
| Create project in group | | | | ✓ | ✓ |
| Manage group members | | | | | ✓ |
| Remove group | | | | | ✓ |
| Manage group labels | | ✓ | ✓ | ✓ | ✓ |
## External Users
......
......@@ -10,43 +10,49 @@ module Gitlab
delegate :sidekiq_throttling_enabled?, to: :current_application_settings
def fake_application_settings
OpenStruct.new(::ApplicationSetting.defaults)
def fake_application_settings(defaults = ApplicationSetting.defaults)
FakeApplicationSettings.new(defaults)
end
private
def ensure_application_settings!
unless ENV['IN_MEMORY_APPLICATION_SETTINGS'] == 'true'
settings = retrieve_settings_from_database?
end
return in_memory_application_settings if ENV['IN_MEMORY_APPLICATION_SETTINGS'] == 'true'
settings || in_memory_application_settings
cached_application_settings || uncached_application_settings
end
def retrieve_settings_from_database?
settings = retrieve_settings_from_database_cache?
return settings if settings.present?
return fake_application_settings unless connect_to_db?
def cached_application_settings
begin
db_settings = ::ApplicationSetting.current
# In case Redis isn't running or the Redis UNIX socket file is not available
ApplicationSetting.cached
rescue ::Redis::BaseError, ::Errno::ENOENT
db_settings = ::ApplicationSetting.last
# In case Redis isn't running or the Redis UNIX socket file is not available
end
db_settings || ::ApplicationSetting.create_from_defaults
end
def retrieve_settings_from_database_cache?
def uncached_application_settings
return fake_application_settings unless connect_to_db?
# This loads from the database into the cache, so handle Redis errors
begin
settings = ApplicationSetting.cached
db_settings = ApplicationSetting.current
rescue ::Redis::BaseError, ::Errno::ENOENT
# In case Redis isn't running or the Redis UNIX socket file is not available
settings = nil
end
settings
# If there are pending migrations, it's possible there are columns that
# need to be added to the application settings. To prevent Rake tasks
# and other callers from failing, use any loaded settings and return
# defaults for missing columns.
if ActiveRecord::Migrator.needs_migration?
defaults = ApplicationSetting.defaults
defaults.merge!(db_settings.attributes.symbolize_keys) if db_settings.present?
return fake_application_settings(defaults)
end
return db_settings if db_settings.present?
ApplicationSetting.create_from_defaults || in_memory_application_settings
end
def in_memory_application_settings
......
......@@ -42,25 +42,25 @@ module Gitlab
end
def added?
type == 'new' || type == 'new-nonewline'
%w[new new-nonewline].include?(type)
end
def removed?
type == 'old' || type == 'old-nonewline'
end
def rich_text
@parent_file.highlight_lines! if @parent_file && !@rich_text
@rich_text
%w[old old-nonewline].include?(type)
end
def meta?
type == 'match'
%w[match new-nonewline old-nonewline].include?(type)
end
def discussable?
!['match', 'new-nonewline', 'old-nonewline'].include?(type)
!meta?
end
def rich_text
@parent_file.highlight_lines! if @parent_file && !@rich_text
@rich_text
end
def as_json(opts = nil)
......
......@@ -14,16 +14,7 @@ module Gitlab
lines = []
highlighted_diff_lines = diff_file.highlighted_diff_lines
highlighted_diff_lines.each do |line|
if line.meta? || line.unchanged?
# line in the right panel is the same as in the left one
lines << {
left: line,
right: line
}
free_right_index = nil
i += 1
elsif line.removed?
if line.removed?
lines << {
left: line,
right: nil
......@@ -51,6 +42,15 @@ module Gitlab
free_right_index = nil
i += 1
end
elsif line.meta? || line.unchanged?
# line in the right panel is the same as in the left one
lines << {
left: line,
right: line
}
free_right_index = nil
i += 1
end
end
......
# This class extends an OpenStruct object by adding predicate methods to mimic
# ActiveRecord access. We rely on the initial values being true or false to
# determine whether to define a predicate method because for a newly-added
# column that has not been migrated yet, there is no way to determine the
# column type without parsing db/schema.rb.
module Gitlab
class FakeApplicationSettings < OpenStruct
def initialize(options = {})
super
FakeApplicationSettings.define_predicate_methods(options)
end
# Mimic ActiveRecord predicate methods for boolean values
def self.define_predicate_methods(options)
options.each do |key, value|
next if key.to_s.end_with?('?')
next unless [true, false].include?(value)
define_method "#{key}?" do
actual_key = key.to_s.chomp('?')
self[actual_key]
end
end
end
end
end
......@@ -14,7 +14,7 @@ module Gitlab
# timeout - The maximum amount of seconds to block the caller for. This
# ensures we don't indefinitely block a caller in case a job takes
# long to process, or is never processed.
def wait(timeout = 60)
def wait(timeout = 10)
start = Time.current
while (Time.current - start) <= timeout
......
FactoryGirl.define do
factory :application_setting do
end
end
......@@ -210,6 +210,13 @@ describe 'New/edit issue', :feature, :js do
expect(find('.js-assignee-search')).to have_content(user2.name)
end
it 'description has autocomplete' do
find('#issue_description').native.send_keys('')
fill_in 'issue_description', with: '@'
expect(page).to have_selector('.atwho-view')
end
end
context 'edit issue' do
......@@ -258,6 +265,13 @@ describe 'New/edit issue', :feature, :js do
end
end
end
it 'description has autocomplete' do
find('#issue_description').native.send_keys('')
fill_in 'issue_description', with: '@'
expect(page).to have_selector('.atwho-view')
end
end
describe 'sub-group project' do
......
......@@ -36,7 +36,7 @@ feature 'Merge Request closing issues message', feature: true, js: true do
let(:merge_request_description) { "Description\n\nclosing #{issue_1.to_reference}, #{issue_2.to_reference}" }
it 'does not display closing issue message' do
expect(page).to have_content("Closes issues #{issue_1.to_reference} and #{issue_2.to_reference}")
expect(page).to have_content("Closed issues #{issue_1.to_reference} and #{issue_2.to_reference}")
end
end
......@@ -44,7 +44,7 @@ feature 'Merge Request closing issues message', feature: true, js: true do
let(:merge_request_description) { "Description\n\nRefers to #{issue_1.to_reference} and #{issue_2.to_reference}" }
it 'does not display closing issue message' do
expect(page).to have_content("Issues #{issue_1.to_reference} and #{issue_2.to_reference} are mentioned but will not be closed.")
expect(page).to have_content("Issues #{issue_1.to_reference} and #{issue_2.to_reference} are mentioned but were not closed")
end
end
......@@ -52,8 +52,8 @@ feature 'Merge Request closing issues message', feature: true, js: true do
let(:merge_request_title) { "closes #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" }
it 'does not display closing issue message' do
expect(page).to have_content("Closes issue #{issue_1.to_reference}.")
expect(page).to have_content("Issue #{issue_2.to_reference} is mentioned but will not be closed.")
expect(page).to have_content("Closed issue #{issue_1.to_reference}")
expect(page).to have_content("Issue #{issue_2.to_reference} is mentioned but was not closed")
end
end
......@@ -61,7 +61,7 @@ feature 'Merge Request closing issues message', feature: true, js: true do
let(:merge_request_title) { "closing #{issue_1.to_reference}, #{issue_2.to_reference}" }
it 'does not display closing issue message' do
expect(page).to have_content("Closes issues #{issue_1.to_reference} and #{issue_2.to_reference}")
expect(page).to have_content("Closed issues #{issue_1.to_reference} and #{issue_2.to_reference}")
end
end
......@@ -69,7 +69,7 @@ feature 'Merge Request closing issues message', feature: true, js: true do
let(:merge_request_title) { "Refers to #{issue_1.to_reference} and #{issue_2.to_reference}" }
it 'does not display closing issue message' do
expect(page).to have_content("Issues #{issue_1.to_reference} and #{issue_2.to_reference} are mentioned but will not be closed.")
expect(page).to have_content("Issues #{issue_1.to_reference} and #{issue_2.to_reference} are mentioned but were not closed")
end
end
......@@ -77,8 +77,8 @@ feature 'Merge Request closing issues message', feature: true, js: true do
let(:merge_request_title) { "closes #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" }
it 'does not display closing issue message' do
expect(page).to have_content("Closes issue #{issue_1.to_reference}. Issue #{issue_2.to_reference} is mentioned but will not be closed.")
expect(page).to have_content("Issue #{issue_2.to_reference} is mentioned but will not be closed.")
expect(page).to have_content("Closed issue #{issue_1.to_reference}")
expect(page).to have_content("Issue #{issue_2.to_reference} is mentioned but was not closed")
end
end
end
......@@ -96,6 +96,13 @@ describe 'New/edit merge request', feature: true, js: true do
.to end_with(merge_request_path(merge_request))
end
end
it 'description has autocomplete' do
find('#merge_request_description').native.send_keys('')
fill_in 'merge_request_description', with: '@'
expect(page).to have_selector('.atwho-view')
end
end
context 'edit merge request' do
......@@ -157,6 +164,13 @@ describe 'New/edit merge request', feature: true, js: true do
end
end
end
it 'description has autocomplete' do
find('#merge_request_description').native.send_keys('')
fill_in 'merge_request_description', with: '@'
expect(page).to have_selector('.atwho-view')
end
end
end
......
require 'spec_helper'
feature 'Creating a new project milestone', :feature, :js do
let(:user) { create(:user) }
let(:project) { create(:empty_project, name: 'test', namespace: user.namespace) }
before do
login_as(user)
visit new_namespace_project_milestone_path(project.namespace, project)
end
it 'description has autocomplete' do
find('#milestone_description').native.send_keys('')
fill_in 'milestone_description', with: '@'
expect(page).to have_selector('.atwho-view')
end
end
require 'spec_helper'
describe 'Project snippets', feature: true do
describe 'Project snippets', :js, feature: true do
context 'when the project has snippets' do
let(:project) { create(:empty_project, :public) }
let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) }
......@@ -26,5 +26,19 @@ describe 'Project snippets', feature: true do
expect(page).to have_content(snippets[1].title)
end
end
context 'when submitting a note' do
before do
login_as :admin
visit namespace_project_snippet_path(project.namespace, project, snippets[0])
end
it 'should have autocomplete' do
find('#note_note').native.send_keys('')
fill_in 'note[note]', with: '@'
expect(page).to have_selector('.atwho-view')
end
end
end
end
......@@ -133,6 +133,22 @@ feature 'Projects > Wiki > User creates wiki page', js: true, feature: true do
expect(page).to have_content('My awesome wiki!')
end
end
scenario 'content has autocomplete', :js do
click_link 'New page'
page.within '#modal-new-wiki' do
fill_in :new_wiki_path, with: 'test-autocomplete'
click_button 'Create page'
end
page.within '.wiki-form' do
find('#wiki_content').native.send_keys('')
fill_in :wiki_content, with: '@'
end
expect(page).to have_selector('.atwho-view')
end
end
end
......
......@@ -5,11 +5,10 @@ feature 'Projects > Wiki > User updates wiki page', feature: true do
background do
project.team << [user, :master]
WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute
login_as(user)
visit namespace_project_path(project.namespace, project)
WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute
click_link 'Wiki'
visit namespace_project_wikis_path(project.namespace, project)
end
context 'in the user namespace' do
......@@ -42,6 +41,15 @@ feature 'Projects > Wiki > User updates wiki page', feature: true do
expect(page).to have_content('Content can\'t be blank')
expect(find('textarea#wiki_content').value).to eq ''
end
scenario 'content has autocomplete', :js do
click_link 'Edit'
find('#wiki_content').native.send_keys('')
fill_in :wiki_content, with: '@'
expect(page).to have_selector('.atwho-view')
end
end
end
......
......@@ -70,6 +70,22 @@ describe 'Comments on personal snippets', :js, feature: true do
expect(find('div#notes')).to have_content('This is awesome!')
end
it 'should not have autocomplete' do
wait_for_requests
request_count_before = page.driver.network_traffic.count
find('#note_note').native.send_keys('')
fill_in 'note[note]', with: '@'
wait_for_requests
request_count_after = page.driver.network_traffic.count
# This selector probably won't be in place even if autocomplete was enabled
# but we want to make sure
expect(page).not_to have_selector('.atwho-view')
expect(request_count_before).to eq(request_count_after)
end
end
context 'when editing a note' do
......
......@@ -7,61 +7,79 @@ feature 'Master creates tag', feature: true do
before do
project.team << [user, :master]
login_with(user)
visit namespace_project_tags_path(project.namespace, project)
end
scenario 'with an invalid name displays an error' do
create_tag_in_form(tag: 'v 1.0', ref: 'master')
context 'from tag list' do
before do
visit namespace_project_tags_path(project.namespace, project)
end
expect(page).to have_content 'Tag name invalid'
end
scenario 'with an invalid name displays an error' do
create_tag_in_form(tag: 'v 1.0', ref: 'master')
scenario 'with an invalid reference displays an error' do
create_tag_in_form(tag: 'v2.0', ref: 'foo')
expect(page).to have_content 'Tag name invalid'
end
expect(page).to have_content 'Target foo is invalid'
end
scenario 'with an invalid reference displays an error' do
create_tag_in_form(tag: 'v2.0', ref: 'foo')
scenario 'that already exists displays an error' do
create_tag_in_form(tag: 'v1.1.0', ref: 'master')
expect(page).to have_content 'Target foo is invalid'
end
expect(page).to have_content 'Tag v1.1.0 already exists'
end
scenario 'that already exists displays an error' do
create_tag_in_form(tag: 'v1.1.0', ref: 'master')
expect(page).to have_content 'Tag v1.1.0 already exists'
end
scenario 'with multiline message displays the message in a <pre> block' do
create_tag_in_form(tag: 'v3.0', ref: 'master', message: "Awesome tag message\n\n- hello\n- world")
scenario 'with multiline message displays the message in a <pre> block' do
create_tag_in_form(tag: 'v3.0', ref: 'master', message: "Awesome tag message\n\n- hello\n- world")
expect(current_path).to eq(
namespace_project_tag_path(project.namespace, project, 'v3.0'))
expect(page).to have_content 'v3.0'
page.within 'pre.wrap' do
expect(page).to have_content "Awesome tag message\n\n- hello\n- world"
expect(current_path).to eq(
namespace_project_tag_path(project.namespace, project, 'v3.0'))
expect(page).to have_content 'v3.0'
page.within 'pre.wrap' do
expect(page).to have_content "Awesome tag message\n\n- hello\n- world"
end
end
end
scenario 'with multiline release notes parses the release note as Markdown' do
create_tag_in_form(tag: 'v4.0', ref: 'master', desc: "Awesome release notes\n\n- hello\n- world")
scenario 'with multiline release notes parses the release note as Markdown' do
create_tag_in_form(tag: 'v4.0', ref: 'master', desc: "Awesome release notes\n\n- hello\n- world")
expect(current_path).to eq(
namespace_project_tag_path(project.namespace, project, 'v4.0'))
expect(page).to have_content 'v4.0'
page.within '.description' do
expect(page).to have_content 'Awesome release notes'
expect(page).to have_selector('ul li', count: 2)
expect(current_path).to eq(
namespace_project_tag_path(project.namespace, project, 'v4.0'))
expect(page).to have_content 'v4.0'
page.within '.description' do
expect(page).to have_content 'Awesome release notes'
expect(page).to have_selector('ul li', count: 2)
end
end
scenario 'opens dropdown for ref', js: true do
click_link 'New tag'
ref_row = find('.form-group:nth-of-type(2) .col-sm-10')
page.within ref_row do
ref_input = find('[name="ref"]', visible: false)
expect(ref_input.value).to eq 'master'
expect(find('.dropdown-toggle-text')).to have_content 'master'
find('.js-branch-select').trigger('click')
expect(find('.dropdown-menu')).to have_content 'empty-branch'
end
end
end
scenario 'opens dropdown for ref', js: true do
click_link 'New tag'
ref_row = find('.form-group:nth-of-type(2) .col-sm-10')
page.within ref_row do
ref_input = find('[name="ref"]', visible: false)
expect(ref_input.value).to eq 'master'
expect(find('.dropdown-toggle-text')).to have_content 'master'
context 'from new tag page' do
before do
visit new_namespace_project_tag_path(project.namespace, project)
end
find('.js-branch-select').trigger('click')
it 'description has autocomplete', :js do
find('#release_description').native.send_keys('')
fill_in 'release_description', with: '@'
expect(find('.dropdown-menu')).to have_content 'empty-branch'
expect(page).to have_selector('.atwho-view')
end
end
......
......@@ -24,6 +24,17 @@ feature 'Master updates tag', feature: true do
expect(page).to have_content 'v1.1.0'
expect(page).to have_content 'Awesome release notes'
end
scenario 'description has autocomplete', :js do
page.within(first('.content-list .controls')) do
click_link 'Edit release notes'
end
find('#release_description').native.send_keys('')
fill_in 'release_description', with: '@'
expect(page).to have_selector('.atwho-view')
end
end
context 'from a specific tag page' do
......
......@@ -148,12 +148,21 @@ describe DiffHelper do
it 'puts comments on added lines' do
left = Gitlab::Diff::Line.new('\\nonewline', 'old-nonewline', 3, 3, 3)
right = Gitlab::Diff::Line.new('new line', 'add', 3, 3, 3)
right = Gitlab::Diff::Line.new('new line', 'new', 3, 3, 3)
result = helper.parallel_diff_discussions(left, right, diff_file)
expect(result).to eq([nil, 'comment'])
end
it 'puts comments on unchanged lines' do
left = Gitlab::Diff::Line.new('unchanged line', nil, 3, 3, 3)
right = Gitlab::Diff::Line.new('unchanged line', nil, 3, 3, 3)
result = helper.parallel_diff_discussions(left, right, diff_file)
expect(result).to eq(['comment', nil])
end
end
describe "#diff_match_line" do
......
......@@ -95,5 +95,33 @@ describe('Description component', () => {
done();
});
});
it('clears task status text when no tasks are present', (done) => {
vm.taskStatus = '0 of 0';
setTimeout(() => {
expect(
document.querySelector('.issuable-meta #task_status').textContent.trim(),
).toBe('');
done();
});
});
});
it('applies syntax highlighting and math when description changed', (done) => {
spyOn(vm, 'renderGFM').and.callThrough();
spyOn($.prototype, 'renderGFM').and.callThrough();
vm.descriptionHtml = 'changed';
Vue.nextTick(() => {
setTimeout(() => {
expect(vm.$refs['gfm-content']).toBeDefined();
expect(vm.renderGFM).toHaveBeenCalled();
expect($.prototype.renderGFM).toHaveBeenCalled();
done();
});
});
});
});
......@@ -176,7 +176,7 @@ import '~/notes';
Notes.updateNoteTargetSelector($note);
expect($note.toggleClass).toHaveBeenCalledWith('target', null);
expect($note.toggleClass).toHaveBeenCalledWith('target', false);
});
});
......
import Vue from 'vue';
import relatedLinksComponent from '~/vue_merge_request_widget/components/mr_widget_related_links';
import MRWidgetRelatedLinks from '~/vue_merge_request_widget/components/mr_widget_related_links';
const createComponent = (data) => {
const Component = Vue.extend(relatedLinksComponent);
describe('MRWidgetRelatedLinks', () => {
let vm;
beforeEach(() => {
const Component = Vue.extend(MRWidgetRelatedLinks);
vm = new Component({
el: document.createElement('div'),
propsData: {
isMerged: false,
relatedLinks: {},
},
});
});
return new Component({
el: document.createElement('div'),
propsData: data,
afterEach(() => {
vm.$destroy();
});
};
describe('MRWidgetRelatedLinks', () => {
describe('props', () => {
it('should have props', () => {
const { relatedLinks } = relatedLinksComponent.props;
const { isMerged, relatedLinks } = MRWidgetRelatedLinks.props;
expect(isMerged).toBeDefined();
expect(isMerged.type).toBe(Boolean);
expect(isMerged.required).toBeTruthy();
expect(relatedLinks).toBeDefined();
expect(relatedLinks.type instanceof Object).toBeTruthy();
expect(relatedLinks.required).toBeTruthy();
......@@ -22,16 +33,38 @@ describe('MRWidgetRelatedLinks', () => {
});
describe('computed', () => {
describe('closingText', () => {
const dummyIssueLabel = 'dummy label';
beforeEach(() => {
spyOn(vm, 'issueLabel').and.returnValue(dummyIssueLabel);
});
it('outputs text for closing issues', () => {
vm.isMerged = false;
const text = vm.closingText;
expect(text).toBe(`Closes ${dummyIssueLabel}`);
});
it('outputs text for closed issues', () => {
vm.isMerged = true;
const text = vm.closingText;
expect(text).toBe(`Closed ${dummyIssueLabel}`);
});
});
describe('hasLinks', () => {
it('should return correct value when we have links reference', () => {
const data = {
relatedLinks: {
closing: '/foo',
mentioned: '/foo',
assignToMe: '/foo',
},
vm.relatedLinks = {
closing: '/foo',
mentioned: '/foo',
assignToMe: '/foo',
};
const vm = createComponent(data);
expect(vm.hasLinks).toBeTruthy();
vm.relatedLinks.closing = null;
......@@ -44,95 +77,160 @@ describe('MRWidgetRelatedLinks', () => {
expect(vm.hasLinks).toBeFalsy();
});
});
});
describe('methods', () => {
const data = {
relatedLinks: {
closing: '<a href="#">#23</a> and <a>#42</a>',
mentioned: '<a href="#">#7</a>',
},
};
const vm = createComponent(data);
describe('mentionedText', () => {
it('outputs text for one mentioned issue before merging', () => {
vm.isMerged = false;
spyOn(vm, 'hasMultipleIssues').and.returnValue(false);
describe('hasMultipleIssues', () => {
it('should return true if the given text has multiple issues', () => {
expect(vm.hasMultipleIssues(data.relatedLinks.closing)).toBeTruthy();
const text = vm.mentionedText;
expect(text).toBe('is mentioned but will not be closed');
});
it('should return false if the given text has one issue', () => {
expect(vm.hasMultipleIssues(data.relatedLinks.mentioned)).toBeFalsy();
it('outputs text for one mentioned issue after merging', () => {
vm.isMerged = true;
spyOn(vm, 'hasMultipleIssues').and.returnValue(false);
const text = vm.mentionedText;
expect(text).toBe('is mentioned but was not closed');
});
it('outputs text for multiple mentioned issue before merging', () => {
vm.isMerged = false;
spyOn(vm, 'hasMultipleIssues').and.returnValue(true);
const text = vm.mentionedText;
expect(text).toBe('are mentioned but will not be closed');
});
it('outputs text for multiple mentioned issue after merging', () => {
vm.isMerged = true;
spyOn(vm, 'hasMultipleIssues').and.returnValue(true);
const text = vm.mentionedText;
expect(text).toBe('are mentioned but were not closed');
});
});
});
describe('issueLabel', () => {
describe('methods', () => {
const relatedLinks = {
oneIssue: '<a href="#">#7</a>',
twoIssues: '<a href="#">#23</a> and <a>#42</a>',
threeIssues: '<a href="#">#1</a>, <a>#2</a>, and <a>#3</a>',
};
beforeEach(() => {
vm.relatedLinks = relatedLinks;
});
describe('hasMultipleIssues', () => {
it('should return true if the given text has multiple issues', () => {
expect(vm.issueLabel('closing')).toEqual('issues');
expect(vm.hasMultipleIssues(relatedLinks.twoIssues)).toBeTruthy();
expect(vm.hasMultipleIssues(relatedLinks.threeIssues)).toBeTruthy();
});
it('should return false if the given text has one issue', () => {
expect(vm.issueLabel('mentioned')).toEqual('issue');
expect(vm.hasMultipleIssues(relatedLinks.oneIssue)).toBeFalsy();
});
});
describe('verbLabel', () => {
describe('issueLabel', () => {
it('should return true if the given text has multiple issues', () => {
expect(vm.verbLabel('closing')).toEqual('are');
expect(vm.issueLabel('twoIssues')).toEqual('issues');
expect(vm.issueLabel('threeIssues')).toEqual('issues');
});
it('should return false if the given text has one issue', () => {
expect(vm.verbLabel('mentioned')).toEqual('is');
expect(vm.issueLabel('oneIssue')).toEqual('issue');
});
});
});
describe('template', () => {
it('should have only have closing issues text', () => {
const vm = createComponent({
relatedLinks: {
closing: '<a href="#">#23</a> and <a>#42</a>',
},
});
const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim();
expect(content).toContain('Closes issues #23 and #42');
expect(content).not.toContain('mentioned');
});
it('should have only have closing issues text', (done) => {
vm.relatedLinks = {
closing: '<a href="#">#23</a> and <a>#42</a>',
};
it('should have only have mentioned issues text', () => {
const vm = createComponent({
relatedLinks: {
mentioned: '<a href="#">#7</a>',
},
});
Vue.nextTick()
.then(() => {
const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim();
expect(vm.$el.innerText).toContain('issue #7');
expect(vm.$el.innerText).toContain('is mentioned but will not be closed.');
expect(vm.$el.innerText).not.toContain('Closes');
expect(content).toContain('Closes issues #23 and #42');
expect(content).not.toContain('mentioned');
})
.then(done)
.catch(done.fail);
});
it('should have closing and mentioned issues at the same time', () => {
const vm = createComponent({
relatedLinks: {
closing: '<a href="#">#7</a>',
mentioned: '<a href="#">#23</a> and <a>#42</a>',
},
});
const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim();
it('should have only have mentioned issues text', (done) => {
vm.relatedLinks = {
mentioned: '<a href="#">#7</a>',
};
Vue.nextTick()
.then(() => {
expect(vm.$el.innerText).toContain('issue #7');
expect(vm.$el.innerText).toContain('is mentioned but will not be closed');
expect(vm.$el.innerText).not.toContain('Closes');
})
.then(done)
.catch(done.fail);
});
expect(content).toContain('Closes issue #7.');
expect(content).toContain('issues #23 and #42');
expect(content).toContain('are mentioned but will not be closed.');
it('should have closing and mentioned issues at the same time', (done) => {
vm.relatedLinks = {
closing: '<a href="#">#7</a>',
mentioned: '<a href="#">#23</a> and <a>#42</a>',
};
Vue.nextTick()
.then(() => {
const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim();
expect(content).toContain('Closes issue #7.');
expect(content).toContain('issues #23 and #42');
expect(content).toContain('are mentioned but will not be closed');
})
.then(done)
.catch(done.fail);
});
it('should have assing issues link', () => {
const vm = createComponent({
relatedLinks: {
assignToMe: '<a href="#">Assign yourself to these issues</a>',
},
});
it('should have assing issues link', (done) => {
vm.relatedLinks = {
assignToMe: '<a href="#">Assign yourself to these issues</a>',
};
Vue.nextTick()
.then(() => {
expect(vm.$el.innerText).toContain('Assign yourself to these issues');
})
.then(done)
.catch(done.fail);
});
expect(vm.$el.innerText).toContain('Assign yourself to these issues');
it('should use different wording after merging', (done) => {
vm.isMerged = true;
vm.relatedLinks = {
closing: '<a href="#">#7</a>',
mentioned: '<a href="#">#23</a> and <a>#42</a>',
};
Vue.nextTick()
.then(() => {
const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim();
expect(content).toContain('Closed issue #7.');
expect(content).toContain('issues #23 and #42');
expect(content).toContain('are mentioned but were not closed');
})
.then(done)
.catch(done.fail);
});
});
});
......@@ -48,12 +48,13 @@ describe('mrWidgetOptions', () => {
});
describe('shouldRenderMergeHelp', () => {
it('should return false for the initial merged state', () => {
it('should return false after merging', () => {
vm.mr.isMerged = true;
expect(vm.shouldRenderMergeHelp).toBeFalsy();
});
it('should return true for a state which requires help widget', () => {
vm.mr.state = 'conflicts';
it('should return true before merging', () => {
vm.mr.isMerged = false;
expect(vm.shouldRenderMergeHelp).toBeTruthy();
});
});
......
......@@ -18,5 +18,17 @@ describe('MergeRequestStore', () => {
store.setData({ ...mockData, work_in_progress: !mockData.work_in_progress });
expect(store.hasSHAChanged).toBe(false);
});
it('sets isMerged to true for merged state', () => {
store.setData({ ...mockData, state: 'merged' });
expect(store.isMerged).toBe(true);
});
it('sets isMerged to false for readyToMerge state', () => {
store.setData({ ...mockData, state: 'readyToMerge' });
expect(store.isMerged).toBe(false);
});
});
});
......@@ -32,6 +32,37 @@ describe Gitlab::CurrentSettings do
expect(current_application_settings).to be_a(ApplicationSetting)
end
context 'with migrations pending' do
before do
expect(ActiveRecord::Migrator).to receive(:needs_migration?).and_return(true)
end
it 'returns an in-memory ApplicationSetting object' do
settings = current_application_settings
expect(settings).to be_a(OpenStruct)
expect(settings.sign_in_enabled?).to eq(settings.sign_in_enabled)
expect(settings.sign_up_enabled?).to eq(settings.sign_up_enabled)
end
it 'uses the existing database settings and falls back to defaults' do
db_settings = create(:application_setting,
home_page_url: 'http://mydomain.com',
signup_enabled: false)
settings = current_application_settings
app_defaults = ApplicationSetting.last
expect(settings).to be_a(OpenStruct)
expect(settings.home_page_url).to eq(db_settings.home_page_url)
expect(settings.signup_enabled?).to be_falsey
expect(settings.signup_enabled).to be_falsey
# Check that unspecified values use the defaults
settings.reject! { |key, _| [:home_page_url, :signup_enabled].include? key }
settings.each { |key, _| expect(settings[key]).to eq(app_defaults[key]) }
end
end
end
context 'with DB unavailable' do
......
require 'spec_helper'
describe Gitlab::FakeApplicationSettings do
let(:defaults) { { signin_enabled: false, foobar: 'asdf', signup_enabled: true, 'test?' => 123 } }
subject { described_class.new(defaults) }
it 'wraps OpenStruct variables properly' do
expect(subject.signin_enabled).to be_falsey
expect(subject.signup_enabled).to be_truthy
expect(subject.foobar).to eq('asdf')
end
it 'defines predicate methods' do
expect(subject.signin_enabled?).to be_falsey
expect(subject.signup_enabled?).to be_truthy
end
it 'predicate method changes when value is updated' do
subject.signin_enabled = true
expect(subject.signin_enabled?).to be_truthy
end
it 'does not define a predicate method' do
expect(subject.foobar?).to be_nil
end
it 'does not override an existing predicate method' do
expect(subject.test?).to eq(123)
end
end
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