Merge branch 'feature/rails3' of github.com:finnlabs/chiliproject into feature/rails3

pull/1186/head
Philipp Tessenow 12 years ago
commit 9e389a1c0f
  1. 4
      Gemfile
  2. 42
      Guardfile
  3. 7
      app/helpers/application_helper.rb
  4. 2
      app/models/setting.rb
  5. 2
      app/views/settings/_display.html.erb
  6. 32
      features/custom_fields/create_bool.feature
  7. 17
      features/custom_fields/create_date.feature
  8. 18
      features/custom_fields/create_float.feature
  9. 17
      features/custom_fields/create_int.feature
  10. 21
      features/custom_fields/create_list.feature
  11. 32
      features/custom_fields/create_text.feature
  12. 46
      features/custom_fields/edit_bool.feature
  13. 67
      features/custom_fields/edit_bool_delete_localizations.feature
  14. 55
      features/custom_fields/edit_text.feature
  15. 156
      features/groups/group_memberships.feature
  16. 70
      features/groups/membership.feature
  17. 48
      features/issues/copy_with_watchers.feature
  18. 45
      features/issues/export.feature
  19. 30
      features/issues/index_pagination.feature
  20. 49
      features/issues/issue.feature
  21. 172
      features/issues/list_sums.feature
  22. 13
      features/projects/create.feature
  23. 22
      features/step_definitions/_then_steps.rb
  24. 19
      features/step_definitions/_when_steps.rb
  25. 7
      features/step_definitions/breadcrumb_steps.rb
  26. 46
      features/step_definitions/common_steps.rb
  27. 35
      features/step_definitions/custom_field_steps.rb
  28. 70
      features/step_definitions/custom_web_steps.rb
  29. 19
      features/step_definitions/drop_down_steps.rb
  30. 559
      features/step_definitions/general_steps.rb
  31. 54
      features/step_definitions/group_steps.rb
  32. 134
      features/step_definitions/i18n_steps.rb
  33. 18
      features/step_definitions/image_steps.rb
  34. 67
      features/step_definitions/issue_list_sums_steps.rb
  35. 9
      features/step_definitions/issue_steps.rb
  36. 10
      features/step_definitions/link_steps.rb
  37. 11
      features/step_definitions/menu_steps.rb
  38. 13
      features/step_definitions/principal_steps.rb
  39. 78
      features/step_definitions/project_member_steps.rb
  40. 30
      features/step_definitions/settings_steps.rb
  41. 11
      features/step_definitions/user_steps.rb
  42. 384
      features/step_definitions/web_steps.rb
  43. 32
      features/step_definitions/wiki_steps.rb
  44. 2
      features/support/object_daddy.rb
  45. 240
      features/support/paths.rb
  46. 54
      features/users/deleting.feature
  47. 32
      features/wiki/breadcrumb.feature
  48. 41
      features/wiki/wiki_index.feature
  49. 38
      features/wiki/wiki_new_child.feature
  50. 90
      features/wiki_menu_items/wiki_menu_items.feature
  51. 11
      lib/instance_finder.rb
  52. 71
      lib/redmine/themes.rb
  53. 42
      lib/redmine/themes/theme.rb
  54. 25
      lib/scenario_disabler.rb
  55. 82
      spec/controllers/custom_fields_controller_spec.rb
  56. 94
      spec/controllers/projects_controller_spec.rb
  57. 187
      spec/controllers/users_controller_spec.rb
  58. 563
      spec/controllers/wiki_controller_spec.rb
  59. 42
      spec/controllers/wiki_menu_authentication_spec.rb
  60. 7
      spec/factories/attachment_factory.rb
  61. 8
      spec/factories/board_factory.rb
  62. 8
      spec/factories/changeset_factory.rb
  63. 6
      spec/factories/document_category_factory.rb
  64. 8
      spec/factories/document_factory.rb
  65. 6
      spec/factories/group_factory.rb
  66. 8
      spec/factories/issue_category_factory.rb
  67. 16
      spec/factories/issue_factory.rb
  68. 27
      spec/factories/issue_priority_factory.rb
  69. 11
      spec/factories/issue_status_factory.rb
  70. 18
      spec/factories/member_factory.rb
  71. 7
      spec/factories/message_factory.rb
  72. 9
      spec/factories/news_factory.rb
  73. 26
      spec/factories/project_factory.rb
  74. 17
      spec/factories/query_factory.rb
  75. 7
      spec/factories/repository_factory.rb
  76. 20
      spec/factories/role_factory.rb
  77. 7
      spec/factories/time_entry_activity_factory.rb
  78. 9
      spec/factories/time_entry_factory.rb
  79. 35
      spec/factories/tracker_factory.rb
  80. 63
      spec/factories/user_custom_value_factory.rb
  81. 38
      spec/factories/user_factory.rb
  82. 7
      spec/factories/version_factory.rb
  83. 9
      spec/factories/wiki_content_factory.rb
  84. 7
      spec/factories/wiki_factory.rb
  85. 8
      spec/factories/wiki_menu_item_factory.rb
  86. 10
      spec/factories/wiki_page_factory.rb
  87. 8
      spec/factories/wiki_redirect_factory.rb
  88. 11
      spec/factories/workflow_factory.rb
  89. 92
      spec/lib/acts_as_journalized/journaled_spec.rb
  90. 107
      spec/lib/redmine/themes/theme_spec.rb
  91. 152
      spec/lib/redmine/themes_spec.rb
  92. 304
      spec/models/custom_field_spec.rb
  93. 82
      spec/models/deleted_user_spec.rb
  94. 83
      spec/models/issue_spec.rb
  95. 12
      spec/models/role_spec.rb
  96. 418
      spec/models/user_deletion_spec.rb
  97. 69
      spec/models/user_spec.rb
  98. 72
      spec/models/wiki_menu_item_spec.rb
  99. 39
      spec/support/identical_ext.rb
  100. 48
      spec/support/matchers/have_exactly_one_selected_menu_item_in.rb
  101. Some files were not shown because too many files have changed in this diff Show More

@ -8,7 +8,7 @@ gem "rdoc", ">= 2.4.2"
# Needed only on RUBY_VERSION = 1.8, ruby 1.9+ compatible interpreters should bring their csv
gem "fastercsv", "~> 1.5.0", :platforms => [:ruby_18, :jruby, :mingw_18]
# master includes the uniqueness validator, formerly patched in config/initializers/globalize3_patch.rb
gem 'globalize3', :git => 'git://github.com/svenfuchs/globalize3.git'
gem 'globalize3', :github => 'svenfuchs/globalize3'
gem "delayed_job_active_record" # that's how delayed job's readme recommends it
# TODO: adds #auto_link which was deprecated in rails 3.1
@ -40,7 +40,7 @@ gem 'jquery-rails'
group :test do
gem 'shoulda', '~> 3.1.1'
gem 'object-daddy', :git => 'git://github.com/awebneck/object_daddy.git'
gem 'object-daddy', :github => 'awebneck/object_daddy'
gem 'mocha'
gem "launchy", "~> 2.1.0"
gem "factory_girl_rails", "~> 4.0"

@ -12,27 +12,27 @@
# watch(%r{features/support/}) { :cucumber }
# end
# guard :rspec, :cli => "--drb" do
# watch(%r{^spec/.+_spec\.rb$})
# watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
# watch('spec/spec_helper.rb') { "spec" }
#
# # Rails example
# watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
# watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
# watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] }
# watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
# watch('config/routes.rb') { "spec/routing" }
# watch('app/controllers/application_controller.rb') { "spec/controllers" }
#
# # Capybara request specs
# watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" }
#
# # Turnip features and steps
# watch(%r{^spec/acceptance/(.+)\.feature$})
# watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' }
# end
#
guard :rspec do # , :cli => "--drb" do
watch(%r{^spec/.+_spec\.rb$})
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
watch('spec/spec_helper.rb') { "spec" }
# Rails example
watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] }
watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
watch('config/routes.rb') { "spec/routing" }
watch('app/controllers/application_controller.rb') { "spec/controllers" }
# Capybara request specs
watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" }
# Turnip features and steps
watch(%r{^spec/acceptance/(.+)\.feature$})
watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' }
end
# guard :cucumber do # , :cli => "--drb" do
# watch(%r{^features/.+\.feature$})
# watch(%r{^features/support/.+$}) { 'features' }

@ -512,10 +512,9 @@ module ApplicationHelper
# Returns the theme, controller name, and action as css classes for the
# HTML body.
def body_css_classes
css = []
if theme = Redmine::Themes.theme(Setting.ui_theme)
css << 'theme-' + theme.name.to_s
end
theme = Redmine::Themes.theme(Setting.ui_theme)
css = ['theme-' + theme.name.to_s]
if params[:controller] && params[:action]
css << 'controller-' + params[:controller]

@ -107,7 +107,7 @@ class Setting < ActiveRecord::Base
end
validates_uniqueness_of :name
validates_inclusion_of :name, :in => @@available_settings.keys
validates_inclusion_of :name, :in => lambda { |setting| @@available_settings.keys } # lambda, because @available_settings changes at runtime
validates_numericality_of :value, :only_integer => true, :if => Proc.new { |setting| @@available_settings[setting.name]['format'] == 'int' }
def value

@ -1,7 +1,7 @@
<%= form_tag({:action => 'edit', :tab => 'display'}) do %>
<div class="box tabular settings">
<p><%= setting_select :ui_theme, Redmine::Themes.all.map { |theme| [theme.name, theme.name] }, :label => :label_theme %></p>
<p><%= setting_select :ui_theme, Redmine::Themes.collect { |theme| [theme.name, theme.name] }, :label => :label_theme %></p>
<p id="setting_available_languages"><%= setting_multiselect :available_languages, all_lang_options_for_select(false) %></p>

@ -0,0 +1,32 @@
Feature: Localized boolean custom fields can be created
Background:
Given I am admin
And the following languages are active:
| en |
| de |
When I go to the custom fields page
When I follow "New custom field"
@javascript
Scenario: Available fields
When I select "Boolean" from "custom_field_field_format"
Then there should be the following localizations:
| locale | name | default_value | possible_values |
| en | | 0 | |
And there should be a "custom_field_tracker_ids_1" field visible
And I should see "Bug"
And there should be a "custom_field_tracker_ids_2" field visible
And I should see "Feature"
And there should be a "custom_field_tracker_ids_3" field visible
And I should see "Support"
And there should be a "custom_field_is_required" field visible
And there should be a "custom_field_is_for_all" field visible
And there should be a "custom_field_is_filter" field visible
And there should be a "custom_field_searchable" field invisible
@javascript
Scenario: Creating a boolean custom field
And I add the english localization of the "name" attribute as "New Field"
And I select "Boolean" from "custom_field_field_format"
Then I should not see "Possible values"

@ -0,0 +1,17 @@
Feature: Date custom fields can be created
Background:
Given I am admin
And the following languages are active:
| en |
| de |
When I go to the custom fields page
When I follow "New custom field"
@javascript
Scenario: Creating a date custom field
When I select "Date" from "custom_field_field_format"
And I add the english localization of the "name" attribute as "New Field"
And I add the english localization of the "default_value" attribute as "2012-01-01"
And I press "Save"
Then I should be on the custom fields page

@ -0,0 +1,18 @@
Feature: Localized decimal custom fields can be created
Background:
Given I am admin
And the following languages are active:
| en |
| de |
When I go to the custom fields page
When I follow "New custom field"
@javascript
Scenario: Creating a decimal custom field
When I select "Float" from "custom_field_field_format"
And I add the english localization of the "name" attribute as "New Field"
And I add the english localization of the "default_value" attribute as "20.34"
And I press "Save"
Then I should be on the custom fields page

@ -0,0 +1,17 @@
Feature: Int custom fields can be created
Background:
Given I am admin
And the following languages are active:
| en |
| de |
When I go to the custom fields page
When I follow "New custom field"
@javascript
Scenario: Creating a date custom field
When I select "Integer" from "custom_field_field_format"
And I add the english localization of the "name" attribute as "New Field"
And I add the english localization of the "default_value" attribute as "342"
And I press "Save"
Then I should be on the custom fields page

@ -0,0 +1,21 @@
Feature: Localized list custom fields can be created
Background:
Given I am admin
And the following languages are active:
| en |
| de |
When I go to the custom fields page
When I follow "New custom field"
@javascript
Scenario: Creating a list custom field
When I select "List" from "custom_field_field_format"
And I add the english localization of the "name" attribute as "New Field"
And I add the english localization of the "possible_values" attribute as "one\ntwo\nthree\n"
And I add the english localization of the "default_value" attribute as "two"
And I press "Save"
And I follow "New Field"
Then there should be the following localizations:
| locale | name | possible_values | default_value |
| en | New Field | one\ntwo\nthree | two |

@ -0,0 +1,32 @@
Feature: Text custom fields can be created
Background:
Given I am admin
And the following languages are active:
| en |
| de |
| fr |
When I go to the custom fields page
When I follow "New custom field"
@javascript
Scenario: Creating a text custom field with multiple name and default_value localizations
When I select "Text" from "custom_field_field_format"
And I add the english localization of the "name" attribute as "New Field"
And I add the german localization of the "name" attribute as "Neues Feld"
And I add the french localization of the "name" attribute as "Lorem ipsum"
And I add the english localization of the "default_value" attribute as "default"
And I add the german localization of the "default_value" attribute as "Standard"
And I add the french localization of the "default_value" attribute as "Lorem"
And I press "Save"
And I follow "New Field"
Then there should be the following localizations:
| locale | name | default_value |
| en | New Field | default |
| de | Neues Feld | Standard |
| fr | Lorem ipsum | Lorem |
Scenario: Creating a custom field with one name
And I add the english localization of the "name" attribute as "Issue Field"
And I press "Save"
Then I should be on the custom fields page

@ -0,0 +1,46 @@
Feature: Editing a bool custom field
Background:
Given I am already logged in as "admin"
And the following languages are active:
| en |
| de |
| fr |
And the following issue custom fields are defined:
| name | type |
| IssueCustomField | bool |
When I go to the custom fields page
@javascript
Scenario: Adding a localized name
When I follow "IssueCustomField"
And I add the english localization of the "name" attribute as "Issue Field"
And I add the german localization of the "name" attribute as "Ticket Feld"
And I add the french localization of the "name" attribute as "Lorem"
And I press "Save"
Then I should be on the custom fields page
When I follow "Issue Field"
Then there should be the following localizations:
| locale | name | default_value |
| en | Issue Field | 0 |
| de | Ticket Feld | nil |
| fr | Lorem | nil |
And I should not see "Add" within "#custom_field_name_attributes"
Scenario: Entering a long name displays an error
When I follow "IssueCustomField"
And I fill in "custom_field_translations_attributes_0_name" with "Long name which forces an error"
And I press "Save"
Then the "custom_field_translations_attributes_0_name" field should contain "Long name which forces an error"
And I should see "Name is too long" within "#errorExplanation"
Scenario: Entering an already taken name displays an error
Given the following issue custom fields are defined:
| name | type |
| Taken name | bool |
When I follow "IssueCustomField"
And I add the english localization of the "name" attribute as "Taken name"
And I press "Save"
Then I should see "Name has already been taken" within "#errorExplanation"
And the "custom_field_translations_attributes_0_name" field should contain "Taken name"

@ -0,0 +1,67 @@
Feature: Name localizations of bool custom fields can be deleted
Background:
Given I am already logged in as "admin"
And the following languages are active:
| en |
| de |
| fr |
And the following issue custom fields are defined:
| name | type |
| My Custom Field | bool |
And the Custom Field called "My Custom Field" has the following localizations:
| locale | name |
| en | My Custom Field |
| de | Mein Benutzerdefiniertes Feld |
When I go to the custom fields page
@javascript
Scenario: Deleting a localized name
When I follow "My Custom Field"
And I delete the german localization of the "name" attribute
And I press "Save"
And I follow "My Custom Field"
Then there should be the following localizations:
| locale | name | default_value |
| en | My Custom Field | 0 |
@javascript
Scenario: Deleting a name localization and adding another of same locale in same action
When I follow "My Custom Field"
And I delete the german localization of the "name" attribute
And I add the german localization of the "name" attribute as "Neuer Name"
And I press "Save"
And I follow "My Custom Field"
Then there should be the following localizations:
| locale | name | default_value |
| en | My Custom Field | 0 |
| de | Neuer Name | nil |
@javascript
Scenario: Deleting a name localization frees the locale to be used by other translation field
When I follow "My Custom Field"
And I delete the english localization of the "name" attribute
And I change the german localization of the "name" attribute to be english
And I press "Save"
And I follow "Mein Benutzerdefiniertes Feld"
Then there should be the following localizations:
| locale | name | default_value |
| en | Mein Benutzerdefiniertes Feld | 0 |
@javascript
Scenario: Deleting a newly added localization
When I follow "My Custom Field"
And I add the french localization of the "name" attribute as "To delete"
And I delete the french localization of the "name" attribute
And I press "Save"
And I follow "My Custom Field"
Then there should be the following localizations:
| locale | name | default_value |
| en | My Custom Field | 0 |
| de | Mein Benutzerdefiniertes Feld | nil |
@javascript
Scenario: Deletion link is hidden when only one localization exists
When I follow "My Custom Field"
And I delete the german localization of the "name" attribute
Then the delete link for the english localization of the "name" attribute should not be visible

@ -0,0 +1,55 @@
Feature: Editing text custom fields
Background:
Given I am already logged in as "admin"
And the following languages are active:
| en |
| de |
And the following issue custom fields are defined:
| name | type |
| My Custom Field | text |
@javascript
Scenario: Adding localized default_values
When I go to the custom fields page
And I follow "My Custom Field"
And I add the english localization of the "default_value" attribute as "default"
And I add the german localization of the "default_value" attribute as "Standard"
And I press "Save"
And I follow "My Custom Field"
Then there should be the following localizations:
| locale | default_value | name |
| en | default | My Custom Field |
| de | Standard | nil |
@javascript
Scenario: Changing a localization which is not present for any other attribute to a locale existing in another attribute deletes the localization completely
When the Custom Field called "My Custom Field" has the following localizations:
| locale | name | default_value |
| en | My Custom Field | nil |
| de | nil | default |
And I go to the custom fields page
And I follow "My Custom Field"
And I select "English" from "custom_field_translations_attributes_1_locale"
And I press "Save"
And I follow "My Custom Field"
Then there should be the following localizations:
| locale | name | default_value |
| en | My Custom Field | default |
@javascript
Scenario: Changing a localization of one attribute to a non existent localization creates the localization
When the Custom Field called "My Custom Field" has the following localizations:
| locale | name | default_value |
| en | My Custom Field | default |
And I go to the custom fields page
And I follow "My Custom Field"
And I select "Deutsch" from "custom_field_translations_attributes_0_locale"
And I press "Save"
And I follow "My Custom Field"
Then there should be the following localizations:
| locale | name | default_value |
| en | nil | default |
| de | My Custom Field | nil |

@ -0,0 +1,156 @@
Feature: Group Memberships
Background:
Given there is a role "Manager"
And there is a role "Developer"
And there is 1 project with the following:
| Name | Project1 |
| Identifier | project1 |
And there is 1 User with:
| Login | peter |
| Firstname | Peter |
| Lastname | Pan |
And there is 1 User with:
| Login | bob |
| Firstname | Bob |
| Lastname | Bobbit |
And there is 1 User with:
| Login | hannibal |
| Firstname | Hannibal |
| Lastname | Smith |
And there is a group named "A-Team" with the following members:
| peter |
| bob |
@javascript
Scenario: Adding a group to a project on the project's page adds the group members as well
Given I am admin
When I go to the settings page of the project called "project1"
And I click on "tab-members"
And I check "A-Team"
And I check "Manager"
And I press "Add"
Then I should be on the settings page of the project called "project1"
And I should see "A-Team" within ".members"
And I should see "Bob Bobbit" within ".members"
And I should see "Peter Pan" within ".members"
@javascript
Scenario: Group-based memberships and individual memberships are handled separately
Given I am admin
When I go to the settings page of the project called "project1"
And I click on "tab-members"
And I check "Bob Bobbit"
And I check "Manager"
And I press "Add"
And I wait for the AJAX requests to finish
And I check "A-Team"
And I check "Developer"
And I press "Add"
And I wait for the AJAX requests to finish
When I delete the "A-Team" membership
And I wait for the AJAX requests to finish
Then I should see "Bob Bobbit" within ".members"
And I should not see "A-Team" within ".members"
And I should not see "Peter Pan" within ".members"
@javascript
Scenario: Removing a group from a project on the project's page removes all group members as well
Given I am admin
When I go to the settings page of the project called "project1"
And I click on "tab-members"
And I check "A-Team"
And I check "Manager"
And I press "Add"
Then I should be on the settings page of the project called "project1"
And I wait for the AJAX requests to finish
When I delete the "A-Team" membership
And I wait for the AJAX requests to finish
Then I should not see "A-Team" within ".members"
And I should not see "Bob Bobbit" within ".members"
And I should not see "Peter Pan" within ".members"
@javascript
Scenario: Adding a user to a group adds the user to projects as well
Given I am admin
When I go to the admin page of the group called "A-Team"
And I click on "tab-memberships"
And I select "Project1" from "Projects"
And I check "Manager"
And I press "Add"
And I wait for the AJAX requests to finish
And I click on "tab-users"
And I check "Hannibal Smith"
And I press "Add"
And I wait for the AJAX requests to finish
When I go to the settings page of the project called "project1"
And I click on "tab-members"
Then I should see "A-Team" within ".members"
And I should see "Bob Bobbit" within ".members"
And I should see "Peter Pan" within ".members"
And I should see "Hannibal Smith" within ".members"
@javascript
Scenario: Removing a user from a group removes the user from projects as well
Given I am admin
When I go to the admin page of the group called "A-Team"
And I click on "tab-memberships"
And I select "Project1" from "Projects"
And I check "Manager"
And I press "Add"
And I wait for the AJAX requests to finish
When I click on "tab-users"
And I delete "bob" from the group
And I wait for the AJAX requests to finish
When I go to the settings page of the project called "project1"
And I click on "tab-members"
Then I should see "A-Team" within ".members"
And I should not see "Bob Bobbit" within ".members"
And I should see "Peter Pan" within ".members"
@javascript
Scenario: Adding a group to project on the group's page adds the group members as well
Given I am admin
When I go to the admin page of the group called "A-Team"
And I click on "tab-memberships"
And I select "Project1" from "Projects"
And I check "Manager"
And I press "Add"
And I wait for the AJAX requests to finish
Then the project member "A-Team" should have the role "Manager"
When I go to the settings page of the project called "project1"
And I click on "tab-members"
Then I should see "A-Team" within ".members"
And I should see "Bob Bobbit" within ".members"
And I should see "Peter Pan" within ".members"

@ -0,0 +1,70 @@
Feature: Group memberships
Background:
Given there is 1 project with the following:
| name | project1 |
| identifier | project1 |
And there is 1 user with the following:
| login | bob |
| firstname | Bob |
| Lastname | Bobbit |
And there is 1 user with the following:
| login | alice |
| firstname | Alice |
| lastname | Wonderland |
And there is 1 group with the following:
| name | group1 |
And there is a role "alpha"
And there is a role "beta"
And the role "alpha" may have the following rights:
| manage_members |
And the user "bob" is a "alpha" in the project "project1"
Scenario: Adding a group with members to a project
Given the group "group1" has the following members:
| alice |
And I am already logged in as "bob"
When I go to the members tab of the settings page of the project "project1"
And I add the principal "group1" as a member with the roles:
| beta |
Then I should see the principal "group1" as a member with the roles:
| beta |
And I should see the principal "alice" as a member with the roles:
| beta |
Scenario: Adding members to a group after the group has been added to the project adds the users to the project
Given the group "group1" is a "beta" in the project "project1"
And I am already logged in as "admin"
When I go to the edit page of the group called "group1"
And I follow "Users" within ".tabs"
And I add the user "alice" to the group
And I go to the members tab of the settings page of the project "project1"
Then I should see the principal "group1" as a member with the roles:
| beta |
And I should see the principal "alice" as a member with the roles:
| beta |
@javascript
Scenario: Removing a group from a project removes it's members (users) as well if they have no roles of their own
Given the group "group1" has the following members:
| alice |
And the group "group1" is a "beta" in the project "project1"
And I am already logged in as "bob"
When I go to the members tab of the settings page of the project "project1"
And I follow the delete link of the project member "group1"
Then I should not see the principal "group1" as a member
And I should not see the principal "alice" as a member
@javascript
Scenario: Removing a group from a project leaves a member if he has other roles besides those inherited from the group
Given the group "group1" has the following members:
| alice |
And the user "alice" is a "alpha" in the project "project1"
And the group "group1" is a "beta" in the project "project1"
And I am already logged in as "bob"
When I go to the members tab of the settings page of the project "project1"
And I follow the delete link of the project member "group1"
Then I should not see the principal "group1" as a member
And I should see the principal "alice" as a member with the roles:
| alpha |

@ -0,0 +1,48 @@
Feature: Copying an issue should copy the watchers
Background:
Given there is 1 project with the following:
| identifier | omicronpersei8 |
And there is a role "CanCopyIssues"
And the role "CanCopyIssues" may have the following rights:
| add_issues |
| view_issues |
| view_issue_watchers |
And there is a role "CanAddWatchers"
And the role "CanAddWatchers" may have the following rights:
| add_issues |
| view_issues |
| view_issue_watchers |
| add_issue_watchers |
And there is 1 user with the following:
| login | ndnd |
And the user "ndnd" is a "CanCopyIssues" in the project "omicronpersei8"
And there is 1 user with the following:
| login | lrrr |
And the user "lrrr" is a "CanAddWatchers" in the project "omicronpersei8"
And there are the following issue status:
| name | is_default |
| New | true |
And the user "lrrr" has 1 issue with the following:
| subject | Improve sex drive |
| description | Acquire human horn |
And the issue "Improve sex drive" is watched by:
| lrrr |
| ndnd |
Scenario: Watchers shouldn't be copied when the user doesn't have the permission to
Given I am logged in as "ndnd"
When I go to the copy page for the issue "Improve sex drive"
Then I should not see "Watchers"
When I fill in "issue_subject" with "Improve sex drive even more"
When I click on "Create"
Then I should not see "Watchers"
And the issue "Improve sex drive even more" should have 0 watchers
Scenario: Watchers should be copied when the user has the permission to
Given I am logged in as "lrrr"
When I go to the copy page for the issue "Improve sex drive"
Then I should see "Watchers" within "p#watchers_form"
When I fill in "issue_subject" with "Improve sex drive even more"
When I click on "Create"
Then I should see "Watchers (2)"
And the issue "Improve sex drive even more" should have 2 watchers

@ -0,0 +1,45 @@
Feature: Exporting issues
Background:
Given there is 1 user with the following:
| login | bob |
And there is a role "member"
And the role "member" may have the following rights:
| view_issues |
And there is 1 project with the following:
| name | project1 |
| identifier | project1 |
And the user "bob" is a "member" in the project "project1"
And the user "bob" has 1 issue with the following:
| subject | Some Issue |
And I am already logged in as "bob"
Scenario: No export links on project issues index if user has no "export_issues" permission
When I go to the issues index page of the project called "project1"
Then I should not see "CSV" within ".other-formats"
And I should not see "PDF" within ".other-formats"
Scenario: Export links on project issues index if user has the "export_issues" permission
Given the role "member" may have the following rights:
| view_issues |
| export_issues |
When I go to the issues index page of the project called "project1"
Then I should see "CSV" within ".other-formats"
And I should see "PDF" within ".other-formats"
Scenario: No export links on global issues index if user has no "export_issues" permission
When I go to the global index page of issues
Then I should not see "CSV" within ".other-formats"
And I should not see "PDF" within ".other-formats"
Scenario: Export links on global issues index if user has the "export_issues" permission
Given the role "member" may have the following rights:
| view_issues |
| export_issues |
When I go to the global index page of issues
Then I should see "CSV" within ".other-formats"
And I should see "PDF" within ".other-formats"

@ -0,0 +1,30 @@
Feature: Paginated issue index list
Background:
Given there is 1 project with the following:
| identifier | project1 |
| name | project1 |
And there is 1 user with the following:
| login | bob |
And there is a role "member"
And the role "member" may have the following rights:
| view_issues |
| create_issues |
And the user "bob" is a "member" in the project "project1"
And the user "bob" has 26 issues with the following:
| subject | Issuesubject |
And I am already logged in as "bob"
Scenario: Pagination within a project
When I go to the issues index page of the project "project1"
Then I should see 25 issues
When I follow "2" within ".pagination"
Then I should be on the issues index page of the project "project1"
And I should see 1 issue
Scenario: Pagination outside a project
When I go to the global index page of issues
Then I should see 25 issues
When I follow "2" within ".pagination"
Then I should be on the global index page of issues
And I should see 1 issue

@ -0,0 +1,49 @@
Feature: Issue textile quickinfo links
Background:
Given there is 1 project with the following:
| name | parent |
| identifier | parent |
And there is a role "member"
And the role "member" may have the following rights:
| add_issues |
| view_issues |
And there is 1 user with the following:
| login | bob|
And the user "bob" is a "member" in the project "parent"
And there are the following issue status:
| name | is_closed | is_default |
| New | false | true |
| In Progress | false | false |
Given the user "bob" has 1 issue with the following:
| subject | issue1 |
| due_date | 2012-05-04 |
| start_date | 2011-05-04 |
| description | Aioli Sali Grande |
And I am already logged in as "bob"
Scenario: Adding an issue link
When I go to the issues/new page of the project called "parent"
And I fill in "One hash key" for "issue_subject"
And I fill in the ID of "issue1" with 1 hash for "issue_description"
And I press "Create"
Then I should see an issue link for "issue1" within "div.wiki"
When I follow the issue link with 1 hash for "issue1"
Then I should be on the page of the issue "issue1"
Scenario: Adding an issue quickinfo link
When I go to the issues/new page of the project called "parent"
And I fill in "One hash key" for "issue_subject"
And I fill in the ID of "issue1" with 2 hash for "issue_description"
And I press "Create"
Then I should see a quickinfo link for "issue1" within "div.wiki"
When I follow the issue link with 2 hash for "issue1"
Then I should be on the page of the issue "issue1"
Scenario: Adding an issue quickinfo link with description
When I go to the issues/new page of the project called "parent"
And I fill in "One hash key" for "issue_subject"
And I fill in the ID of "issue1" with 3 hash for "issue_description"
And I press "Create"
Then I should see a quickinfo link with description for "issue1" within "div.wiki"
When I follow the issue link with 3 hash for "issue1"
Then I should be on the page of the issue "issue1"

@ -0,0 +1,172 @@
Feature: Issue Sum Calculations for Currency
Background:
Given there is 1 project with the following:
| name | project1 |
| identifier | project1 |
And the following issue custom fields are defined:
| name | type |
| cf1 | float |
And the custom field "cf1" is summable
And there is a role "Manager"
And there is 1 user with:
| Login | manager |
And the user "manager" is a "Manager" in the project "project1"
And I am already logged in as "admin"
@javascript
Scenario: Should calculate an overall sum for a standard issue query
Given the user "manager" has 1 issue with the following:
| subject | Some issue |
| cf1 | 100 |
And the user "manager" has 1 issue with the following:
| subject | Some other issue |
| cf1 | 50 |
When I go to the issues index page for the project called "project1"
And I select to see columns
| cf1 |
And I toggle the Options fieldset
And I check "display_sums"
And I click on "Apply"
And I wait 10 seconds for AJAX
Then I should see "150" in the overall sum
@javascript
Scenario: Should not calculate an overall sum for a standard issue query if the column isn't summable
Given the user "manager" has 1 issue with the following:
| subject | Some issue |
| cf1 | 100 |
And the user "manager" has 1 issue with the following:
| subject | Some other issue |
| cf1 | 50 |
And the custom field "cf1" is not summable
When I go to the issues index page for the project called "project1"
And I select to see columns
| cf1 |
And I toggle the Options fieldset
And I check "display_sums"
And I click on "Apply"
And I wait 10 seconds for AJAX
Then I should not see "150" in the overall sum
@javascript
Scenario: Should tick the checkbox on query edit if we previously displayed sums
Given the user "manager" has 1 issue with the following:
| subject | Some issue |
| cf1 | 100 |
And the user "manager" has 1 issue with the following:
| subject | Some other issue |
| cf1 | 50 |
When I go to the issues index page for the project called "project1"
And I select to see columns
| cf1 |
And I toggle the Options fieldset
And I check "display_sums"
And I click on "Apply"
And I click on "Save"
And I fill in "TestQuery" for "query_name"
And I press "Save"
And I go to the issues index page for the project called "project1"
And I click on "TestQuery"
Then I should be on the issues index page for the project called "project1"
And the "display_sums" checkbox should be checked
And I should see "150" in the overall sum
And I click on "Edit"
Then the "query[display_sums]" checkbox should be checked
@javascript
Scenario: Should not tick the checkbox on query edit if we did not previously display sums
Given the user "manager" has 1 issue with the following:
| subject | Some issue |
| cf1 | 100 |
And the user "manager" has 1 issue with the following:
| subject | Some other issue |
| cf1 | 50 |
When I go to the issues index page for the project called "project1"
And I select to see columns
| cf1 |
And I toggle the Options fieldset
And I uncheck "display_sums"
And I click on "Apply"
And I click on "Save"
And I fill in "TestQuery" for "query_name"
And I press "Save"
And I go to the issues index page for the project called "project1"
And I click on "TestQuery"
Then I should be on the issues index page for the project called "project1"
And the "display_sums" checkbox should not be checked
And I click on "Edit"
Then the "query[display_sums]" checkbox should not be checked
@javascript
Scenario: Should calculate an overall sum for a grouped issue query with multiple groups
Given there is 1 user with:
| Login | alice |
And the user "alice" is a "Manager" in the project "project1"
And there is 1 user with:
| Login | bob |
And the user "bob" is a "Manager" in the project "project1"
And the user "manager" has 1 issue with the following:
| subject | Some issue |
| cf1 | 100 |
And the user "manager" has 1 issue with the following:
| subject | Some issue |
| cf1 | 50 |
And the user "alice" has 1 issue with the following:
| subject | Some issue |
| cf1 | 300 |
And the user "bob" has 1 issue with the following:
| subject | Some issue |
| cf1 | 200 |
And the user "bob" has 1 issue with the following:
| subject | Some issue |
| cf1 | 250 |
When I go to the issues index page for the project called "project1"
And I select to see columns
| cf1 |
And I toggle the Options fieldset
And I check "display_sums"
And I select "Assignee" from "group_by"
And I click on "Apply"
And I wait 10 seconds for AJAX
Then I should see "150" in the grouped sum
And I should see "300" in the grouped sum
And I should see "450" in the grouped sum
And I should see "900" in the overall sum
@javascript
Scenario: Should calculate an overall sum for a grouped issue query with a single group
Given the user "manager" has 1 issue with the following:
| subject | Some issue |
| cf1 | 100 |
And the user "manager" has 1 issue with the following:
| subject | Some issue |
| cf1 | 50 |
When I go to the issues index page for the project called "project1"
And I select to see columns
| cf1 |
And I toggle the Options fieldset
And I check "display_sums"
And I select "Assignee" from "group_by"
And I click on "Apply"
And I wait 10 seconds for AJAX
Then I should see "150" in the grouped sum
And I should see "150" in the overall sum
@javascript
Scenario: Should strip floats down to a precission of 2 number
Given the user "manager" has 1 issue with the following:
| subject | Some issue |
| cf1 | 100.0000001 |
And the user "manager" has 1 issue with the following:
| subject | Some issue |
| cf1 | 50.09 |
When I go to the issues index page for the project called "project1"
And I select to see columns
| cf1 |
And I toggle the Options fieldset
And I check "display_sums"
And I select "Assignee" from "group_by"
And I click on "Apply"
And I wait 10 seconds for AJAX
Then I should see "150.09" in the grouped sum
And I should see "150.09" in the overall sum

@ -0,0 +1,13 @@
Feature: Creating Projects
@javascript
Scenario: Creating a Subproject
Given there is 1 project with the following:
| name | Parent |
| identifier | parent |
And I am admin
When I go to the overview page of the project "Parent"
And I follow "New subproject"
And I fill in "project_name" with "child"
And I press "Save"
Then I should be on the settings page of the project called "child"

@ -0,0 +1,22 @@
# encoding: utf-8
Then /^I should see an issue link for "([^"]*)" within "([^"]*)"$/ do |issue_name, container|
issue = Issue.find_by_subject(issue_name)
text = "##{issue.id}"
step %Q{I should see "#{text}" within "#{container}"}
end
Then /^I should see a quickinfo link for "([^"]*)" within "([^"]*)"$/ do |issue_name, container|
issue = Issue.find_by_subject(issue_name)
text = "##{issue.id} #{issue.status}: #{issue.subject} #{issue.start_date.to_s}#{issue.due_date.to_s} (#{issue.assigned_to.to_s})"
step %Q{I should see "#{text}" within "#{container}"}
end
Then /^I should see a quickinfo link with description for "([^"]*)" within "([^"]*)"$/ do |issue_name, container|
issue = Issue.find_by_subject(issue_name)
step %Q{I should see a quickinfo link for "#{issue_name}" within "#{container}"}
step %Q{I should see "#{issue.description}" within "#{container}"}
end

@ -0,0 +1,19 @@
When /^I fill in the ID of "([^"]*)" with (\d+) hash for "([^"]*)"$/ do |issue_name, number_hash_keys, container|
issue = Issue.find_by_subject(issue_name)
text = "#{('#' * number_hash_keys.to_i)}#{issue.id}"
step %Q{I fill in "#{text}" for "#{container}"}
end
When /^I follow the issue link with (\d+) hash for "([^"]*)"$/ do |hash_count, issue_name|
issue = Issue.find_by_subject(issue_name)
text = ""
if hash_count.to_i > 1
text = "##{issue.id} #{issue.status}: #{issue.subject}"
elsif hash_count.to_i == 1
text = "##{issue.id}"
end
step %Q{I follow "#{text}"}
end

@ -0,0 +1,7 @@
Then /^the breadcrumb should contain "(.+)"$/ do |string|
container = ChiliProject::VERSION::MAJOR < 2 ? "p.breadcrumb a" : "#breadcrumb a"
steps %Q{ Then I should see "#{string}" within "#{container}" }
end

@ -0,0 +1,46 @@
# "Then I should see 5 articles"
Then /^I should see (\d+) ([^\" ]+)(?: within "([^\"]*)")?$/ do |number, name, selector|
with_scope(selector) do
if defined?(Spec::Rails::Matchers)
page.should have_css(".#{name.singularize}", :count => number.to_i)
else
assert page.has_css?(".#{name.singularize}", :count => number.to_i)
end
end
end
Then /^I should not see(?: (\d+))? ([^\" ]+)(?: within "([^\"]*)")?$/ do |number, name, selector|
options = number ? {:count => number.to_i} : {}
with_scope(selector) do
if defined?(Spec::Rails::Matchers)
page.should have_no_css(".#{name.singularize}", options)
else
assert page.has_no_css?(".#{name.singularize}", options)
end
end
end
Around('@changes_environment') do |scenario, block|
saved_env = ENV["RAILS_ENV"]
block.call
ENV["RAILS_ENV"] = saved_env
end
Then /^I am in "([^\"]*)" mode$/ do |env|
ENV["RAILS_ENV"] = env
end
Given /^the [pP]roject(?: "([^\"]+?)")? uses the following trackers:$/ do |project, table|
project = get_project(project)
trackers = table.raw.map do |line|
name = line.first
tracker = Tracker.find_by_name(name)
tracker = FactoryGirl.create(:tracker, :name => name) if tracker.blank?
tracker
end
project.update_attributes :tracker_ids => trackers.map(&:id).map(&:to_s)
end

@ -0,0 +1,35 @@
Given /^the following (user|issue) custom fields are defined:$/ do |type, table|
type = (type + "_custom_field").to_sym
as_admin do
table.hashes.each_with_index do |r, i|
attr_hash = { :name => r['name'],
:field_format => r['type']}
attr_hash[:possible_values] = r['possible_values'].split(",").collect(&:strip) if r['possible_values']
attr_hash[:is_required] = (r[:required] == 'true') if r[:required]
attr_hash[:editable] = (r[:editable] == 'true') if r[:editable]
attr_hash[:visible] = (r[:visible] == 'true') if r[:visible]
attr_hash[:default_value] = r[:default_value] ? r[:default_value] : nil
attr_hash[:is_for_all] = r[:is_for_all] || true
FactoryGirl.create type, attr_hash
end
end
end
Given /^the user "(.+?)" has the user custom field "(.+?)" set to "(.+?)"$/ do |login, field_name, value|
user = User.find_by_login(login)
custom_field = UserCustomField.find_by_name(field_name)
user.custom_values.build(:custom_field => custom_field, :value => value)
user.save!
end
Given /^the custom field "(.+)" is( not)? summable$/ do |field_name, negative|
custom_field = IssueCustomField.find_by_name(field_name)
Setting.issue_list_summable_columns = negative ?
Setting.issue_list_summable_columns - ["cf_#{custom_field.id}"] :
Setting.issue_list_summable_columns << "cf_#{custom_field.id}"
end

@ -0,0 +1,70 @@
module Capybara::Node::Finders
alias old_find find
def find(*args)
tries = 0
begin
old_find(*args)
rescue Capybara::ElementNotFound => e
tries += 1
tries < 3 ? retry : raise(e)
end
end
end
Then /^I should (not )?see "([^"]*)"\s*\#.*$/ do |negative, name|
steps %Q{
Then I should #{negative}see "#{name}"
}
end
Then /^I should (not )?see "([^"]*)" within "([^"]*)"\s*\#.*$/ do |negative, name, scope|
steps %Q{
Then I should #{negative}see "#{name}" within "#{scope}"
}
end
When /^I click(?:| on) "([^"]*)"$/ do |name|
begin
steps %Q{
When I follow "#{name}"
}
rescue Capybara::ElementNotFound
steps %Q{
When I press "#{name}"
}
end
end
When /^(?:|I )jump to [Pp]roject "([^\"]*)"$/ do |project|
begin
find(:xpath, '//div[@id="quick-search"]/select[last()]').select project
rescue Capybara::ElementNotFound
click_link('Projects')
find(:css, '#account-nav .chzn-results li', :text => project).click
end
end
Then /^"([^"]*)" should be selected for "([^"]*)"$/ do |value, select_id|
# that makes capybara wait for the ajax request
find(:xpath, "//body")
# if you wanna see ugly things, look at the following line
(page.evaluate_script("$('#{select_id}').value") =~ /^#{value}$/).should be_present
end
Then /^"([^"]*)" should (not )?be selectable from "([^"]*)"$/ do |value, negative, select_id|
#more page.evaluate ugliness
find(:xpath, "//body")
bool = negative ? false : true
(page.evaluate_script("$('#{select_id}').select('option[value=#{value}]').first.disabled") =~ /^#{bool}$/).should be_present
end
# This does NOT trigger actual hovering by means of :hover.
# To use this, you have to adjust your stylesheet accordingly.
When /^I hover over "([^"]+)"$/ do |selector|
page.execute_script "jQuery(#{selector.inspect}).addClass('hover');"
end
When /^I stop hovering over "([^"]*)"$/ do |selector|
page.execute_script "jQuery(#{selector.inspect}).removeClass('hover');"
end

@ -0,0 +1,19 @@
Given /^the "(.+)" drop-down should( not)? have the following options:$/ do |id, neg, table|
meth = neg ? :should_not : :should
table.raw.each do | option |
page.send(meth, have_xpath("//select[@id = '#{id}']//option[@value = '#{option[0]}']"))
end
end
Then /^the "(.+)" drop-down should have the following options (enabled|disabled):$/ do |id, state, table|
state = state == "disabled" ? "" : "not"
table.raw.each do | option |
page.should have_xpath "//select[@id = '#{id}']//option[@value = '#{option[0]}' and #{state}(@disabled)]"
end
end
Then /^the "(.+)" drop-down(?: within "([^\"]*)")? should have "([^\"]*)" selected$/ do |field_name, selector, option_name|
with_scope(selector) do
find_field(field_name).find('option[selected]').text.should == option_name
end
end

@ -0,0 +1,559 @@
# encoding: utf-8
require 'active_record/fixtures'
Before do |scenario|
unless ScenarioDisabler.empty_if_disabled(scenario)
# Reset the DB and load the minimal fixtures for a functional system before each scenario
system "RAILS_ENV=test db:test:load >/dev/null 2>/dev/null"
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test'])
ActiveRecord::Schema.verbose = false
fixtures_path = File.expand_path(Rails.root.join('test/fixtures'))
FactoryGirl.create(:admin) unless User.find_by_login("admin")
FactoryGirl.create(:anonymous) unless AnonymousUser.count > 0
Setting.notified_events = [] #can not test mailer
Dir.glob(File.join(fixtures_path, "*.yml")) do |table_name|
ActiveRecord::Fixtures.create_fixtures(fixtures_path, File.basename(table_name).gsub(".yml", ""))
end
if Capybara.current_driver.to_s.include?("selenium")
Capybara.current_session.driver.browser.manage.window.resize_to(3000, 3000)
end
end
end
Given /^(?:|I )am not logged in$/ do
User.current = AnonymousUser.first
end
Given /^(?:|I )am [aA]dmin$/ do
steps %Q{
Given I am logged in as \"admin\"
}
end
Given /^I am already logged in as "(.+?)"$/ do |login|
# consider using: http://collectiveidea.com/blog/archives/2012/01/05/capybara-cucumber-and-how-the-cookie-crumbles/
# or: https://github.com/railsware/rack_session_access
# once the application is rails3
user = User.find_by_login(login)
ApplicationController.class_eval do
define_method :set_user_session do
session[:user_id] = user.id
ApplicationController.skip_before_filter :set_user_session
ApplicationController.subclasses.each do |subclass_name|
unless subclass_name.to_s.include?("Spec::Rails")
subclass_name.prepend_before_filter :set_user_session
end
end
end
end
ApplicationController.prepend_before_filter :set_user_session
ApplicationController.subclasses.each do |subclass_name|
unless subclass_name.to_s.include?("Spec::Rails")
subclass_name.prepend_before_filter :set_user_session
end
end
end
def login_user(username, password = "admin")
steps %Q{
Given I am on the logout page
When I go to the login page
And I fill in "#{username}" for "username"
And I fill in "#{password}" for "password"
And I press "Login"
Then I should be logged in as "#{username}"
}
end
Given /^(?:|I )am logged in as "([^\"]*)"$/ do |username|
FactoryGirl.create(:admin) unless User.find_by_login("admin")
FactoryGirl.create(:anonymous) unless AnonymousUser.count > 0
login_user(username)
end
Given /^there is 1 [pP]roject with(?: the following)?:$/ do |table|
p = FactoryGirl.build(:project)
send_table_to_object(p, table)
end
Then /^the project "([^"]*)" is( not)? public$/ do |project_name, negation|
p = Project.find_by_name(project_name)
p.update_attribute(:is_public, !negation)
end
Given /^the [Pp]roject "([^\"]*)" has 1 [wW]iki(?: )?[pP]age with the following:$/ do |project, table|
p = Project.find_by_name(project)
p.wiki.create! unless p.wiki
page = FactoryGirl.create(:wiki_page, :wiki => p.wiki)
content = FactoryGirl.create(:wiki_content, :page => page)
send_table_to_object(page, table)
end
Given /^there is 1 [Uu]ser with(?: the following)?:$/ do |table|
login = table.rows_hash[:Login].to_s + table.rows_hash[:login].to_s
user = User.find_by_login(login) unless login.blank?
if user
table = table.reject_key(/(L|l)ogin/)
else
user = FactoryGirl.create(:user)
user.password = user.password_confirmation = nil
end
modify_user(user, table)
end
Given /^the [Uu]ser "([^\"]*)" has:$/ do |user, table|
u = User.find_by_login(user)
raise "No such user: #{user}" unless u
modify_user(u, table)
end
Given /^there are the following users:$/ do |table|
table.raw.flatten.each do |login|
FactoryGirl.create(:user, :login => login)
end
end
Given /^the plugin (.+) is loaded$/ do |plugin_name|
plugin_name = plugin_name.gsub("\"", "")
Redmine::Plugin.all.detect {|x| x.id == plugin_name.to_sym}.present? ? nil : pending("Plugin #{plugin_name} not loaded")
end
Given /^(?:the )?[pP]roject "([^\"]*)" uses the following [mM]odules:$/ do |project, table|
p = Project.find_by_name(project)
p.enabled_module_names += table.raw.map { |row| row.first }
p.reload
end
Given /^the [Uu]ser "([^\"]*)" is a "([^\"]*)" (?:in|of) the [Pp]roject "([^\"]*)"$/ do |user, role, project|
u = User.find_by_login(user)
r = Role.find_by_name(role)
p = Project.find_by_name(project) || Project.find_by_identifier(project)
as_admin do
Member.new.tap do |m|
m.user = u
m.privacy_unnecessary = true if plugin_loaded?("redmine_dtag_privacy")
m.roles << r
m.project = p
end.save!
end
end
Given /^there is a(?:n)? (default )?(?:issue)?status with:$/ do |default, table|
name = table.raw.select { |ary| ary.include? "name" }.first[table.raw.first.index("name") + 1].to_s
IssueStatus.find_by_name(name) || IssueStatus.create(:name => name.to_s, :is_default => !!default)
end
Given /^there is a(?:n)? (default )?issuepriority with:$/ do |default, table|
name = table.raw.select { |ary| ary.include? "name" }.first[table.raw.first.index("name") + 1].to_s
project = get_project
IssuePriority.new.tap do |prio|
prio.name = name
prio.is_default = !!default
prio.project = project
prio.save!
end
end
Given /^there is a [rR]ole "([^\"]*)"$/ do |name|
Role.spawn.tap { |r| r.name = name }.save! unless Role.find_by_name(name)
end
Given /^there are the following roles:$/ do |table|
table.raw.flatten.each do |name|
FactoryGirl.create(:role, :name => name) unless Role.find_by_name(name)
end
end
Given /^the [rR]ole "([^\"]*)" may have the following [rR]ights:$/ do |role, table|
r = Role.find_by_name(role)
raise "No such role was defined: #{role}" unless r
as_admin do
available_perms = Redmine::AccessControl.permissions.collect(&:name)
r.permissions = []
table.raw.each do |_perm|
perm = _perm.first
unless perm.blank?
perm = perm.gsub(" ", "_").underscore.to_sym
if available_perms.include?(:"#{perm}")
r.permissions << perm
end
end
end
r.save!
end
end
Given /^the [rR]ole "(.+?)" has no (?:[Pp]ermissions|[Rr]ights)$/ do |role_name|
role = Role.find_by_name(role_name)
raise "No such role was defined: #{role_name}" unless role
as_admin do
role.permissions = []
role.save!
end
end
Given /^the [Uu]ser "([^\"]*)" has 1 time [eE]ntry$/ do |user|
u = User.find_by_login user
p = u.projects.last
raise "This user must be member of a project to have issues" unless p
i = Issue.generate_for_project!(p)
t = TimeEntry.generate
t.user = u
t.issue = i
t.project = p
t.activity.project = p
t.activity.save!
t.save!
end
Given /^the [Uu]ser "([^\"]*)" has 1 time entry with (\d+\.?\d*) hours? at the project "([^\"]*)"$/ do |user, hours, project|
p = Project.find_by_name(project) || Project.find_by_identifier(project)
as_admin do
t = TimeEntry.generate
i = Issue.generate_for_project!(p)
t.project = p
t.issue = i
t.hours = hours.to_f
t.user = User.find_by_login user
t.activity.project = p
t.activity.save!
t.save!
end
end
Given /^the [Uu]ser "([^\"]*)" has (\d+) [iI]ssue(?:s)? with(?: the following)?:$/ do |user, count, table|
u = User.find_by_login user
raise "This user must be member of a project to have issues" unless u.projects.last
as_admin count do
i = Issue.generate_for_project!(u.projects.last)
i.author = u
i.assigned_to = u
i.tracker = Tracker.find_by_name(table.rows_hash.delete("tracker")) if table.rows_hash["tracker"]
send_table_to_object(i, table, {}, method(:add_custom_value_to_issue))
i.save!
end
end
Given /^the [Pp]roject "([^\"]*)" has (\d+) [tT]ime(?: )?[eE]ntr(?:ies|y) with the following:$/ do |project, count, table|
p = Project.find_by_name(project) || Project.find_by_identifier(project)
as_admin count do
t = TimeEntry.generate
i = Issue.generate_for_project!(p)
t.project = p
t.issue = i
t.activity.project = p
t.activity.save!
send_table_to_object(t, table,
:user => Proc.new do |o,v|
o.user = User.find_by_login(v)
o.save!
end,
:spent_on => Proc.new do |object, value|
# This works for definitions like "2 years ago"
number, time_unit, tempus = value.split
time = number.to_i.send(time_unit.to_sym).send(tempus.to_sym)
object.spent_on = time
object.save!
end
)
end
end
Given /^the [Pp]roject "([^\"]*)" has (\d+) [iI]ssue(?:s)? with(?: the following)?:$/ do |project, count, table|
p = Project.find_by_name(project) || Project.find_by_identifier(project)
as_admin count do
i = Issue.generate_for_project!(p)
send_table_to_object(i, table, {}, method(:add_custom_value_to_issue))
end
end
Given /^the [Pp]roject "([^\"]*)" has (\d+) [Dd]ocument with(?: the following)?:$/ do |project, count, table|
p = Project.find_by_name(project) || Project.find_by_identifier(project)
as_admin count do
d = Document.spawn
d.project = p
d.category = DocumentCategory.first
d.save!
send_table_to_object(d, table)
end
end
Given /^the [Pp]roject (.+) has 1 version with(?: the following)?:$/ do |project, table|
project.gsub!("\"", "")
p = Project.find_by_name(project) || Project.find_by_identifier(project)
table.rows_hash["effective_date"] = eval(table.rows_hash["effective_date"]).to_date if table.rows_hash["effective_date"]
as_admin do
v = Version.generate
send_table_to_object(v, table)
p.versions << v
end
end
Given /^the [pP]roject "([^\"]*)" has 1 [sS]ubproject$/ do |project|
parent = Project.find_by_name(project)
p = Project.generate
p.set_parent!(parent)
p.save!
end
Given /^the [pP]roject "([^\"]*)" has 1 [sS]ubproject with the following:$/ do |project, table|
parent = Project.find_by_name(project)
p = FactoryGirl.build(:project)
as_admin do
send_table_to_object(p, table)
end
p.set_parent!(parent)
p.save!
end
Given /^there are the following trackers:$/ do |table|
table.hashes.each_with_index do |t, i|
tracker = Tracker.find_by_name(t['name'])
tracker = Tracker.new :name => t['name'] if tracker.nil?
tracker.position = t['position'] ? t['position'] : i
tracker.is_in_roadmap = t['is_in_roadmap'] ? t['is_in_roadmap'] : true
tracker.save!
end
end
Given /^there are the following issue status:$/ do |table|
table.hashes.each_with_index do |t, i|
status = IssueStatus.find_by_name(t['name'])
status = IssueStatus.new :name => t['name'] if status.nil?
status.is_closed = t['is_closed'] == 'true' ? true : false
status.is_default = t['is_default'] == 'true' ? true : false
status.position = t['position'] ? t['position'] : i
status.default_done_ratio = t['default_done_ratio']
status.save!
end
end
Given /^the tracker "(.+?)" has the default workflow for the role "(.+?)"$/ do |tracker_name, role_name|
role = Role.find_by_name(role_name)
tracker = Tracker.find_by_name(tracker_name)
tracker.workflows = []
IssueStatus.all(:order => "id ASC").collect(&:id).combination(2).each do |c|
tracker.workflows.build(:old_status_id => c[0], :new_status_id => c[1], :role => role)
end
tracker.save!
end
Given /^the [iI]ssue "([^\"]*)" has (\d+) [tT]ime(?: )?[eE]ntr(?:ies|y) with the following:$/ do |issue, count, table|
i = Issue.find(:last, :conditions => ["subject = '#{issue}'"])
raise "No such issue: #{issue}" unless i
as_admin count do
t = TimeEntry.generate
t.project = i.project
t.spent_on = DateTime.now
t.issue = i
send_table_to_object(t, table,
{:user => Proc.new do |o,v|
o.user = User.find_by_login(v)
o.save!
end})
end
end
Given /^I select to see [cC]olumn "([^\"]*)"$/ do |column_name|
steps %Q{
When I select \"#{column_name}\" from \"available_columns\"
When I click on \"\"
When I click on \"Apply\"
}
end
Given /I select to see [cC]olumn(?:s)?$/ do |table|
params = "?set_filter=1&" + table.raw.collect(&:first).collect do |name|
page.source =~ /<option value="(.*?)">#{name}<\/option>/
column_name = $1 || name.gsub(" ", "_").downcase
"query[column_names][]=#{column_name}"
end.join("&")
visit(current_path + params)
end
Given /^I start debugging$/ do
save_and_open_page
binding.pry
true
end
Given /^I (?:stop|pause) (?:step )?execution$/ do
loop do
$stdout.puts "\nPausing step execution. Press <Enter> to continue. Enter `debug` to start debugging."
text = $stdin.readline
step "I start debugging" if text =~ /debug/
break if text.strip.empty?
end
end
When /^(?:|I )login as (.+)? with password (.+)?$/ do |username, password|
username = username.gsub("\"", "")
password = password.gsub("\"", "")
login_user(username, password)
end
Then /^I should be logged in as "([^\"]*)"?$/ do |username|
user = User.find_by_login(username) || User.anonymous
page.should have_xpath("//div[contains(., 'Logged in as #{username}')] | //a[contains(.,'#{user.name}')]")
User.current = user
end
When /^(?:|I )login as (.+)?$/ do |username|
steps %Q{
When I login as #{username} with password admin
}
end
When /^I satisfy the "(.+)" plugin to (.+)$/ do |plugin_name, action|
if plugin_loaded?(plugin_name)
action_name = action.gsub("\"", "")
plugin_action(plugin_name, action_name)
end
end
Given /^I am working in [pP]roject "(.+?)"$/ do |project_name|
@project = Project.find_by_name(project_name)
end
Given /^the [pP]roject uses the following modules:$/ do |table|
step %Q{the project "#{get_project}" uses the following modules:}, table
end
Given /the user "(.*?)" is a "(.*?)"/ do |user, role|
step %Q{the user "#{user}" is a "#{role}" in the project "#{get_project.name}"}
end
Given /^the [pP]roject(?: "([^\"]*)")? has the following trackers:$/ do |project_name, table|
p = get_project(project_name)
table.hashes.each_with_index do |t, i|
tracker = Tracker.find_by_name(t['name'])
tracker = Tracker.new :name => t['name'] if tracker.nil?
tracker.position = t['position'] ? t['position'] : i
tracker.is_in_roadmap = t['is_in_roadmap'] ? t['is_in_roadmap'] : true
tracker.save!
p.trackers << tracker
p.save!
end
end
def get_project(project_name = nil)
if project_name.blank?
project = @project
else
project = Project.find_by_name(project_name)
end
if project.nil?
if project_name.blank?
raise "Could not identify the current project. Make sure to use the 'I am working in project \"Project Name\" step beforehand."
else
raise "Could not find project with the name \"#{project_name}\"."
end
end
project
end
# Modify a given user using the specified table
def modify_user(u, table)
as_admin do
send_table_to_object(u, table,
:default_rate => Proc.new do |user, value|
user.save!
DefaultHourlyRate.new.tap do |r|
r.valid_from = 3.years.ago.to_date
r.rate = value
r.user_id = user.id
end.save!
end,
:name => Proc.new {|user, value| user.login = name; user.save!},
:hourly_rate => Proc.new do |user, value|
user.save!
HourlyRate.new.tap do |r|
r.valid_from = (2.years.ago + HourlyRate.count.days).to_date
r.rate = value
r.user_id = user.id
r.project = user.projects.last
end.save!
end
)
u.save!
end
u
end
# Encapsule the logic to set a custom field on an issue
def add_custom_value_to_issue(object, key, value)
if IssueCustomField.all.collect(&:name).include? key.to_s
cv = CustomValue.find(:first, :conditions => ["customized_id = '#{object.id}'"])
cv ||= CustomValue.new
cv.customized_type = "Issue"
cv.customized_id = object.id
cv.custom_field_id = IssueCustomField.first(:joins => :translations, :conditions => ["custom_field_translations.name = ?", key]).id
cv.value = value
cv.save!
end
end
# Try to assign an object the values set in a table
def send_table_to_object(object, table, except = {}, rescue_block = nil)
return unless table.raw.present?
as_admin do
table.rows_hash.each do |key, value|
_key = key.gsub(" ", "_").underscore.to_sym
if except[_key]
except[_key].call(object, value)
elsif except[key]
except[key].call(object, value)
elsif object.respond_to? :"#{_key}="
object.send(:"#{_key}=", value)
elsif rescue_block
rescue_block.call(object, key, value)
else
raise "No such method #{_key} on a #{object.class}"
end
end
object.save! if object.changed?
end
end
# Do something as admin
def as_admin(count = 1)
cur_user = User.current
User.current = User.find_by_login("admin")
retval = nil
count.to_i.times do
retval = yield
end
User.current = cur_user
retval
end
def plugin_loaded?(name)
Redmine::Plugin.all.detect {|x| x.id == name.to_sym}.present?
end

@ -0,0 +1,54 @@
Given /^there is 1 group with the following:$/ do |table|
group = FactoryGirl.build(:group)
send_table_to_object group, table, { :name => Proc.new { |group, name| group.lastname = name } }
end
Given /^the group "(.+)" is a "(.+)" in the project "(.+)"$/ do |group_name, role_name, project_identifier|
steps %Q{ Given the principal "#{group_name}" is a "#{role_name}" in the project "#{project_identifier}" }
end
Given /^the group "(.+?)" has the following members:$/ do |name, table|
group = Group.find_by_lastname(name)
raise "No group with name #{name} found" unless group.present?
user_names = table.raw.flatten
users = User.find_all_by_login(user_names)
not_found = user_names - users.map(&:login)
raise "Could not find users with login: #{not_found}" if not_found.size > 0
group.add_member!(users)
end
When /^I add the user "(.+)" to the group$/ do |user_login|
user = User.find_by_login(user_login)
raise "Could not find users with login: #{user_login}" if user.nil?
steps %Q{ When I check "#{user.name}" within "#tab-content-users #users"
And I press "Add" }
end
Given /^there is a group named "(.*?)" with the following members:$/ do |name, table|
group = FactoryGirl.create(:group, :lastname => name)
table.raw.flatten.each do |login|
group.users << User.find_by_login!(login)
end
end
When /^I delete the "([^"]*)" membership$/ do |group_name|
membership = member_for_login(group_name)
step %Q(I follow "Delete" within "#member-#{membership.id}")
end
When /^I delete "([^"]*)" from the group$/ do |login|
user = User.find_by_login!(login)
step %Q(I follow "Delete" within "#user-#{user.id}")
end
InstanceFinder.register(Group, Proc.new{ |name| Group.find_by_lastname(name) })

@ -0,0 +1,134 @@
Given /^the following languages are active:$/ do |table|
Setting.available_languages = table.raw.flatten
end
Given /^the (.+) called "(.+)" has the following localizations:$/ do |model_name, object_name, table|
model = model_name.downcase.gsub(/\s/, "_").camelize.constantize
object = model.find_by_name(object_name)
object.translations = []
table.hashes.each do |h|
h.each do |k, v|
h[k] = nil if v == "nil"
end
object.translations.create h
end
end
When /^I delete the (.+) localization of the "(.+)" attribute$/ do |language, attribute|
locale = { "german" => "de", "english" => "en", "french" => "fr" }[language]
attribute_spans = []
wait_until(5) do
attribute_spans = page.all(:css, "span.#{attribute}_translation")
attribute_spans.size > 0
end
attribute_span = attribute_spans.detect do |attribute_span|
attribute_span.find(:css, ".locale_selector")["value"] == locale
end
destroy = attribute_span.find(:css, "a.destroy_locale")
destroy.click
end
When /^I change the (.+) localization of the "(.+)" attribute to be (.+)$/ do |language, attribute, new_language|
attribute_span = span_for_localization language, attribute
locale_selector = attribute_span.find(:css, ".locale_selector")
locale_name = locale_selector.all(:css, "option").detect{ |o| o.value == locale_for_language(new_language) }
locale_selector.select(locale_name.text) if locale_name
end
When /^I add the (.+) localization of the "(.+)" attribute as "(.+)"$/ do |language, attribute, value|
new_elements = span_for_localization language, attribute
unless new_elements.present?
attribute_p = page.find(:xpath, "//span[contains(@class, '#{attribute}_translation')]/..")
add_link = attribute_p.find(:css, ".add_locale")
add_link.click
new_elements = attribute_p.all(:css, ".#{attribute}_translation").last
end
new_value = new_elements.find(:css, "input[type=text], textarea")
new_locale = new_elements.find(:css, ".locale_selector")
new_value.set(value.gsub("\\n", "\n"))
locale_name = new_locale.all(:css, "option").detect{|o| o.value == locale_for_language(language)}
new_locale.select(locale_name.text) if locale_name
end
Then /^there should be the following localizations:$/ do |table|
wait_for_page_load
cleaned_expectation = table.hashes.map do |x|
x.reject{ |k, v| v == "nil" }
end
attributes = []
wait_until(5) do
attributes = page.all(:css, "[name*=\"translations_attributes\"]:not([disabled=disabled])")
attributes.size > 0
end
name_regexp = /\[(\d)+\]\[(\w+)\]$/
attribute_group = attributes.inject({}) do |h, element|
if element['name'] =~ name_regexp
h[$1] ||= []
h[$1] << element
end
h
end
actual_localizations = attribute_group.inject([]) do |a, (k, group)|
a << group.inject({}) do |h, element|
if element['name'] =~ name_regexp
if $2 != "id" and
$2 != "_destroy" and
(element['type'] != 'checkbox' or (element['type'] == 'checkbox' and element.checked?))
h[$2] = element['value']
end
end
h
end
a
end
actual_localizations = actual_localizations.group_by{|e| e["locale"]}.collect{|(k, v)| v.inject({}){|a, x| a.merge(x)} }
actual_localizations.should =~ cleaned_expectation
end
Then /^the delete link for the (.+) localization of the "(.+)" attribute should not be visible$/ do |locale, attribute_name|
attribute_span = span_for_localization locale, attribute_name
attribute_span.find(:css, "a.destroy_locale").should_not be_visible
end
def span_for_localization language, attribute
locale = locale_for_language language
attribute_spans = page.all(:css, "span.#{attribute}_translation")
attribute_spans.detect do |attribute_span|
attribute_span.find(:css, ".locale_selector")["value"] == locale &&
attribute_span.visible?
end
end
def locale_for_language language
{ "german" => "de", "english" => "en", "french" => "fr" }[language]
end

@ -0,0 +1,18 @@
Then /^I should see the "(.+)" image(?: within "([^\"]*)")?$/ do |image, selector|
search_string = selector ? Nokogiri::CSS.xpath_for(selector).first + "//img[contains(@src,\"#{image}\")]" : "//img[contains(@src,\"#{image}\")]"
page.should have_xpath(search_string)
end
Then /^I should not see the "(.+)" image(?: within "([^\"]*)")?$/ do |image, selector|
search_string = selector ? Nokogiri::CSS.xpath_for(selector).first + "//img[contains(@src,\"#{image}\")]" : "//img[contains(@src,\"#{image}\")]"
page.should have_no_xpath(search_string)
end
Then /^I should see an image(?: within "(.+?)")$/ do |selector|
with_scope(selector) do
img = find :css, 'img'
img.should be_present
img.should be_visible
end
end

@ -0,0 +1,67 @@
# TODO: check if this step can be removed as it is plugin specific
Given /^there is a standard project named "([^\"]*)"$/ do |name|
steps %Q{
Given there is 1 project with the following:
| Name | #{name} |
And there is a role "Manager"
And there is a role "Developer"
And there is a role "Designer"
And the role "Manager" may have the following rights:
| view_own_hourly_rate |
| view_hourly_rates |
| view_cost_rates |
| view_own_time_entries |
| view_own_cost_entries |
| view_cost_entries |
| view_time_entries |
And the role "Developer" may have the following rights:
| view_own_hourly_rate |
| view_hourly_rates |
| view_cost_rates |
| view_own_time_entries |
| view_own_cost_entries |
| view_cost_entries |
| view_time_entries |
And the role "Designer" may have the following rights:
| view_own_hourly_rate |
| view_hourly_rates |
| view_cost_rates |
| view_own_time_entries |
| view_own_cost_entries |
| view_cost_entries |
| view_time_entries |
And there is 1 user with:
| Login | manager |
| Firstname | Mac |
| Lastname | Moneysack |
| default rate | 10.00 |
And there is 1 user with:
| Login | developer |
| Firstname | Alan |
| Lastname | Kay |
| default rate | 10.00 |
And there is 1 user with:
| Login | designer |
| Firstname | Tom |
| Lastname | Kelley |
| default rate | 10.00 |
And the user "manager" is a "Manager" in the project "#{name}"
And the user "designer" is a "Designer" in the project "#{name}"
And the user "developer" is a "Developer" in the project "#{name}"
}
end
Then /^[iI] should (not )?see "([^\"]*)" in the overall sum(?:s)?$/ do |negative, sum|
step %Q{I should #{negative}see "#{sum}" within "tr.sum.all"}
end
Then /^[iI] should (not )?see "([^\"]*)" in the grouped sum(?:s)?$/ do |negative, sum|
step %Q{I should #{negative}see "#{sum}" within "tr.sum.grouped"}
end
Then /^[iI] toggle the [oO]ptions fieldset$/ do
page.execute_script <<-JS
f = $$("fieldset").without($("filters")).first();
toggleFieldset($(f).select("legend").first());
JS
end

@ -0,0 +1,9 @@
Given /^the issue "(.*?)" is watched by:$/ do |issue_subject, watchers|
issue = Issue.find(:last, :conditions => {:subject => issue_subject}, :order => :created_on)
watchers.raw.flatten.each {|w| issue.add_watcher User.find_by_login(w)}
issue.save
end
Then /^the issue "(.*?)" should have (\d+) watchers$/ do |issue_subject, watcher_count|
Issue.find_by_subject(issue_subject).watchers.count.should == watcher_count.to_i
end

@ -0,0 +1,10 @@
When /^(?:|I )follow "([^\"]*)"(?: within "([^\"]*)")?$/ do |link, selector|
with_scope(selector) do
click_link(link)
end
end
Then /^the "([^"]*)" link should point to "([^"]*)"$/ do |title, target|
node = page.find(:xpath, "//a[contains(.,'#{title}')]")
node['href'].should == target
end

@ -0,0 +1,11 @@
When /^I toggle the "([^"]+)" submenu$/ do |menu_name|
nodes = all(:css, ".menu_root a[title=\"#{menu_name}\"] .toggler")
# w/o javascript, all menu elements are expanded by default. So the toggler
# might not be present.
nodes.first.click if nodes.present?
end
Then /^there should be no menu item selected$/ do
page.should_not have_css("#main-menu .selected")
end

@ -0,0 +1,13 @@
Given /^the principal "(.+)" is a "(.+)" in the project "(.+)"$/ do |principal_name, role_name, project_identifier|
project = Project.find_by_identifier(project_identifier)
raise "No project with identifier '#{project_identifier}' found" if project.nil?
role = Role.find_by_name(role_name)
raise "No role with name '#{role_name}' found" if role.nil?
principal = InstanceFinder.find(Principal, principal_name)
project.add_member!(principal, role)
end
InstanceFinder.register(Principal, Proc.new{ |name| Principal.first(:conditions => ["lastname = ? OR login = ?", name, name]) })

@ -0,0 +1,78 @@
def filter_user_by_login login
# method to be overridden by plugins
user = User.find_by_login(login)
steps %Q{ When I fill in "principal_search" with "#{user.name}"
And I wait for the AJAX requests to finish }
end
When /^I check the role "(.+?)" for the project member "(.+?)"$/ do |role_name, user_login|
role = Role.find_by_name(role_name)
member = member_for_login user_login
steps %Q{When I check "member_role_ids_#{role.id}" within "#member-#{member.id}"}
end
Then /^the project member "(.+?)" should not be in edit mode$/ do |user_login|
member = member_for_login user_login
page.find("#member-#{member.id}-roles-form").should_not be_visible
end
Then /^the project member "(.+?)" should have the role "(.+?)"$/ do |user_login, role_name|
member = member_for_login user_login
steps %Q{Then I should see "#{role_name}" within "#member-#{member.id}-roles"}
end
When /^I follow the delete link of the project member "(.+?)"$/ do |login_name|
member = member_for_login login_name
steps %Q{When I follow "Delete" within "#member-#{member.id}"}
end
When /^I go to the project member settings of the project(?: called) "(.+?)"$/ do |project_name|
steps %Q{
When I go to the settings page of the project called "#{project_name}"
And I click on "tab-members"
}
end
When /^I add the principal "(.+)" as a member with the roles:$/ do |principal_name, roles_table|
steps %Q{ When I check "#{principal_name}" within "#tab-content-members" }
roles_table.raw.flatten.each do |role_name|
steps %Q{ When I check "#{role_name}" within "#tab-content-members .splitcontentright" }
end
steps %Q{ When I press "Add" within "#tab-content-members .splitcontentright" }
end
Then /^I should see the principal "(.+)" as a member with the roles:$/ do |principal_name, roles_table|
principal = InstanceFinder.find(Principal, principal_name)
steps %Q{ Then I should see "#{principal.name}" within "#tab-content-members .member" }
found_roles = page.find(:xpath, "//tr[contains(concat(' ',normalize-space(@class),' '),' member ')][contains(.,'#{principal.name}')]").find(:css, "td.roles span").text.split(",").map(&:strip)
found_roles.should =~ roles_table.raw.flatten
end
Then /^I should not see the principal "(.+)" as a member$/ do |principal_name|
principal = InstanceFinder.find(Principal, principal_name)
steps %Q{ Then I should not see "#{principal.name}" within "#tab-content-members .member" }
end
When /^I filter for the user "(.+?)"$/ do |login|
filter_user_by_login login
end
def member_for_login principal_name
principal = InstanceFinder.find(Principal, principal_name)
#the assumption here is, that there is only one project
principal.members.first
end

@ -0,0 +1,30 @@
Given /^the rest api is enabled$/ do
Setting.rest_api_enabled = "1"
end
Given /^the following languages are available:$/ do |table|
Setting.available_languages += table.raw.map(&:first)
end
#Given /^the "(.+?)" setting is set to (true|false)$/ do |name, trueish|
# Setting[name.to_sym] = (trueish == "true" ? "1" : "0")
#end
Given /^the "(.+?)" setting is set to (.+)$/ do |name, value|
value = case value
when "true"
"1"
when "false"
"0"
else
value
end
value = value.to_i if Setting.available_settings[name]["format"] == "int"
Setting[name.to_sym] = value
end
Then /^the "(.+?)" setting should be (true|false)$/ do |name, trueish|
Setting.send((name + "?").to_sym).should == (trueish == "true")
end

@ -0,0 +1,11 @@
Then /^there should be a user with the following:$/ do |table|
expected = table.rows_hash
user = User.find_by_login(expected["login"])
user.should_not be_nil
expected.each do |key, value|
user.send(key).should == value
end
end

@ -0,0 +1,384 @@
# IMPORTANT: This file is generated by cucumber-rails - edit at your own peril.
# It is recommended to regenerate this file in the future when you upgrade to a
# newer version of cucumber-rails. Consider adding your own code to a new file
# instead of editing this one. Cucumber will automatically load all features/**/*.rb
# files.
require 'uri'
require File.expand_path(File.join(File.dirname(__FILE__), "..", "support", "paths"))
module WithinHelpers
def with_scope(locator)
locator ? within(locator) { yield } : yield
end
end
World(WithinHelpers)
Given /^(?:|I )am on (.+)$/ do |page_name|
visit path_to(page_name)
disable_warn_unsaved_popup
end
When /^(?:|I )go to (.+)$/ do |page_name|
step %Q{I am on #{page_name}}
end
When /^I click(?: on)? "(.+?)" within "([^\"]*)"$/ do |target, selector|
element = find_lowest_containing_element(target, selector).first
if element.nil?
with_scope(selector) do
element = find_button(target)
end
end
element.click
end
When /^(?:|I )press "([^\"]*)"(?: within "([^\"]*)")?$/ do |button, selector|
with_scope(selector) do
begin
click_button(button)
rescue Capybara::ElementNotFound
page.find(:css, button).click
end
end
end
When /^(?:|I )fill in "([^\"]*)" with "([^\"]*)"(?: within "([^\"]*)")?(?: if plugin "(.+)" is loaded)?$/ do |field, value, selector, plugin_name|
if plugin_name.nil? || Redmine::Plugin.installed?(plugin_name)
with_scope(selector) do
fill_in(field, :with => value)
end
end
end
When /^(?:|I )fill in "([^\"]*)" for "([^\"]*)"(?: within "([^\"]*)")?$/ do |value, field, selector|
with_scope(selector) do
fill_in(field, :with => value)
end
end
# Use this to fill in an entire form with data from a table. Example:
#
# When I fill in the following:
# | Account Number | 5002 |
# | Expiry date | 2009-11-01 |
# | Note | Nice guy |
# | Wants Email? | |
#
# TODO: Add support for checkbox, select og option
# based on naming conventions.
#
When /^(?:|I )fill in the following(?: within "([^\"]*)")?:$/ do |selector, fields|
with_scope(selector) do
fields.rows_hash.each do |name, value|
step %Q{I fill in "#{name}" with "#{value}"}
end
end
end
When /^(?:|I )select "([^\"]*)" from "([^\"]*)"(?: within "([^\"]*)")?$/ do |value, field, selector|
with_scope(selector) do
select(value, :from => field)
end
end
When /^(?:|I )unselect "([^\"]*)" from "([^\"]*)"(?: within "([^\"]*)")?$/ do |value, field, selector|
with_scope(selector) do
unselect(value, :from => field)
end
end
When /^(?:|I )check "([^\"]*)"(?: within "([^\"]*)")?$/ do |field, selector|
with_scope(selector) do
check(field)
end
end
When /^(?:|I )uncheck "([^\"]*)"(?: within "([^\"]*)")?$/ do |field, selector|
with_scope(selector) do
uncheck(field)
end
end
When /^(?:|I )choose "([^\"]*)"(?: within "([^\"]*)")?$/ do |field, selector|
with_scope(selector) do
choose(field)
end
end
When /^(?:|I )attach the file "([^\"]*)" to "([^\"]*)"(?: within "([^\"]*)")?$/ do |path, field, selector|
with_scope(selector) do
attach_file(field, path)
end
end
When /^I wait(?: (\d+) seconds)? for(?: the)? [Aa][Jj][Aa][Xx](?: requests?(?: to finish)?)?$/ do |timeout|
ajax_done = lambda do
page.evaluate_script(%Q{
(function (){
var done = true;
if (window.jQuery) {
if (window.jQuery.active != 0) {
done = false;
}
}
if (window.Prototype && window.Ajax) {
if (window.Ajax.activeRequestCount != 0) {
done = false;
}
}
return done;
}())
}.gsub("\n", ''))
end
timeout = timeout.present? ?
timeout.to_f :
5.0
wait_until(timeout) do
ajax_done.call
end
end
Then /^(?:|I )should see "([^\"]*)"(?: within "([^\"]*)")?$/ do |text, selector|
if defined?(Spec::Rails::Matchers)
begin
wait_until(5) do
elements = find_lowest_containing_element text, selector
elements.length > 0 && elements[-1].visible?
end
rescue Capybara::TimeoutError
fail_msg = "#{text} did not become visible within #{selector} within 5 sec."
unless page.has_content?(text)
fail_msg << " It might not even on the page at all!"
end
fail fail_msg
end
else
with_scope(selector) do
assert page.has_content?(text)
end
end
end
Then /^(?:|I )should see \/([^\/]*)\/(?: within "([^\"]*)")?$/ do |regexp, selector|
regexp = Regexp.new(regexp)
with_scope(selector) do
if defined?(Spec::Rails::Matchers)
page.should have_xpath('//*', :text => regexp)
else
assert page.has_xpath?('//*', :text => regexp)
end
end
end
Then /^(?:|I )should not see "([^\"]*)"(?: within "([^\"]*)")?$/ do |text, selector|
if defined?(Spec::Rails::Matchers)
begin
wait_until(5) do
elements = find_lowest_containing_element text, selector
elements.length == 0 || !elements[-1].visible?
end
rescue Capybara::TimeoutError
fail "#{text} did not become in-visible within #{selector} within 5 sec"
rescue Selenium::WebDriver::Error::ObsoleteElementError
with_selector = selector.present? ? " within \"#{selector}\"" : ""
step %Q{I should not see "#{text}#{with_selector}"}
end
else
with_scope(selector) do
assert page.has_no_content?(text)
end
end
end
Then /^(?:|I )should not see \/([^\/]*)\/(?: within "([^\"]*)")?$/ do |regexp, selector|
regexp = Regexp.new(regexp)
with_scope(selector) do
if defined?(Spec::Rails::Matchers)
page.should have_no_xpath('//*', :text => regexp)
else
assert page.has_no_xpath?('//*', :text => regexp)
end
end
end
Then /^the "([^\"]*)" field(?: within "([^\"]*)")? should contain "([^\"]*)"$/ do |field, selector, value|
with_scope(selector) do
if defined?(Spec::Rails::Matchers)
find_field(field).value.should =~ /#{value}/
else
assert_match(/#{value}/, field_labeled(field).value)
end
end
end
Then /^the "([^\"]*)" field(?: within "([^\"]*)")? should not contain "([^\"]*)"$/ do |field, selector, value|
with_scope(selector) do
if defined?(Spec::Rails::Matchers)
find_field(field).value.should_not =~ /#{value}/
else
assert_no_match(/#{value}/, find_field(field).value)
end
end
end
Then /^there should be a( disabled)? "(.+)" field( visible| invisible)?(?: within "([^\"]*)")?(?: if plugin "(.+)" is loaded)?$/ do |disabled, fieldname, visible, selector, plugin_name|
if plugin_name.nil? || Redmine::Plugin.installed?(plugin_name)
with_scope(selector) do
if defined?(Spec::Rails::Matchers)
find_field(fieldname).should_not be_nil
if visible && visible == " visible"
find_field(fieldname).should be_visible
elsif visible && visible == " invisible"
find_field(fieldname).should_not be_visible
end
if disabled
find_field(fieldname)[:disabled].should == "disabled"
else
find_field(fieldname)[:disabled].should == nil
end
else
assert_not_nil find_field(fieldname)
end
end
end
end
Then /^there should not be a "(.+)" field(?: within "([^\"]*)")?$/ do |fieldname, selector|
with_scope(selector) do
if defined?(Spec::Rails::Matchers)
lambda {find_field(fieldname)}.should raise_error(Capybara::ElementNotFound)
else
assert_nil find_field(fieldname)
end
end
end
Then /^there should be a "(.+)" button$/ do |button_label|
if defined?(Spec::Rails::Matchers)
page.should have_xpath("//input[@value='#{button_label}']")
else
raise NotImplementedError, "Only Matcher implemented"
end
end
Then /^the "([^\"]*)" select(?: within "([^\"]*)")? should have the following options:$/ do |field, selector, option_table|
options_expected = option_table.raw.collect(&:to_s)
with_scope(selector) do
field = find_field(field)
options_actual = field.all('option').collect(&:text)
if defined?(Spec::Rails::Matchers)
options_actual.should =~ options_expected
else
raise NotImplementedError, "Only Matcher implemented"
end
end
end
Then /^the "([^\"]*)" checkbox(?: within "([^\"]*)")? should be checked$/ do |label, selector|
with_scope(selector) do
if defined?(Spec::Rails::Matchers)
find_field(label)['checked'].should be_true
else
assert_equal 'checked', field_labeled(label)['checked']
end
end
end
Then /^the "([^\"]*)" checkbox(?: within "([^\"]*)")? should not be checked$/ do |label, selector|
with_scope(selector) do
if defined?(Spec::Rails::Matchers)
find_field(label)['checked'].should be_false
else
assert_not_equal 'checked', field_labeled(label)['checked']
end
end
end
Then /^(?:|I )should be on (.+)$/ do |page_name|
wait_for_page_load
if defined?(Spec::Rails::Matchers)
URI.parse(current_url).path.should == path_to(page_name)
else
assert_equal path_to(page_name), URI.parse(current_url).path
end
end
Then /^(?:|I )should have the following query string:$/ do |expected_pairs|
actual_params = CGI.parse(URI.parse(current_url).query)
expected_params = Hash[expected_pairs.rows_hash.map{|k,v| [k,[v]]}]
if defined?(Spec::Rails::Matchers)
actual_params.should == expected_params
else
assert_equal expected_params, actual_params
end
end
Then /^show me the page$/ do
save_and_open_page
end
# This needs an active js driver to work properly
Given /^I (accept|dismiss) the alert dialog$/ do |method|
if Capybara.current_driver.to_s.include?("selenium")
page.driver.browser.switch_to.alert.send(method.to_s)
end
end
def find_lowest_containing_element text, selector
elements = []
node_criteria = "[contains(., \"#{text}\") and not(self::script) and not(child::*[contains(., \"#{text}\")])]"
if selector
search_string = Nokogiri::CSS.xpath_for(selector).first + "//*#{node_criteria}"
search_string += " | " + Nokogiri::CSS.xpath_for(selector).first + "#{node_criteria}"
else
search_string = "//*#{node_criteria}"
end
elements = all(:xpath, search_string)
rescue Capybara::TimeoutError, Nokogiri::CSS::SyntaxError
elements
end
def disable_warn_unsaved_popup
# disable WarnLeavingUnsaved function call when testing with selenium as this
# will freeze the server
if defined?(ChiliProject::VERSION::MAJOR) &&
ChiliProject::VERSION::MAJOR > 1 &&
Capybara.current_driver.to_s.include?("selenium")
page.execute_script("window.onbeforeunload = null")
end
end
def wait_for_page_load(seconds = 5)
begin
wait_until(seconds) do
page.has_css?('body')
end
rescue Capybara::TimeoutError
fail "Page did not load within #{seconds} seconds"
end
end

@ -0,0 +1,32 @@
Given /^the project "(.*?)" has (?:1|a) wiki menu item with the following:$/ do |project_name, table|
item = FactoryGirl.build(:wiki_menu_item)
send_table_to_object(item, table)
item.wiki = Project.find_by_name(project_name).wiki
item.save!
end
Given /^the project "(.*?)" has a child wiki page of "(.*?)" with the following:$/ do |project_name, parent_page_title, table|
wiki = Project.find_by_name(project_name).wiki
wikipage = FactoryGirl.build(:wiki_page, :wiki => wiki)
send_table_to_object(wikipage, table)
FactoryGirl.create(:wiki_content, :page => wikipage)
parent_page = WikiPage.find_by_wiki_id_and_title(wiki.id, parent_page_title)
wikipage.parent_id = parent_page.id
wikipage.save!
end
Then /^the table of contents wiki menu item within the "(.*?)" menu item should be selected$/ do |parent_item_name|
parent_item = WikiMenuItem.find_by_title(parent_item_name)
page.should have_css(".#{parent_item.item_class}-toc.selected")
end
Then /^the child page wiki menu item within the "(.*?)" menu item should be selected$/ do |parent_item_name|
parent_item = WikiMenuItem.find_by_title(parent_item_name)
page.should have_css(".#{parent_item.item_class}-new-page.selected")
end

@ -0,0 +1,2 @@
require File.expand_path('../../../test/object_daddy_helpers', __FILE__)
World(ObjectDaddyHelpers)

@ -0,0 +1,240 @@
module NavigationHelpers
# Maps a name to a path. Used by the
#
# When /^I go to (.+)$/ do |page_name|
#
# step definition in web_steps.rb
#
def path_to(page_name)
case page_name
when /the home\s?page/
'/'
when /the [wW]iki [pP]age "([^\"]+)" (?:for|of) the project called "([^\"]+)"$/
wiki_page = Wiki.titleize($1)
project_identifier = $2.gsub("\"", "")
project = Project.find_by_name(project_identifier)
project_identifier = project.identifier.gsub(' ', '%20')
"/projects/#{project_identifier}/wiki/#{wiki_page}"
when /the [cC]ost [rR]eports page (?:of|for) the project called "([^\"]+)" without filters or groups$/
project_identifier = Project.find_by_name($1).identifier.gsub(' ', '%20')
"/projects/#{project_identifier}/cost_reports?set_filter=1"
when /the [cC]ost [rR]eports page (?:of|for) the project called "([^\"]+)"$/
project_identifier = Project.find_by_name($1).identifier.gsub(' ', '%20')
"/projects/#{project_identifier}/cost_reports"
when /the overall [cC]ost [rR]eports page$/
"/cost_reports"
when /the overall [cC]ost [rR]eports page without filters or groups$/
"/cost_reports?set_filter=1"
when /the overall [cC]ost [rR]eports page with standard groups in debug mode$/
"/cost_reports?set_filter=1&groups[columns][]=cost_type_id&groups[rows][]=user_id&debug=1"
when /the overall [cC]ost [rR]eports page with standard groups/
"/cost_reports?set_filter=1&groups[columns][]=cost_type_id&groups[rows][]=user_id"
when /the overall [pP]rojects page/
"/projects"
when /^the (?:(?:overview |home ?))?page (?:for|of) the project(?: called)? "(.+)"$/
project_identifier = $1.gsub("\"", "")
project_identifier = Project.find_by_name(project_identifier).identifier.gsub(' ', '%20')
"/projects/#{project_identifier}"
when /^the activity page of the project(?: called)? "(.+)"$/
project_identifier = $1.gsub("\"", "")
project_identifier = Project.find_by_name(project_identifier).identifier.gsub(' ', '%20')
"/projects/#{project_identifier}/activity"
when /the page (?:for|of) the issue "([^\"]+)"/
issue = Issue.find_by_subject($1)
"/issues/#{issue.id}"
when /the edit page (?:for|of) the issue "([^\"]+)"/
issue = Issue.find_by_subject($1)
"/issues/#{issue.id}/edit"
when /the copy page (?:for|of) the issue "([^\"]+)"/
issue = Issue.find_by_subject($1)
project = issue.project
"/projects/#{project.identifier}/issues/#{issue.id}/copy"
when /the issues? index page (?:for|of) (the)? project(?: called)? (.+)/
project_identifier = $2.gsub("\"", "")
project_identifier = Project.find_by_name(project_identifier).identifier.gsub(' ', '%20')
"/projects/#{project_identifier}/issues"
when /the wiki index page(?: below the (.+) page)? (?:for|of) (?:the)? project(?: called)? (.+)/
parent_page_title, project_identifier = $1, $2
project_identifier.gsub!("\"", "")
project_identifier = Project.find_by_name(project_identifier).identifier.gsub(' ', '%20')
if parent_page_title.present?
parent_page_title.gsub!("\"", "")
"/projects/#{project_identifier}/wiki/#{parent_page_title}/toc"
else
"/projects/#{project_identifier}/wiki/index"
end
when /the wiki new child page below the (.+) page (?:for|of) (?:the)? project(?: called)? (.+)/
parent_page_title, project_identifier = $1, $2
project_identifier.gsub!("\"", "")
parent_page_title.gsub!("\"", "")
project_identifier = Project.find_by_name(project_identifier).identifier.gsub(' ', '%20')
"/projects/#{project_identifier}/wiki/#{parent_page_title}/new"
when /the edit page (?:for |of )(the )?role(?: called)? (.+)/
role_identifier = $2.gsub("\"", "")
role_identifier = Role.find_by_name(role_identifier).id
"/roles/edit/#{role_identifier}"
when /the edit page (?:for |of )(the )?user(?: called)? (.+)/
user_identifier = $2.gsub("\"", "")
user_identifier = User.find_by_login(user_identifier).id
"/users/#{user_identifier}/edit"
when /the show page (?:for |of )(the )?user(?: called)? (.+)/
user_identifier = $2.gsub("\"", "")
user_identifier = User.find_by_login(user_identifier).id
"/users/#{user_identifier}"
when /the index page (?:for|of) users/
"/users"
when /^the global index page (?:for|of) (.+)$/
"/#{$1}"
when /the edit page (?:for |of )the version(?: called) (.+)/
version_name = $1.gsub("\"", "")
version = Version.find_by_name(version_name)
"/versions/edit/#{version.id}"
when /the new page (?:for|of) (.+)/
model = $1.gsub!("\"", "").downcase
"/#{model.pluralize}/new"
when /the edit page (?:for|of) (?:the )?([^\"]+?)(?: called)? "([^\"]+)"$/
model, identifier = $1, $2
identifier.gsub!("\"", "")
model = model.gsub("\"", "").gsub(/\s/, "_")
begin
instance = InstanceFinder.find(model.camelize.constantize, identifier)
rescue NameError
instance = InstanceFinder.find(model.to_sym, identifier)
end
root = RouteMap.route(instance.class)
"#{root}/#{instance.id}/edit"
when /the log\s?in page/
'/login'
when /^the log ?out page$/
'/logout'
when /the (register|registration) page/
'/account/register'
when /the activate registration page for the user called (.+) with (.+)/
name = $1.dup
selection = $2.dup
name.gsub!("\"","")
selection.gsub!("\"","")
u = User.find_by_login(name)
"/account/#{u.id}/activate?#{selection}"
when /the My page/
'/my/page'
when /the [mM]y account page/
'/my/account'
when /^the (administration|admin) page$/
'/admin'
when /the(?: (.+?) tab of the) settings page$/
if $1.nil?
"/settings"
else
"/settings/edit?tab=#{$1}"
end
when /the(?: (.+?) tab of the) settings page (?:of|for) the project "(.+?)"$/
if $1.nil?
"/projects/#{$2}/settings"
else
"/projects/#{$2}/settings/#{$1}"
end
when /the edit page of Announcement/
'/announcements/1/edit'
when /the index page of Roles/
'/roles'
when /the search page/
'/search'
when /the custom fields page/
'/custom_fields'
when /the enumerations page/
'/enumerations'
when /the authentication modes page/
'/auth_sources'
when /the page of the planning element "([^\"]+)" of the project called "([^\"]+)"$/
planning_element_name = $1
project_name = $2
project_identifier = Project.find_by_name(project_name).identifier.gsub(' ', '%20')
planning_element = Timelines::PlanningElement.find_by_name(planning_element_name)
"/timelines/projects/#{project_identifier}/planning_elements/#{planning_element.id}"
when /the (.+) page (?:for|of) the project called "([^\"]+)"$/
project_page = $1
project_identifier = $2.gsub("\"", "")
project_page = project_page.gsub(' ', '').underscore
project_identifier = Project.find_by_name(project_identifier).identifier.gsub(' ', '%20')
"/projects/#{project_identifier}/#{project_page}"
when /^the quick reference for wiki syntax$/
"/help/wiki_syntax"
when /^the detailed wiki syntax help page$/
"/help/wiki_syntax_detailed"
when /^the configuration page of the "(.+)" plugin$/
"/settings/plugin/#{$1}"
when /^the admin page of the group called "([^"]*)"$/
id = Group.find_by_lastname!($1).id
"/groups/#{id}/edit"
# Add more mappings here.
# Here is an example that pulls values out of the Regexp:
#
# when /^(.*)'s profile page$/i
# user_profile_path(User.find_by_login($1))
else
begin
super
rescue
raise "Can't find mapping from \"#{page_name}\" to a path.\n" +
"Now, go and add a mapping in #{__FILE__}"
end
end
end
end
World(NavigationHelpers)

@ -0,0 +1,54 @@
Feature: User deletion
@javascript
Scenario: A user can delete himself if the setting permitts it
Given the "users_deletable_by_self" setting is set to true
And there is 1 user with the following:
| login | bob |
And I am already logged in as "bob"
And I go to the my account page
And I follow "Delete account"
And I press "Delete"
And I accept the alert dialog
Then I should see "Account successfully deleted"
And I should be on the login page
Scenario: A user can not delete himself if the setting forbidds it
Given the "users_deletable_by_self" setting is set to false
And there is 1 user with the following:
| login | bob |
And I am already logged in as "bob"
And I go to the my account page
Then I should not see "Delete account" within "#main-menu"
@javascript
Scenario: An admin can delete other users if the setting permitts it
Given the "users_deletable_by_admins" setting is set to true
And there is 1 user with the following:
| login | bob |
And I am already logged in as "admin"
When I go to the edit page of the user "bob"
And I follow "Delete" within ".contextual"
And I press "Delete"
And I accept the alert dialog
Then I should see "Account successfully deleted"
And I should be on the index page of users
Scenario: An admin can not delete other users if the setting forbidds it
Given the "users_deletable_by_admins" setting is set to false
And there is 1 user with the following:
| login | bob |
And I am already logged in as "admin"
And I go to the edit page of the user "bob"
Then I should not see "Delete" within ".contextual"
Scenario: Deletablilty settings can be set in the users tab of the settings
Given I am already logged in as "admin"
And the "users_deletable_by_admins" setting is set to false
And the "users_deletable_by_self" setting is set to false
And I go to the users tab of the settings page
And I check "settings_users_deletable_by_admins"
And I check "settings_users_deletable_by_self"
And I press "Save"
Then the "users_deletable_by_admins" setting should be true
Then the "users_deletable_by_self" setting should be true

@ -0,0 +1,32 @@
Feature: Wiki menu items
Background:
Given there is 1 project with the following:
| name | Awesome Project |
| identifier | awesome-project |
And there is a role "member"
And the role "member" may have the following rights:
| view_wiki_pages |
| edit_wiki_pages |
And there is 1 user with the following:
| login | bob |
And the user "bob" is a "member" in the project "Awesome Project"
And the project "Awesome Project" has 1 wiki page with the following:
| Title | Wiki |
And the project "Awesome Project" has 1 wiki page with the following:
| Title | Level1 |
And the project "Awesome Project" has a child wiki page of "Level1" with the following:
| Title | Level2 |
And the project "Awesome Project" has a child wiki page of "Level2" with the following:
| Title | Level3 |
And I am already logged in as "bob"
Scenario: Breadcrumb with wiki hierarchy and a different menu item name
Given the project "Awesome Project" has a wiki menu item with the following:
| title | Level3 |
| name | SomethingCompletelyDifferent |
When I go to the wiki page "Level3" for the project called "Awesome Project"
Then I should see "Level1" within ".breadcrumb"
And I should see "Level2" within ".breadcrumb"
And I should not see "Level3" within ".breadcrumb"
And I should see "SomethingCompletelyDifferent" within ".breadcrumb"

@ -0,0 +1,41 @@
Feature: Viewing the wiki index page
Background:
Given there is 1 user with the following:
| login | bob |
And there is a role "member"
And the role "member" may have the following rights:
| view_wiki_pages |
And there is 1 project with the following:
| name | project1 |
| identifier | project1 |
And the user "bob" is a "member" in the project "project1"
And I am already logged in as "bob"
Scenario: Visiting the wiki index page without a related page should show the overall index page and select no menu item
When I go to the wiki index page of the project called "project1"
Then I should see "Index by title" within "#content"
And there should be no menu item selected
Scenario: Visiting the wiki index page with a related page that has the index page option enabled on it's menu item should show the page and select the toc menu entry within the wiki menu item
Given the project "project1" has 1 wiki page with the following:
| title | ParentWikiPage |
And the project "project1" has 1 wiki menu item with the following:
| title | ParentWikiPage |
| index_page | true |
When I go to the wiki index page below the "ParentWikiPage" page of the project called "project1"
Then I should see "Index by title" within "#content"
And the table of contents wiki menu item within the "ParentWikiPage" menu item should be selected
Scenario: Visiting the wiki index page with a related page that has the index page option disabled on it's menu item should show the page and select no menu item
Given the project "project1" has 1 wiki page with the following:
| title | ParentWikiPage |
And the project "project1" has 1 wiki menu item with the following:
| title | ParentWikiPage |
When I go to the wiki index page below the "ParentWikiPage" page of the project called "project1"
Then I should see "Index by title" within "#content"
And there should be no menu item selected

@ -0,0 +1,38 @@
Feature: Viewing the wiki new child page
Background:
Given there is 1 user with the following:
| login | bob |
And there is a role "member"
And the role "member" may have the following rights:
| view_wiki_pages |
| edit_wiki_pages |
And there is 1 project with the following:
| name | project1 |
| identifier | project1 |
And the user "bob" is a "member" in the project "project1"
And I am already logged in as "bob"
Scenario: Visiting the wiki new child page with a parent page that has the new child page option enabled on it's menu item should show the page and select the toc menu entry within the wiki menu item
Given the project "project1" has 1 wiki page with the following:
| title | ParentWikiPage |
And the project "project1" has 1 wiki menu item with the following:
| title | ParentWikiPage |
| new_wiki_page | true |
When I go to the wiki new child page below the "ParentWikiPage" page of the project called "project1"
Then I should see "Create new child page" within "#content"
And the child page wiki menu item within the "ParentWikiPage" menu item should be selected
Scenario: Visiting the wiki new child page with a related page that has the new child page option disabled on it's menu item should show the page and select no menu item
Given the project "project1" has 1 wiki page with the following:
| title | ParentWikiPage |
And the project "project1" has 1 wiki menu item with the following:
| title | ParentWikiPage |
When I go to the wiki new child page below the "ParentWikiPage" page of the project called "project1"
Then I should see "Create new child page" within "#content"
And there should be no menu item selected
Scenario: Visiting the wiki new child page with an invalid parent page
When I go to the wiki new child page below the "InvalidPage" page of the project called "project1"
Then I should see "404" within "#content"
Then I should see "The page you were trying to access doesn't exist or has been removed."

@ -0,0 +1,90 @@
Feature: Wiki menu items
Background:
Given there is 1 project with the following:
| name | Awesome Project |
| identifier | awesome-project |
And there is a role "member"
And the role "member" may have the following rights:
| view_wiki_pages |
| edit_wiki_pages |
| manage_wiki_menu |
And there is 1 user with the following:
| login | bob |
And the user "bob" is a "member" in the project "Awesome Project"
And the project "Awesome Project" has 1 wiki page with the following:
| Title | Wiki |
And the project "Awesome Project" has 1 wiki page with the following:
| Title | AwesomePage |
And I am already logged in as "bob"
@javascript
Scenario: Adding a main menu entry without index and toc links
When I go to the wiki page "AwesomePage" for the project called "Awesome Project"
And I click on "More functions"
And I click on "Configure menu item"
And I fill in "Aioli Wuaärst" for "wiki_menu_item_name"
And I choose "Show as menu item in project navigation"
And I press "Save"
And I should see "Aioli Wuaärst" within "#main-menu"
@javascript
Scenario: Adding a main menu entry with index and toc links
When I go to the wiki page "AwesomePage" for the project called "Awesome Project"
And I click on "More functions"
And I click on "Configure menu item"
And I fill in "Aioli Wuaärst" for "wiki_menu_item_name"
And I choose "Show as menu item in project navigation"
And I check "Show submenu item 'Create new child page'"
And I check "Show submenu item 'Table of Contents'"
And I press "Save"
When I go to the wiki page "AwesomePage" for the project called "Awesome Project"
Then I should see "Aioli Wuaärst" within "#main-menu"
Then I should see "Table of Contents" within "#main-menu"
Then I should see "Create new child page" within "#main-menu"
@javascript
Scenario: Change existing entry
When I go to the wiki page "Wiki" for the project called "Awesome Project"
Then I should see "Table of Contents" within "#main-menu"
Then I should see "Create new child page" within "#main-menu"
When I click on "More functions"
And I click on "Configure menu item"
And I fill in "Wikikiki" for "wiki_menu_item_name"
And I uncheck "Show submenu item 'Table of Contents'"
And I uncheck "Show submenu item 'Create new child page'"
And I press "Save"
When I go to the wiki page "Wiki" for the project called "Awesome Project"
Then I should see "Wikikiki" within "#main-menu"
Then I should not see "Table of Contents" within "#main-menu"
Then I should not see "Create new child page" within "#main-menu"
@javascript
Scenario: Adding a sub menu entry
Given the project "Awesome Project" has a wiki menu item with the following:
| title | SelectMe |
| name | SelectMe |
Given the project "Awesome Project" has a wiki menu item with the following:
| title | AwesomePage |
| name | RichtigGeil |
When I go to the wiki page "Wiki" for the project called "Awesome Project"
When I click on "More functions"
And I click on "Configure menu item"
And I choose "Show as submenu item of"
When I select "SelectMe" from "parent_wiki_menu_item"
When I select "RichtigGeil" from "parent_wiki_menu_item"
And I press "Save"
When I go to the wiki page "Wiki" for the project called "Awesome Project"
Then I should see "Wiki" within ".menu-children"
@javascript
Scenario: Removing a menu item
Given the project "Awesome Project" has a wiki menu item with the following:
| title | DontKillMe |
| name | DontKillMe |
When I go to the wiki page "Wiki" for the project called "Awesome Project"
When I click on "More functions"
And I click on "Configure menu item"
And I choose "Do not show this wikipage in project navigation"
And I press "Save"
Then I should not see "Wiki" within "#main-menu"

@ -0,0 +1,11 @@
class InstanceFinder
def self.register(model, method)
@model_method_map ||= {}
@model_method_map[model] = method
end
def self.find(model, identifier)
instance = @model_method_map[model].call(identifier)
end
end

@ -11,12 +11,11 @@
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'active_support/core_ext/module/attribute_accessors'
require 'redmine/themes/theme'
module Redmine
module Themes
class DefaultThemeNotFoundError < StandardError
end
mattr_accessor :installed_themes
self.installed_themes = Set.new
@ -25,73 +24,51 @@ module Redmine
end
def self.themes
installed_themes
installed_themes.to_a
end
def self.register(*themes)
self.installed_themes += themes.map { |theme| Theme.from(theme) }
self.installed_themes += themes.collect { |theme| Theme.from(theme) }
all
end
def self.theme(name)
find_theme(name) || Theme.default
find_theme(name) || default_theme
end
def self.find_theme(name)
installed_themes.detect { |theme| theme.name.to_s == name.to_s }
theme_to_find = Theme.from(name)
installed_themes.detect { |theme| theme == theme_to_find }
end
extend Enumerable
def self.each(&block)
installed_themes.each(&block)
def self.new_theme(name)
Theme.new name
end
Theme = Struct.new(:name) do
cattr_accessor :default_theme
def self.from(theme)
theme.kind_of?(Theme) ? theme : new(theme)
end
def self.default
self.default_theme ||= from default_theme_name
end
def favicon_path
@favicon_path ||= begin
path = '/favicon.ico'
path = "#{name}#{path}" unless default?
path
end
end
def main_stylesheet_path
name
end
def default?
self == self.class.default
end
def self.default_theme
Theme.default
end
def self.default_theme_name
:default
end
def self.clear
installed_themes.clear
end
include Comparable
def self.each(&block)
installed_themes.each(&block)
end
extend Enumerable
def <=>(other)
name.to_s <=> other.name.to_s
end
def self.register_default_theme
register default_theme
end
self.register Theme.default
self.register_default_theme # called on load
end
end
module ApplicationHelper
def current_theme
@current_theme ||= Redmine::Themes.theme(Setting.ui_theme)
raise DefaultThemeNotFoundError, 'default theme was not found' unless @current_theme
@current_theme
end
end

@ -0,0 +1,42 @@
require 'active_support/core_ext/class/attribute_accessors'
module Redmine
module Themes
class Theme < Struct.new(:name)
cattr_accessor :default_theme, instance_reader: false
def self.from(theme)
theme.kind_of?(Theme) ? theme : new(theme)
end
def self.default
self.default_theme ||= new default_theme_name
end
def favicon_path
@favicon_path ||= default? ? '/favicon.ico' : "/#{name}/favicon.ico"
end
def main_stylesheet_path
name.to_s
end
def default?
self === self.class.default
end
def <=>(other)
name.to_s <=> other.name.to_s
end
include Comparable
def self.default_theme_name
:default
end
def self.forget_default_theme
self.default_theme = nil
end
end
end
end

@ -0,0 +1,25 @@
class ScenarioDisabler
def self.empty_if_disabled(scenario)
if self.disabled?(scenario)
step_collection = scenario.instance_variable_get(:@steps)
step_collection.instance_variable_set(:@steps, [])
true
else
false
end
end
def self.disable(options)
@disabled_scenarios ||= []
@disabled_scenarios << options
end
def self.disabled?(scenario)
@disabled_scenarios.present? && @disabled_scenarios.any? do |disabled_scenario|
disabled_scenario[:feature] == scenario.feature.name && disabled_scenario[:scenario] == scenario.name
end
end
end

@ -0,0 +1,82 @@
require 'spec_helper'
describe CustomFieldsController do
let(:custom_field) { FactoryGirl.stub(:custom_field) }
before do
@controller.stub!(:authorize)
@controller.stub!(:check_if_login_required)
@controller.stub!(:require_admin)
end
describe "POST edit" do
before do
Setting.available_languages = ["de", "en"]
CustomField.stub!(:find).and_return(custom_field)
end
describe "WITH all ok params" do
let(:de_name) { "Ticket Feld" }
let(:en_name) { "Issue Field" }
let(:params) { { "custom_field" => { "translations_attributes" => { "0" => { "name" => de_name, "locale" => "de" }, "1" => { "name" => en_name, "locale" => "en" } } } } }
before do
post :edit, params
end
it { response.should be_success }
it { custom_field.name(:de).should == de_name }
it { custom_field.name(:en).should == en_name }
end
describe "WITH one empty name params" do
let(:en_name) { "Issue Field" }
let(:de_name) { "" }
let(:params) { { "custom_field" => { "translations_attributes" => { "0" => { "name" => de_name, "locale" => "de" }, "1" => { "name" => en_name, "locale" => "en" } } } } }
before do
post :edit, params
end
it { response.should be_success }
it { custom_field.name(:de).should == en_name }
it { custom_field.name(:en).should == en_name }
end
end
describe "POST new" do
before do
Setting.available_languages = ["de", "en"]
end
describe "WITH all ok params" do
let(:de_name) { "Ticket Feld" }
let(:en_name) { "Issue Field" }
let(:params) { { "type" => "IssueCustomField",
"custom_field" => { "translations_attributes" => { "0" => { "name" => de_name, "locale" => "de" }, "1" => { "name" => en_name, "locale" => "en" } } } } }
before do
post :new, params
end
it { response.should be_success }
it { assigns(:custom_field).name(:de).should == de_name }
it { assigns(:custom_field).name(:en).should == en_name }
end
describe "WITH one empty name params" do
let(:en_name) { "Issue Field" }
let(:de_name) { "" }
let(:params) { { "type" => "IssueCustomField",
"custom_field" => { "translations_attributes" => { "0" => { "name" => de_name, "locale" => "de" }, "1" => { "name" => en_name, "locale" => "en" } } } } }
before do
post :new, params
end
it { response.should be_success }
it { assigns(:custom_field).name(:de).should == en_name }
it { assigns(:custom_field).name(:en).should == en_name }
end
end
end

@ -0,0 +1,94 @@
require 'spec_helper'
describe ProjectsController do
before :each do
@controller.stub!(:set_localization)
@role = FactoryGirl.create(:non_member)
@user = FactoryGirl.create(:admin)
User.stub!(:current).and_return @user
@params = {}
end
describe 'show' do
integrate_views
describe 'without wiki' do
before do
@project = FactoryGirl.create(:project)
@project.reload # project contains wiki by default
@project.wiki.destroy
@project.reload
@params[:id] = @project.id
end
it 'renders show' do
get 'show', @params
response.should be_success
response.should render_template 'show'
end
it 'renders main menu without wiki menu item' do
get 'show', @params
response.should have_tag('#main-menu') do
without_tag 'a.Wiki'
end
end
end
describe 'with wiki' do
before do
@project = FactoryGirl.create(:project)
@project.reload # project contains wiki by default
@params[:id] = @project.id
end
describe 'without custom wiki menu items' do
it 'renders show' do
get 'show', @params
response.should be_success
response.should render_template 'show'
end
it 'renders main menu with wiki menu item' do
get 'show', @params
response.should have_tag('#main-menu') do
with_tag 'a.Wiki', :content => 'Wiki'
end
end
end
describe 'with custom wiki menu item' do
before do
main_item = FactoryGirl.create(:wiki_menu_item, :wiki_id => @project.wiki.id, :name => 'Example', :title => 'Example')
sub_item = FactoryGirl.create(:wiki_menu_item, :wiki_id => @project.wiki.id, :name => 'Sub', :title => 'Sub', :parent_id => main_item.id)
end
it 'renders show' do
get 'show', @params
response.should be_success
response.should render_template 'show'
end
it 'renders main menu with wiki menu item' do
get 'show', @params
response.should have_tag('#main-menu') do
with_tag 'a.Example', :content => 'Example'
end
end
it 'renders main menu with sub wiki menu item' do
get 'show', @params
response.should have_tag('#main-menu') do
with_tag 'a.Sub', :content => 'Sub'
end
end
end
end
end
end

@ -0,0 +1,187 @@
require 'spec_helper'
describe UsersController do
let(:user) { FactoryGirl.create(:user) }
let(:admin) { FactoryGirl.create(:admin) }
describe :routes do
describe "users" do
it { params_from(:get, "/users/1/deletion_info").should == { :controller => 'users',
:action => 'deletion_info',
:id => "1" } }
it { params_from(:delete, "/users/1").should == { :controller => 'users',
:action => 'destroy',
:id => "1" } }
end
describe "my" do
before do
User.stub!(:current).and_return(user)
end
it { params_from(:get, "/my/deletion_info").should == { :controller => 'users',
:action => 'deletion_info' } }
end
end
describe "GET deletion_info" do
describe "WHEN the current user is the requested user
WHEN the setting users_deletable_by_self is set to true" do
let(:params) { { "id" => user.id.to_s } }
before do
@controller.stub!(:find_current_user).and_return(user)
Setting.stub!(:users_deletable_by_self?).and_return(true)
get :deletion_info, params
end
it { response.should be_success }
it { assigns(:user).should == user }
it { response.should render_template("deletion_info") }
end
describe "WHEN the current user is the requested user
WHEN the setting users_deletable_by_self is set to false" do
let(:params) { { "id" => user.id.to_s } }
before do
@controller.stub!(:find_current_user).and_return(user)
Setting.stub!(:users_deletable_by_self?).and_return(false)
get :deletion_info, params
end
it { response.response_code.should == 404 }
end
describe "WHEN the current user is the anonymous user" do
let(:params) { { "id" => User.anonymous.id.to_s } }
before do
@controller.stub!(:find_current_user).and_return(User.anonymous)
get :deletion_info, params
end
it { response.should redirect_to({ :controller => 'account',
:action => 'login',
:back_url => @controller.url_for({ :controller => 'users',
:action => 'deletion_info' }) }) }
end
describe "WHEN the current user is admin
WHEN the setting users_deletable_by_admins is set to true" do
let(:admin) { FactoryGirl.create(:admin) }
let(:params) { { "id" => user.id.to_s } }
before do
@controller.stub!(:find_current_user).and_return(admin)
Setting.stub!(:users_deletable_by_admins?).and_return(true)
get :deletion_info, params
end
it { response.should be_success }
it { assigns(:user).should == user }
it { response.should render_template("deletion_info") }
end
describe "WHEN the current user is admin
WHEN the setting users_deletable_by_admins is set to false" do
let(:admin) { FactoryGirl.create(:admin) }
let(:params) { { "id" => user.id.to_s } }
before do
@controller.stub!(:find_current_user).and_return(admin)
Setting.stub!(:users_deletable_by_admins?).and_return(false)
get :deletion_info, params
end
it { response.response_code.should == 404 }
end
end
describe "POST destroy" do
describe "WHEN the current user is the requested one
WHEN the setting users_deletable_by_self is set to true" do
let(:params) { { "id" => user.id.to_s } }
before do
@controller.instance_eval{ flash.stub!(:sweep) }
@controller.stub!(:find_current_user).and_return(user)
Setting.stub!(:users_deletable_by_self?).and_return(true)
post :destroy, params
end
it { response.should redirect_to({ :controller => 'account', :action => 'login' }) }
it { flash[:notice].should == I18n.t('account.deleted') }
end
describe "WHEN the current user is the requested one
WHEN the setting users_deletable_by_self is set to false" do
let(:params) { { "id" => user.id.to_s } }
before do
@controller.instance_eval{ flash.stub!(:sweep) }
@controller.stub!(:find_current_user).and_return(user)
Setting.stub!(:users_deletable_by_self?).and_return(false)
post :destroy, params
end
it { response.response_code.should == 404 }
end
describe "WHEN the current user is the anonymous user
EVEN when the setting login_required is set to false" do
let(:params) { { "id" => User.anonymous.id.to_s } }
before do
@controller.stub!(:find_current_user).and_return(User.anonymous)
Setting.stub!(:login_required?).and_return(false)
post :destroy, params
end
# redirecting post is not possible for now
it { response.response_code.should == 403 }
end
describe "WHEN the current user is the admin
WHEN the setting users_deletable_by_admins is set to true" do
let(:admin) { FactoryGirl.create(:admin) }
let(:params) { { "id" => user.id.to_s } }
before do
@controller.instance_eval{ flash.stub!(:sweep) }
@controller.stub!(:find_current_user).and_return(admin)
Setting.stub!(:users_deletable_by_admins?).and_return(true)
post :destroy, params
end
it { response.should redirect_to({ :controller => 'users', :action => 'index' }) }
it { flash[:notice].should == I18n.t('account.deleted') }
end
describe "WHEN the current user is the admin
WHEN the setting users_deletable_by_admins is set to false" do
let(:admin) { FactoryGirl.create(:admin) }
let(:params) { { "id" => user.id.to_s } }
before do
@controller.instance_eval{ flash.stub!(:sweep) }
@controller.stub!(:find_current_user).and_return(admin)
Setting.stub!(:users_deletable_by_admins).and_return(false)
post :destroy, params
end
it { response.response_code.should == 404 }
end
end
end

@ -0,0 +1,563 @@
require 'spec_helper'
describe WikiController do
describe 'routes' do
it 'should connect GET /projects/:project_id/wiki/new to wiki/new' do
params_from(:get, '/projects/abc/wiki/new').should == {
:controller => 'wiki',
:action => 'new',
:project_id => 'abc'
}
end
it 'should connect GET /projects/:project_id/wiki/:id/new to wiki/new_child' do
params_from(:get, '/projects/abc/wiki/def/new').should == {
:controller => 'wiki',
:action => 'new_child',
:project_id => 'abc',
:id => 'def'
}
end
it 'should connect POST /projects/:project_id/wiki/new to wiki/create' do
params_from(:post, '/projects/abc/wiki/new').should == {
:controller => 'wiki',
:action => 'create',
:project_id => 'abc'
}
end
it 'should contect POST /projects/:project_id/wiki/:id/preview to wiki/preview' do
params_from(:post, '/projects/abc/wiki/def/preview').should == {
:controller => 'wiki',
:action => 'preview',
:project_id => 'abc',
:id => 'def'
}
end
it 'should contect POST /projects/:project_id/wiki/preview to wiki/preview' do
params_from(:post, '/projects/abc/wiki/preview').should == {
:controller => 'wiki',
:action => 'preview',
:project_id => 'abc'
}
end
end
describe 'actions' do
before do
@controller.stub!(:set_localization)
@role = FactoryGirl.create(:non_member)
@user = FactoryGirl.create(:admin)
User.stub!(:current).and_return @user
@project = FactoryGirl.create(:project)
@project.reload # to get the wiki into the proxy
# creating pages
@existing_page = FactoryGirl.create(:wiki_page, :wiki_id => @project.wiki.id,
:title => 'ExisitingPage')
# creating page contents
FactoryGirl.create(:wiki_content, :page_id => @existing_page.id,
:author_id => @user.id)
end
shared_examples_for "a 'new' action" do
it 'assigns @project to the current project' do
get_page
assigns[:project].should == @project
end
it 'assigns @page to a newly created wiki page' do
get_page
assigns[:page].should be_new_record
assigns[:page].should be_kind_of WikiPage
assigns[:page].wiki.should == @project.wiki
end
it 'assigns @content to a newly created wiki content' do
get_page
assigns[:content].should be_new_record
assigns[:content].should be_kind_of WikiContent
assigns[:content].page.should == assigns[:page]
end
it 'renders the new action' do
get_page
response.should render_template 'new'
end
end
describe 'new' do
let(:get_page) { get 'new', :project_id => @project }
it_should_behave_like "a 'new' action"
end
describe 'new_child' do
let(:get_page) { get 'new_child', :project_id => @project, :id => @existing_page.title }
it_should_behave_like "a 'new' action"
it 'sets the parent page for the new page' do
get_page
assigns[:page].parent.should == @existing_page
end
it 'renders 404 if used with an unknown page title' do
get 'new_child', :project_id => @project, :id => "foobar"
response.status.should == "404 Not Found"
end
end
describe 'create' do
describe 'successful action' do
it 'redirects to the show action' do
post 'create',
:project_id => @project,
:page => {:title => "abc"},
:content => {:text => "h1. abc"}
response.should redirect_to :action => 'show', :project_id => @project, :id => 'Abc'
end
it 'saves a new WikiPage with proper content' do
post 'create',
:project_id => @project,
:page => {:title => "abc"},
:content => {:text => "h1. abc"}
page = @project.wiki.pages.find_by_title 'Abc'
page.should_not be_nil
page.title.should == 'Abc'
page.content.text.should == 'h1. abc'
end
end
describe 'unsuccessful action' do
it 'renders "wiki/new"' do
post 'create',
:project_id => @project,
:page => {:title => ""},
:content => {:text => "h1. abc"}
response.should render_template('new')
end
it 'assigns project to work with new template' do
post 'create',
:project_id => @project,
:page => {:title => ""},
:content => {:text => "h1. abc"}
assigns[:project].should == @project
end
it 'assigns wiki to work with new template' do
post 'create',
:project_id => @project,
:page => {:title => ""},
:content => {:text => "h1. abc"}
assigns[:wiki].should == @project.wiki
assigns[:wiki].should_not be_new_record
end
it 'assigns page to work with new template' do
post 'create',
:project_id => @project,
:page => {:title => ""},
:content => {:text => "h1. abc"}
assigns[:page].should be_new_record
assigns[:page].wiki.project.should == @project
assigns[:page].title.should == ""
assigns[:page].should_not be_valid
end
it 'assigns content to work with new template' do
post 'create',
:project_id => @project,
:page => {:title => ""},
:content => {:text => "h1. abc"}
assigns[:content].should be_new_record
assigns[:content].page.wiki.project.should == @project
assigns[:content].text.should == 'h1. abc'
end
end
end
end
describe 'view related stuff' do
integrate_views
before :each do
@controller.stub!(:set_localization)
Setting.stub!(:login_required?).and_return(false)
@role = FactoryGirl.create(:non_member)
@user = FactoryGirl.create(:admin)
@anon = User.anonymous.nil? ? FactoryGirl.create(:anonymous) : User.anonymous
Role.anonymous.update_attributes :name => I18n.t(:default_role_anonymous),
:permissions => [:view_wiki_pages]
User.stub!(:current).and_return @user
@project = FactoryGirl.create(:project)
@project.reload # to get the wiki into the proxy
# creating pages
@page_default = FactoryGirl.create(:wiki_page, :wiki_id => @project.wiki.id,
:title => 'Wiki')
@page_with_content = FactoryGirl.create(:wiki_page, :wiki_id => @project.wiki.id,
:title => 'PagewithContent')
@page_without_content = FactoryGirl.create(:wiki_page, :wiki_id => @project.wiki.id,
:title => 'PagewithoutContent')
@unrelated_page = FactoryGirl.create(:wiki_page, :wiki_id => @project.wiki.id,
:title => 'UnrelatedPage')
# creating page contents
FactoryGirl.create(:wiki_content, :page_id => @page_default.id,
:author_id => @user.id)
FactoryGirl.create(:wiki_content, :page_id => @page_with_content.id,
:author_id => @user.id)
FactoryGirl.create(:wiki_content, :page_id => @unrelated_page.id,
:author_id => @user.id)
# creating some child pages
@children = {}
[@page_with_content].each do |page|
child_page = FactoryGirl.create(:wiki_page, :wiki_id => @project.wiki.id,
:parent_id => page.id,
:title => page.title + " child")
FactoryGirl.create(:wiki_content, :page_id => child_page.id,
:author_id => @user.id)
@children[page] = child_page
end
end
describe '- main menu links' do
before do
@main_menu_item_for_page_with_content = FactoryGirl.create(:wiki_menu_item, :wiki_id => @project.wiki.id,
:name => 'Item for Page with Content',
:title => @page_with_content.title)
@main_menu_item_for_new_wiki_page = FactoryGirl.create(:wiki_menu_item, :wiki_id => @project.wiki.id,
:name => 'Item for new WikiPage',
:title => 'NewWikiPage')
@other_menu_item = FactoryGirl.create(:wiki_menu_item, :wiki_id => @project.wiki.id,
:name => 'Item for other page',
:title => @unrelated_page.title)
end
shared_examples_for 'all wiki menu items' do
it "is inactive, when an unrelated page is shown" do
get 'show', :id => @unrelated_page.title, :project_id => @project.id
response.should be_success
response.should have_exactly_one_selected_menu_item_in(:project_menu)
response.should have_tag('#main-menu') do
with_tag "a.#{@wiki_menu_item.item_class}"
without_tag "a.#{@wiki_menu_item.item_class}.selected"
end
end
it "is inactive, when another wiki menu item's page is shown" do
get 'show', :id => @other_wiki_menu_item.title, :project_id => @project.id
response.should be_success
response.should have_exactly_one_selected_menu_item_in(:project_menu)
response.should have_tag('#main-menu') do
with_tag "a.#{@wiki_menu_item.item_class}"
without_tag "a.#{@wiki_menu_item.item_class}.selected"
end
end
it 'is active, when the given wiki menu item is shown' do
get 'show', :id => @wiki_menu_item.title, :project_id => @project.id
response.should be_success
response.should have_exactly_one_selected_menu_item_in(:project_menu)
response.should have_tag('#main-menu') do
with_tag "a.#{@wiki_menu_item.item_class}.selected"
end
end
end
shared_examples_for 'all existing wiki menu items' do
#TODO: Add tests for new and toc options within menu item
it "is active on parents item, when new page is shown" do
get 'new_child', :id => @wiki_menu_item.title, :project_id => @project.identifier
response.should be_success
response.should have_no_selected_menu_item_in(:project_menu)
response.should have_tag '#main-menu' do
with_tag "a.#{@wiki_menu_item.item_class}"
without_tag "a.#{@wiki_menu_item.item_class}.selected"
end
end
it 'is inactive, when a toc page is shown' do
get 'index', :id => @wiki_menu_item.title, :project_id => @project.id
response.should be_success
response.should have_no_selected_menu_item_in(:project_menu)
response.should have_tag('#main-menu') do
with_tag "a.#{@wiki_menu_item.item_class}"
without_tag "a.#{@wiki_menu_item.item_class}.selected"
end
end
end
shared_examples_for 'all wiki menu items with child pages' do
it 'is active, when the given wiki menu item is an ancestor of the shown page' do
get 'show', :id => @child_page.title, :project_id => @project.id
response.should be_success
response.should have_exactly_one_selected_menu_item_in(:project_menu)
response.should have_tag('#main-menu') do
with_tag "a.#{@wiki_menu_item.item_class}.selected"
end
end
end
describe '- wiki menu item pointing to a saved wiki page' do
before do
@wiki_menu_item = @main_menu_item_for_page_with_content
@other_wiki_menu_item = @other_menu_item
@child_page = @children[@page_with_content]
end
it_should_behave_like 'all wiki menu items'
it_should_behave_like 'all existing wiki menu items'
it_should_behave_like 'all wiki menu items with child pages'
end
describe '- wiki menu item pointing to a new wiki page' do
before do
@wiki_menu_item = @main_menu_item_for_new_wiki_page
@other_wiki_menu_item = @other_menu_item
end
it_should_behave_like 'all wiki menu items'
end
describe '- wiki_menu_item containing special chars only' do
before do
@wiki_menu_item = FactoryGirl.create(:wiki_menu_item, :wiki_id => @project.wiki.id,
:name => '?',
:title => 'Help')
@other_wiki_menu_item = @other_menu_item
end
it_should_behave_like 'all wiki menu items'
end
end
describe '- wiki sidebar' do
include ActionView::Helpers
describe 'configure menu items link' do
describe 'on a show page' do
describe "being authorized to configure menu items" do
it 'is visible' do
get 'show', :project_id => @project.id
response.should be_success
response.should have_tag '#content' do
with_tag "a", "Configure menu item"
end
end
end
describe "being unauthorized to configure menu items" do
before do
User.stub!(:current).and_return @anon
end
it 'is invisible' do
get 'show', :project_id => @project.id
response.should be_success
response.should have_tag '#content' do
without_tag "a", "Configure menu item"
end
end
end
end
end
describe 'new child page link' do
describe 'on an index page' do
describe "being authorized to edit wiki pages" do
it 'is invisible' do
get 'index', :project_id => @project.id
response.should be_success
response.should have_tag '#content' do
without_tag "a", "Create new child page"
end
end
end
describe "being unauthorized to edit wiki pages" do
before do
User.stub!(:current).and_return @anon
end
it 'is invisible' do
get 'index', :project_id => @project.id
response.should be_success
response.should have_tag '#content' do
without_tag "a", "Create new child page"
end
end
end
end
describe 'on a wiki page' do
describe "being authorized to edit wiki pages" do
describe "with a wiki page present" do
it "is visible" do
get 'show', :id => @page_with_content.title, :project_id => @project.identifier
response.should be_success
response.should have_tag '#content' do
with_tag "a[href=#{wiki_new_child_path(:project_id => @project, :id => @page_with_content.title)}]",
"Create new child page"
end
end
end
describe "with no wiki page present" do
it 'is invisible' do
get 'show', :id => 'i-am-a-ghostpage', :project_id => @project.identifier
response.should be_success
response.should have_tag '#content' do
without_tag "a[href=#{wiki_new_child_path(:project_id => @project, :id => 'i-am-a-ghostpage')}]",
"Create new child page"
end
end
end
end
describe "being unauthorized to edit wiki pages" do
before do
User.stub!(:current).and_return @anon
end
it 'is invisible' do
get 'show', :id => @page_with_content.title, :project_id => @project.identifier
response.should be_success
response.should have_tag '#content' do
without_tag "a", "Create new child page"
end
end
end
end
end
describe 'new page link' do
describe 'on an index page' do
describe "being authorized to edit wiki pages" do
it 'is visible' do
get 'index', :project_id => @project.id
response.should be_success
response.should have_tag '.menu_root' do
with_tag "a[href=#{wiki_new_child_path(:project_id => @project, :id => 'Wiki')}]",
"Create new child page"
end
end
end
describe "being unauthorized to edit wiki pages" do
before do
User.stub!(:current).and_return @anon
end
it 'is invisible' do
get 'index', :project_id => @project.id
response.should be_success
response.should have_tag '.menu_root' do
without_tag "a", "Create new child page"
end
end
end
end
describe 'on a wiki page' do
describe "being authorized to edit wiki pages" do
it 'is visible' do
get 'show', :id => @page_with_content.title, :project_id => @project.identifier
response.should be_success
response.should have_tag '.menu_root' do
with_tag "a[href=#{wiki_new_child_path(:project_id => @project, :id => 'Wiki')}]",
"Create new child page"
end
end
end
describe "being unauthorized to edit wiki pages" do
before do
User.stub!(:current).and_return @anon
end
it 'is invisible' do
get 'show', :id => @page_with_content.title, :project_id => @project.identifier
response.should be_success
response.should have_tag '.menu_root' do
without_tag "a", "Create new child page"
end
end
end
end
end
end
end
end

@ -0,0 +1,42 @@
require 'spec_helper'
describe WikiMenuItemsController do
before do
User.destroy_all
Role.destroy_all
@project = FactoryGirl.create(:project)
@project.reload # project contains wiki by default
@params = {}
@params[:project_id] = @project.id
page = FactoryGirl.create(:wiki_page, :wiki => @project.wiki)
@params[:id] = page.title
end
describe 'w/ valid auth' do
it 'renders the edit action' do
admin_user = FactoryGirl.create(:admin)
User.stub!(:current).and_return admin_user
permission_role = FactoryGirl.create(:role, :name => "accessgranted", :permissions => [:manage_wiki_menu])
member = FactoryGirl.create(:member, :principal => admin_user, :user => admin_user, :project => @project, :roles => [permission_role])
get 'edit', @params
response.should be_success
end
end
describe 'w/o valid auth' do
it 'be forbidden' do
User.stub!(:current).and_return FactoryGirl.create(:user)
get 'edit', @params
response.status.should == "403 Forbidden"
end
end
end

@ -0,0 +1,7 @@
FactoryGirl.define do
factory :attachment do
container :factory => :document
sequence(:filename) { |n| "test#{n}.test" }
sequence(:disk_filename) { |n| "test#{n}.test" }
end
end

@ -0,0 +1,8 @@
FactoryGirl.define do
factory :board do
project
sequence(:name) { |n| "Board No. #{n}" }
sequence(:description) { |n| "I am the Board No. #{n}" }
end
end

@ -0,0 +1,8 @@
FactoryGirl.define do
factory :changeset do
sequence(:revision) { |n| "#{n}" }
committed_on Time.now
commit_date Date.today
end
end

@ -0,0 +1,6 @@
FactoryGirl.define do
factory :document_category do
project
sequence(:name) { |n| "I am Category No. #{n}" }
end
end

@ -0,0 +1,8 @@
FactoryGirl.define do
factory :document do
project
category :factory => :document_category
sequence(:description) { |n| "I am a document's description No. #{n}" }
sequence(:title) { |n| "I am the document No. #{n}" }
end
end

@ -0,0 +1,6 @@
FactoryGirl.define do
factory :group do
# groups have lastnames? hmm...
sequence(:lastname) { |g| "Group #{g}" }
end
end

@ -0,0 +1,8 @@
FactoryGirl.define do
factory :issue_category do
project
assigned_to :factory => :user
sequence(:name) { |n| "Issue category #{n}" }
end
end

@ -0,0 +1,16 @@
FactoryGirl.define do
factory :issue do
priority
sequence(:subject) { |n| "Issue No. #{n}" }
description { |i| "Description for '#{i.subject}'" }
tracker :factory => :tracker_feature
author :factory => :user
factory :valid_issue do
after :build do |issue|
issue.project = Factory.build(:valid_project)
issue.tracker = issue.project.trackers.first
end
end
end
end

@ -0,0 +1,27 @@
FactoryGirl.define do
factory :priority, :class => IssuePriority do
sequence(:name) { |i| "Priority #{i}" }
active true
factory :priority_low do
name "Low"
end
factory :priority_normal do
name "Normal"
end
factory :priority_high do
name "High"
end
factory :priority_urgent do
name "Urgent"
end
factory :priority_immediate do
name "Immediate"
end
end
end

@ -0,0 +1,11 @@
FactoryGirl.define do
factory :issue_status do
sequence(:name) { |n| "status #{n}" }
is_closed false
factory :default_issue_status do
is_default true
end
end
end

@ -0,0 +1,18 @@
# Create memberships like this:
#
# project = FactoryGirl.create(:project)
# user = FactoryGirl.create(:user)
# role = FactoryGirl.create(:role, :permissions => [:view_wiki_pages, :edit_wiki_pages])
#
# member = FactoryGirl.create(:member, :user => user, :project => project)
# member.role_ids = [role.id]
# member.save!
#
# It looks like you cannot create member_role models directly.
FactoryGirl.define do
factory :member do
user
project
end
end

@ -0,0 +1,7 @@
FactoryGirl.define do
factory :message do
board
sequence(:content) { |n| "Message content {n}" }
sequence(:subject) { |n| "Message subject {n}" }
end
end

@ -0,0 +1,9 @@
FactoryGirl.define do
factory :news do
sequence(:title) { |n| "News title#{n}" }
sequence(:summary) { |n| "News summary#{n}" }
sequence(:description) { |n| "News description#{n}" }
author :factory => :user
project
end
end

@ -0,0 +1,26 @@
FactoryGirl.define do
factory :project do
sequence(:name) { |n| "My Project No. #{n}" }
sequence(:identifier) { |n| "myproject_no_#{n}" }
enabled_module_names Redmine::AccessControl.available_project_modules
factory :public_project do
is_public true
end
factory :project_with_trackers do
after :build do |project|
project.trackers << FactoryGirl.build(:tracker)
end
after :create do |project|
project.trackers.each { |tracker| tracker.save! }
end
factory :valid_project do
after :build do |project|
project.trackers << FactoryGirl.build(:tracker_with_workflow)
end
end
end
end
end

@ -0,0 +1,17 @@
FactoryGirl.define do
factory :query do
project
user :factory => :user
sequence(:name) { |n| "Query {n}" }
factory :public_query do
is_public true
sequence(:name) { |n| "Public query {n}" }
end
factory :private_query do
is_public false
sequence(:name) { |n| "Private query {n}" }
end
end
end

@ -0,0 +1,7 @@
FactoryGirl.define do
factory :repository, :class => Repository::Filesystem do
url 'file:///tmp/test_repo'
project
end
end

@ -0,0 +1,20 @@
FactoryGirl.define do
factory :role do
permissions []
sequence(:name) { |n| "role_#{n}"}
assignable true
factory :non_member do
name "Non member"
builtin Role::BUILTIN_NON_MEMBER
assignable false
end
factory :anonymous_role do
name "Anonymous"
builtin Role::BUILTIN_ANONYMOUS
assignable false
end
end
end

@ -0,0 +1,7 @@
FactoryGirl.define do
factory :time_entry_activity do
sequence(:name) { |n| "Time Entry Activity No. #{n}" }
end
end

@ -0,0 +1,9 @@
FactoryGirl.define do
factory :time_entry do
project
user
issue
spent_on Date.today
end
end

@ -0,0 +1,35 @@
FactoryGirl.define do
factory :tracker do
sequence(:position) { |p| p }
name { |a| "Tracker No. #{a.position}" }
end
factory :tracker_bug, :class => Tracker do
name "Bug"
is_in_chlog true
position 1
factory :tracker_feature do
name "Feature"
position 2
end
factory :tracker_support do
name "Support"
position 3
end
factory :tracker_task do
name "Task"
position 4
end
factory :tracker_with_workflow do
sequence(:name) { |n| "Tracker #{n}" }
sequence(:position) { |n| n }
after :build do |t|
t.workflows = [FactoryGirl.build(:workflow_with_default_status)]
end
end
end
end

@ -0,0 +1,63 @@
FactoryGirl.define do
factory :custom_field do
name "Custom Field"
regexp ""
is_required "0"
min_length "0"
default_value ""
max_length "0"
editable "1"
possible_values ""
visible "1"
field_format "bool"
factory :user_custom_field do
sequence(:name) { |n| "User Custom Field #{n}" }
factory :boolean_user_custom_field do
name "BooleanUserCustomField"
field_format "bool"
end
factory :integer_user_custom_field do
name "IntegerUserCustomField"
field_format "int"
end
factory :text_user_custom_field do
name "TextUserCustomField"
field_format "text"
end
factory :string_user_custom_field do
name "StringUserCustomField"
field_format "string"
end
factory :float_user_custom_field do
name "FloatUserCustomField"
field_format "float"
end
factory :list_user_custom_field do
name "ListUserCustomField"
field_format "list"
possible_values ["1", "2", "3", "4", "5", "6", "7"]
end
factory :date_user_custom_field do
name "DateUserCustomField"
field_format "date"
end
end
factory :issue_custom_field do
sequence(:name) { |n| "Issue Custom Field #{n}" }
factory :user_issue_custom_field do
field_format "user"
sequence(:name) { |n| "UserIssueCustomField #{n}" }
end
end
end
end

@ -0,0 +1,38 @@
FactoryGirl.define do
factory :user do
firstname 'Bob'
lastname 'Bobbit'
sequence(:login) { |n| "bob#{n}" }
sequence(:mail) {|n| "bob#{n}.bobbit@bob.com" }
password 'admin'
password_confirmation 'admin'
mail_notification(Redmine::VERSION::MAJOR > 0 ? 'all' : true)
language 'en'
status User::STATUS_ACTIVE
admin false
first_login false if User.columns.map(&:name).include? 'first_login'
end
factory :admin, :class => User do
firstname 'Redmine'
lastname 'Admin'
login 'admin'
password 'admin'
password_confirmation 'admin'
mail 'admin@example.com'
admin true
first_login false if User.columns.map(&:name).include? 'first_login'
end
factory :anonymous, :class => AnonymousUser do
lastname "Anonymous"
firstname ""
status User::STATUS_BUILTIN
end
factory :deleted_user do
status User::STATUS_BUILTIN
end
end

@ -0,0 +1,7 @@
FactoryGirl.define do
factory :version do
sequence(:name) { |i| "Version #{i}" }
effective_date Date.today + 14.days
project
end
end

@ -0,0 +1,9 @@
FactoryGirl.define do
factory :wiki_content do
page :factory => :wiki_page
author :factory => :user
text { |a| "h1. #{a.page.title}\n\nPage Content Version #{a.version}." }
end
end

@ -0,0 +1,7 @@
FactoryGirl.define do
factory :wiki do
start_page 'Wiki'
project
end
end

@ -0,0 +1,8 @@
FactoryGirl.define do
factory :wiki_menu_item do
wiki
sequence(:name) {|n| "Item No. #{n}" }
sequence(:title) {|n| "Wiki Title #{n}" }
end
end

@ -0,0 +1,10 @@
FactoryGirl.define do
factory :wiki_page do
wiki
sequence(:title) { |n| "Wiki Page No. #{n}" }
factory :wiki_page_with_content do
content :factory => :wiki_content
end
end
end

@ -0,0 +1,8 @@
FactoryGirl.define do
factory :wiki_redirect do
wiki
title 'Source'
redirects_to 'Target'
end
end

@ -0,0 +1,11 @@
FactoryGirl.define do
factory :workflow do
old_status :factory => :issue_status
new_status :factory => :issue_status
role
factory :workflow_with_default_status do
old_status :factory => :default_issue_status
end
end
end

@ -0,0 +1,92 @@
require 'spec_helper'
describe "Journalized Objects" do
before(:each) do
@tracker ||= FactoryGirl.create(:tracker_feature)
@project ||= FactoryGirl.create(:project_with_trackers)
@current = FactoryGirl.create(:user, :login => "user1", :mail => "user1@users.com")
User.stub!(:current).and_return(@current)
end
it 'should work with issues' do
@status_open ||= FactoryGirl.create(:issue_status, :name => "Open", :is_default => true)
@issue ||= FactoryGirl.create(:issue, :project => @project, :status => @status_open, :tracker => @tracker, :author => @current)
initial_journal = @issue.journals.first
recreated_journal = @issue.recreate_initial_journal!
initial_journal.should be_identical(recreated_journal)
end
it 'should work with news' do
@news ||= FactoryGirl.create(:news, :project => @project, :author => @current, :title => "Test", :summary => "Test", :description => "Test")
initial_journal = @news.journals.first
recreated_journal = @news.recreate_initial_journal!
initial_journal.should be_identical(recreated_journal)
end
it 'should work with wiki content' do
@wiki_content ||= FactoryGirl.create(:wiki_content, :author => @current)
initial_journal = @wiki_content.journals.first
recreated_journal = @wiki_content.recreate_initial_journal!
initial_journal.should be_identical(recreated_journal)
end
it 'should work with messages' do
@message ||= FactoryGirl.create(:message, :content => "Test", :subject => "Test", :author => @current)
initial_journal = @message.journals.first
recreated_journal = @message.recreate_initial_journal!
initial_journal.should be_identical(recreated_journal)
end
it 'should work with time entries' do
@status_open ||= FactoryGirl.create(:issue_status, :name => "Open", :is_default => true)
@issue ||= FactoryGirl.create(:issue, :project => @project, :status => @status_open, :tracker => @tracker, :author => @current)
@time_entry ||= FactoryGirl.create(:time_entry, :issue => @issue, :project => @project, :spent_on => Time.now, :hours => 5, :user => @current, :activity => FactoryGirl.create(:time_entry_activity))
initial_journal = @time_entry.journals.first
recreated_journal = @time_entry.recreate_initial_journal!
initial_journal.should be_identical(recreated_journal)
end
it 'should work with documents' do
@document ||= FactoryGirl.create(:document)
initial_journal = @document.journals.first
recreated_journal = @document.recreate_initial_journal!
initial_journal.should be_identical(recreated_journal)
end
it 'should work with attachments' do
@attachment ||= FactoryGirl.create(:attachment, :container => FactoryGirl.create(:document), :author => @current)
initial_journal = @attachment.journals.first
recreated_journal = @attachment.recreate_initial_journal!
initial_journal.should be_identical(recreated_journal)
end
it 'should work with changesets' do
Setting.enabled_scm = ["Subversion"]
@repository ||= Repository.factory("Subversion", :url => "http://svn.test.com")
@repository.save!
@changeset ||= FactoryGirl.create(:changeset, :committer => @current.login, :repository => @repository)
initial_journal = @changeset.journals.first
recreated_journal = @changeset.recreate_initial_journal!
initial_journal.should be_identical(recreated_journal)
end
end

@ -0,0 +1,107 @@
require 'spec_helper'
module Redmine
module Themes
describe Theme do
before do
Theme.forget_default_theme
end
describe '#initialize' do
it 'stores the name' do
theme = Theme.new(:some_name)
expect(theme.name).to eq :some_name
end
end
describe '#from' do
it 'returns self when given a theme' do
theme = Theme.new(:some_name)
expect(Theme.from(theme)).to eq theme
end
it 'returns a new theme given a name' do
expected = Theme.new(:some_name)
theme = Theme.from(:some_name)
expect(theme).to eq expected
end
end
describe '#default' do
it 'returns the default theme' do
expect(Theme.default).to be_default
end
it 'always returns the same theme' do
expect(Theme.default).to be Theme.default
end
end
describe '#default?' do
it 'returns true if it really is the default theme' do
theme = Theme.new(:some_name)
Theme.stub(:default).and_return(theme)
expect(theme).to be_default
end
it 'returns false if it is not the default theme' do
theme = Theme.new(:some_name)
expect(theme).to_not be_default
end
end
describe '#default_theme_name' do
it 'has a default theme name of :default' do
expect(Theme.default.name).to eq :default
end
it 'defines the name of the default theme' do
Theme.stub(:default_theme_name).and_return(:some_default_name)
expect(Theme.default.name).to eq :some_default_name
end
end
describe '#forget_default_theme' do
it 'will clear the old default theme' do
theme = Theme.default
Theme.forget_default_theme
expect(Theme.default).to_not be theme
end
end
describe '#main_stylesheet_path' do
it 'equals the name of the theme' do
theme = Theme.new(:some_name)
expect(theme.main_stylesheet_path).to eq 'some_name'
end
end
describe '#favicon_path' do
it 'is on the root level for the default theme' do
theme = Theme.default
expect(theme.favicon_path).to eq '/favicon.ico'
end
it "prepends the theme name unless it's the default theme" do
theme = Theme.new(:some_name)
expect(theme.favicon_path).to eq '/some_name/favicon.ico'
end
end
describe '#<=>' do
it "is equal when the names match" do
expect(Theme.new(:some_name)).to eq Theme.new(:some_name)
end
it "is not equal when the names don't match" do
expect(Theme.new(:some_name)).to_not eq Theme.new(:some_other_name)
end
it "doesn't make a difference between strings and symbols" do
expect(Theme.new(:some_name)).to eq Theme.new('some_name')
end
end
end
end
end

@ -0,0 +1,152 @@
require 'spec_helper'
module Redmine
describe Themes do
before do
Themes.clear
end
describe '#new_theme' do
it "returns a new theme" do
expected = Themes::Theme.new(:some_name)
theme = Themes.new_theme :some_name
expect(theme).to eq expected
end
end
describe '#register' do
it "registers themes" do
theme = Themes.new_theme :some_name
Themes.register theme
expect(Themes.themes).to include theme
end
it "registers themes by name" do
expected = Themes.new_theme :some_name
Themes.register :some_name
expect(Themes.themes).to include expected
end
it "registers multiple themes at once" do
theme_1 = Themes.new_theme :some_name
theme_2 = Themes.new_theme :some_other_name
Themes.register :some_name, :some_other_name
expect(Themes.themes).to include theme_1
expect(Themes.themes).to include theme_2
end
it "does not store duplicate themes" do
Themes.register :some_name, :some_name
expect(Themes.themes.size).to eq 1
end
it "returns registered themes" do
expected = Themes.new_theme :some_name
themes = Themes.register :some_name
expect(themes).to eq [expected]
end
end
describe '#themes' do
it "returns all themes as an array" do
expected = Themes.new_theme :some_name
Themes.register :some_name
expect(Themes.themes).to eq [expected]
end
end
describe '#all' do
it "returns all themes as an array (same as #themes)" do
expected = Themes.new_theme :some_name
Themes.register :some_name
expect(Themes.themes).to eq [expected]
end
end
describe '#theme' do
it "returns a theme by name" do
theme = Themes.new_theme :some_name
Themes.register theme
expect(Themes.theme(:some_name)).to eq theme
end
it "returns a default theme if not found" do
theme = Themes.new_theme :some_name
Themes.stub(:default_theme).and_return(theme)
expect(Themes.theme(:some_missing_name)).to be theme
end
end
describe '#find_theme' do
it "returns a theme by name" do
theme = Themes.new_theme :some_name
Themes.register theme
expect(Themes.find_theme(:some_name)).to eq theme
end
it "returns nil if no theme found" do
theme = Themes.new_theme :some_name
Themes.register theme
expect(Themes.find_theme(:some_other_name)).to be_nil
end
end
describe '#clear' do
it "clears out the themes list" do
Themes.register :some_name
Themes.clear
expect(Themes.themes).to be_empty
end
end
describe '#default_theme' do
it 'delagates to Theme class' do
expect(Themes.default_theme).to be Themes::Theme.default
end
it 'defines the default theme' do
theme = Themes.new_theme :some_name
Themes.stub(:default_theme).and_return(theme)
expect(Themes.default_theme).to be theme
end
end
describe '#register_default_theme' do
it "registers the default theme" do
Themes.register_default_theme
expect(Themes.all).to include Themes.default_theme
end
it 'always registers the default theme on load' do
# reload the file because we called Themes.clear in the before block
# maybe better: skip the before block for this example?!
load File.expand_path('../../../lib/redmine/themes.rb', __FILE__)
expect(Themes.all).to include Themes.default_theme
end
end
describe '#each' do
it 'iterates over the registered themes' do
Themes.register :some_name
themes = []
Themes.each { |theme| themes << theme.name }
expect(themes).to eq [:some_name]
end
end
describe '#inject' do
it 'iterates over the registered themes' do
Themes.register :some_name
names = Themes.inject [] { |themes, theme| themes << theme.name }
expect(names).to eq [:some_name]
end
end
end
end

@ -0,0 +1,304 @@
require 'spec_helper.rb'
describe CustomField do
let(:field) { FactoryGirl.build :custom_field }
let(:field2) { FactoryGirl.build :custom_field }
describe :name do
describe "uniqueness" do
describe "WHEN value, locale and type are identical" do
before do
field.name = field2.name = "taken name"
field2.save!
end
it { field.should_not be_valid }
end
describe "WHEN value and locale are identical and type is different" do
before do
field.name = field2.name = "taken name"
field2.save!
field.type = "TestCustomField"
end
it { field.should be_valid }
end
describe "WHEN type and locale are identical and value is different" do
before do
field.name = "new name"
field2.name = "taken name"
field2.save!
end
it { field.should be_valid }
end
describe "WHEN value and type are identical and locale is different" do
before do
I18n.locale = :de
field2.name = "taken_name"
field2.save!
I18n.locale = :en
field.name = "taken_name"
end
it { field.should be_valid }
end
end
describe "localization" do
before do
I18n.locale = :de
field.name = "Feld"
I18n.locale = :en
field.name = "Field"
end
after do
I18n.locale = nil
end
it "should return english name when in locale en" do
I18n.locale = :en
field.name.should == "Field"
end
it "should return german name when in locale de" do
I18n.locale = :de
field.name.should == "Feld"
end
end
end
describe :translations_attributes do
describe "WHEN providing a hash with locale and values" do
before do
field.translations_attributes = [ { "name" => "Feld",
"default_value" => "zwei",
"possible_values" => ["eins", "zwei", "drei"],
"locale" => "de" } ]
end
it { field.should have(1).translations }
it { field.name(:de).should == "Feld" }
it { field.default_value(:de).should == "zwei" }
it { field.possible_values(:de).should == ["eins", "zwei", "drei"] }
end
describe "WHEN providing a hash with only a locale" do
before do
field.translations_attributes = [ { "locale" => "de" } ]
end
it { field.should have(0).translations }
end
describe "WHEN providing a hash with a locale and blank values" do
before do
field.translations_attributes = [ { "name" => "",
"default_value" => "",
"possible_values" => "",
"locale" => "de" } ]
end
it { field.should have(0).translations }
end
describe "WHEN providing a hash with a locale and only one values" do
before do
field.translations_attributes = [ { "name" => "Feld",
"locale" => "de" } ]
end
it { field.should have(1).translations }
it { field.name(:de).should == "Feld" }
end
describe "WHEN providing a hash without a locale but with values" do
before do
field.translations_attributes = [ { "name" => "Feld",
"default_value" => "zwei",
"possible_values" => ["eins", "zwei", "drei"],
"locale" => "" } ]
end
it { field.should have(0).translations }
end
describe "WHEN already having a translation and wishing to delete it" do
before do
I18n.locale = :de
field.name = "Feld"
I18n.locale = :en
field.name = "Field"
field.save
field.reload
field.translations_attributes = [ { "id" => field.translations.first.id.to_s,
"_destroy" => "1" } ]
field.save
end
it { field.should have(1).translation }
end
end
describe :default_value do
describe "localization" do
before do
I18n.locale = :de
field.default_value = "Standard"
I18n.locale = :en
field.default_value = "default"
end
it { field.default_value(:en).should == "default" }
it { field.default_value(:de).should == "Standard" }
end
end
describe :possible_values do
describe "localization" do
before do
I18n.locale = :de
field.possible_values = ["eins", "zwei", "drei"]
I18n.locale = :en
field.possible_values = ["one", "two", "three"]
I18n.locale = :fr
field.possible_values = "un\ndeux\ntrois"
I18n.locale = :de
field.save!
field.reload
end
it { field.possible_values(:en).should == ["one", "two", "three"] }
it { field.possible_values(:de).should == ["eins", "zwei", "drei"] }
it { field.possible_values(:fr).should == ["un", "deux", "trois"] }
end
end
describe :valid? do
describe "WITH a list field
WITH two translations
WITH default_value not included in possible_values in the non current locale translation" do
before do
field.field_format = 'list'
field.translations_attributes = [ { "name" => "Feld",
"default_value" => "vier",
"possible_values" => ["eins", "zwei", "drei"],
"locale" => "de" },
{ "name" => "Field",
"locale" => "en",
"possible_values" => "one\ntwo\nthree\n",
"default_value" => "two" } ]
end
it { field.should_not be_valid }
end
describe "WITH a list field
WITH two translations
WITH default_value included in possible_values" do
before do
field.field_format = 'list'
field.translations_attributes = [ { "name" => "Feld",
"default_value" => "zwei",
"possible_values" => ["eins", "zwei", "drei"],
"locale" => "de" },
{ "name" => "Field",
"locale" => "en",
"possible_values" => "one\ntwo\nthree\n",
"default_value" => "two" } ]
end
it { field.should be_valid }
end
describe "WITH a list field
WITH two translations
WITH default_value not included in possible_values in the current locale translation" do
before do
field.field_format = 'list'
field.translations_attributes = [ { "name" => "Feld",
"default_value" => "zwei",
"possible_values" => ["eins", "zwei", "drei"],
"locale" => "de" },
{ "name" => "Field",
"locale" => "en",
"possible_values" => "one\ntwo\nthree\n",
"default_value" => "four" } ]
end
it { field.should_not be_valid }
end
describe "WITH a list field
WITH two translations
WITH possible_values beeing empty in a fallbacked translation" do
before do
field.field_format = 'list'
field.translations_attributes = [ { "name" => "Feld",
"locale" => "de" },
{ "name" => "Field",
"locale" => "en",
"possible_values" => "one\ntwo\nthree\n",
"default_value" => "two" } ]
end
it { field.should be_valid }
end
describe "WITH a list field
WITH the field beeing required
WITH two translations
WITH neither translation defining a default_value" do
before do
field.field_format = 'list'
field.is_required = true
field.translations_attributes = [ { "name" => "Feld",
"locale" => "de" },
{ "name" => "Field",
"possible_values" => "one\ntwo\nthree\n",
"locale" => "en" } ]
end
it { field.should be_valid }
end
describe "WITH a boolean field
WITH the field beeing required
WITH two translations beeing provided
WITH only one translation specifying a default value" do
before do
field.field_format = 'bool'
field.translations_attributes = { "0" => { "name" => "name_en",
"default_value" => "1",
"locale" => "en" },
"1" => { "name" => "name_es",
"locale" => "es" } }
field.is_required = true
end
it { field.should be_valid }
end
end
end

@ -0,0 +1,82 @@
require 'spec_helper.rb'
describe DeletedUser do
let(:user) { DeletedUser.new }
describe :admin do
it { user.admin.should be_false }
end
describe :logged? do
it { user.should_not be_logged }
end
describe :name do
it { user.name.should == I18n.t('user.deleted') }
end
describe :mail do
it { user.mail.should be_nil }
end
describe :time_zone do
it { user.time_zone.should be_nil }
end
describe :rss_key do
it { user.rss_key.should be_nil }
end
describe :destroy do
it { user.destroy.should be_false }
end
describe :available_custom_fields do
before do
FactoryGirl.create(:user_custom_field)
end
it { user.available_custom_fields.should == [] }
end
describe :create do
describe "WHEN creating a second deleted user" do
let(:u1) { FactoryGirl.build(:deleted_user) }
let(:u2) { FactoryGirl.build(:deleted_user) }
before do
u1.save
u2.save
end
it { u1.should_not be_new_record }
it { u2.should be_new_record }
it { u2.errors[:base].should == 'A DeletedUser already exists.' }
end
end
describe :valid do
describe "WHEN no login, first-, lastname and mail is provided" do
let(:user) { DeletedUser.new }
it { user.should be_valid }
end
end
describe :first do
describe "WHEN a deleted user already exists" do
let(:user) { FactoryGirl.build(:deleted_user) }
before do
user.save!
end
it { DeletedUser.first.should == user }
end
describe "WHEN no deleted user exists" do
it { DeletedUser.first.is_a?(DeletedUser).should be_true }
it { DeletedUser.first.should_not be_new_record }
end
end
end

@ -0,0 +1,83 @@
require 'spec_helper'
describe Issue do
describe 'Acts as journalized' do
before(:each) do
IssueStatus.delete_all
IssuePriority.delete_all
@status_resolved ||= FactoryGirl.create(:issue_status, :name => "Resolved", :is_default => false)
@status_open ||= FactoryGirl.create(:issue_status, :name => "Open", :is_default => true)
@status_rejected ||= FactoryGirl.create(:issue_status, :name => "Rejected", :is_default => false)
@priority_low ||= FactoryGirl.create(:priority_low, :is_default => true)
@priority_high ||= FactoryGirl.create(:priority_high)
@tracker ||= FactoryGirl.create(:tracker_feature)
@project ||= FactoryGirl.create(:project_with_trackers)
@current = FactoryGirl.create(:user, :login => "user1", :mail => "user1@users.com")
User.stub!(:current).and_return(@current)
@user2 = FactoryGirl.create(:user, :login => "user2", :mail => "user2@users.com")
@issue ||= FactoryGirl.create(:issue, :project => @project, :status => @status_open, :tracker => @tracker, :author => @current)
end
describe 'ignore blank to blank transitions' do
it 'should not include the "nil to empty string"-transition' do
@issue.description = nil
@issue.save!
@issue.description = ""
@issue.send(:incremental_journal_changes).should be_empty
end
end
describe 'Acts as journalized recreate initial journal' do
it 'should not include certain attributes' do
recreated_journal = @issue.recreate_initial_journal!
recreated_journal.attributes["changed_data"].include?('rgt').should == false
recreated_journal.attributes["changed_data"].include?('lft').should == false
recreated_journal.attributes["changed_data"].include?('lock_version').should == false
recreated_journal.attributes["changed_data"].include?('updated_at').should == false
recreated_journal.attributes["changed_data"].include?('updated_on').should == false
recreated_journal.attributes["changed_data"].include?('id').should == false
recreated_journal.attributes["changed_data"].include?('type').should == false
recreated_journal.attributes["changed_data"].include?('root_id').should == false
end
it 'should not include useless transitions' do
recreated_journal = @issue.recreate_initial_journal!
recreated_journal.attributes["changed_data"].values.each do |change|
change.first.should_not == change.last
end
end
it 'should not be different from the initially created journal by aaj' do
# Creating four journals total
@issue.status = @status_resolved
@issue.assigned_to = @user2
@issue.save!
@issue.reload
@issue.priority = @priority_high
@issue.save!
@issue.reload
@issue.status = @status_rejected
@issue.priority = @priority_low
@issue.estimated_hours = 3
@issue.remaining_hours = 43 if Redmine::Plugin.all.collect(&:id).include?(:backlogs)
@issue.save!
initial_journal = @issue.journals.first
recreated_journal = @issue.recreate_initial_journal!
initial_journal.should be_identical(recreated_journal)
end
end
end
end

@ -0,0 +1,12 @@
require 'spec_helper'
describe Role do
describe '#by_permission' do
it "returns roles with given permission" do
edit_project_role = FactoryGirl.create :role, :permissions => [:edit_project]
expect( Role.by_permission(:edit_project) ).to eq [edit_project_role]
expect( Role.by_permission(:some_other) ).to eq []
end
end
end

@ -0,0 +1,418 @@
require File.expand_path(File.dirname(__FILE__) + "/../spec_helper.rb")
describe User, 'deletion' do
let(:user) { FactoryGirl.build(:user) }
let(:user2) { FactoryGirl.build(:user) }
let(:project) { FactoryGirl.create(:project_with_trackers) }
let(:role) { FactoryGirl.create(:role) }
let(:member) { FactoryGirl.create(:member, :project => project,
:roles => [role],
:principal => user) }
let(:issue_status) { FactoryGirl.create(:issue_status) }
let(:issue) { FactoryGirl.create(:issue, :tracker => project.trackers.first,
:author => user,
:project => project,
:status => issue_status,
:assigned_to => user) }
let(:issue2) { FactoryGirl.create(:issue, :tracker => project.trackers.first,
:author => user2,
:project => project,
:status => issue_status,
:assigned_to => user2) }
let(:substitute_user) { DeletedUser.first }
before do
# for some reason there seem to be users in the db
User.delete_all
user.save!
user2.save!
end
describe "WHEN there is the user" do
before do
user.destroy
end
it { User.find_by_id(user.id).should be_nil }
end
shared_examples_for "updated journalized associated object" do
before do
User.current = user2
associations.each do |association|
associated_instance.send(association.to_s + "=", user2)
end
associated_instance.save!
User.current = user # in order to have the content journal created by the user
associated_instance.reload
associations.each do |association|
associated_instance.send(association.to_s + "=", user)
end
associated_instance.save!
user.destroy
associated_instance.reload
end
it { associated_class.find_by_id(associated_instance.id).should == associated_instance }
it "should replace the user on all associations" do
associations.each do |association|
associated_instance.send(association).should == substitute_user
end
end
it { associated_instance.journals.first.user.should == user2 }
it "should update first journal changes" do
associations.each do |association|
associated_instance.journals.first.changes[association.to_s + "_id"].last.should == user2.id
end
end
it { associated_instance.journals.last.user.should == substitute_user }
it "should update second journal changes" do
associations.each do |association|
associated_instance.journals.last.changes[association.to_s + "_id"].last.should == substitute_user.id
end
end
end
shared_examples_for "created associated object" do
before do
associations.each do |association|
associated_instance.send(association.to_s + "=", user)
end
associated_instance.save!
user.destroy
associated_instance.reload
end
it { associated_class.find_by_id(associated_instance.id).should == associated_instance }
it "should replace the user on all associations" do
associations.each do |association|
associated_instance.send(association).should == substitute_user
end
end
end
shared_examples_for "created journalized associated object" do
before do
User.current = user # in order to have the content journal created by the user
associations.each do |association|
associated_instance.send(association.to_s + "=", user)
end
associated_instance.save!
User.current = user2
associated_instance.reload
associations.each do |association|
associated_instance.send(association.to_s + "=", user2)
end
associated_instance.save!
user.destroy
associated_instance.reload
end
it { associated_class.find_by_id(associated_instance.id).should == associated_instance }
it "should keep the current user on all associations" do
associations.each do |association|
associated_instance.send(association).should == user2
end
end
it { associated_instance.journals.first.user.should == substitute_user }
it "should update the first journal" do
associations.each do |association|
associated_instance.journals.first.changes[association.to_s + "_id"].last.should == substitute_user.id
end
end
it { associated_instance.journals.last.user.should == user2 }
it "should update the last journal" do
associations.each do |association|
associated_instance.journals.last.changes[association.to_s + "_id"].first.should == substitute_user.id
associated_instance.journals.last.changes[association.to_s + "_id"].last.should == user2.id
end
end
end
describe "WHEN the user has created one attachment" do
let(:associated_instance) { FactoryGirl.build(:attachment) }
let(:associated_class) { Attachment }
let(:associations) { [:author] }
it_should_behave_like "created journalized associated object"
end
describe "WHEN the user has updated one attachment" do
let(:associated_instance) { FactoryGirl.build(:attachment) }
let(:associated_class) { Attachment }
let(:associations) { [:author] }
it_should_behave_like "updated journalized associated object"
end
describe "WHEN the user has an issue created and assigned" do
let(:associated_instance) { FactoryGirl.build(:issue, :tracker => project.trackers.first,
:project => project,
:status => issue_status) }
let(:associated_class) { Issue }
let(:associations) { [:author, :assigned_to] }
it_should_behave_like "created journalized associated object"
end
describe "WHEN the user has an issue updated and assigned" do
let(:associated_instance) { FactoryGirl.build(:issue, :tracker => project.trackers.first,
:project => project,
:status => issue_status) }
let(:associated_class) { Issue }
let(:associations) { [:author, :assigned_to] }
before do
User.current = user2
associated_instance.author = user2
associated_instance.assigned_to = user2
associated_instance.save!
User.current = user # in order to have the content journal created by the user
associated_instance.reload
associated_instance.author = user
associated_instance.assigned_to = user
associated_instance.save!
user.destroy
associated_instance.reload
end
it { associated_class.find_by_id(associated_instance.id).should == associated_instance }
it "should replace the user on all associations" do
associated_instance.author.should == substitute_user
associated_instance.assigned_to.should be_nil
end
it { associated_instance.journals.first.user.should == user2 }
it "should update first journal changes" do
associations.each do |association|
associated_instance.journals.first.changes[association.to_s + "_id"].last.should == user2.id
end
end
it { associated_instance.journals.last.user.should == substitute_user }
it "should update second journal changes" do
associations.each do |association|
associated_instance.journals.last.changes[association.to_s + "_id"].last.should == substitute_user.id
end
end
end
describe "WHEN the user has updated a wiki content" do
let(:associated_instance) { FactoryGirl.build(:wiki_content) }
let(:associated_class) { WikiContent}
let(:associations) { [:author] }
it_should_behave_like "updated journalized associated object"
end
describe "WHEN the user has created a wiki content" do
let(:associated_instance) { FactoryGirl.build(:wiki_content) }
let(:associated_class) { WikiContent }
let(:associations) { [:author] }
it_should_behave_like "created journalized associated object"
end
describe "WHEN the user has created a news" do
let(:associated_instance) { FactoryGirl.build(:news) }
let(:associated_class) { News }
let(:associations) { [:author] }
it_should_behave_like "created journalized associated object"
end
describe "WHEN the user has worked on news" do
let(:associated_instance) { FactoryGirl.build(:news) }
let(:associated_class) { News }
let(:associations) { [:author] }
it_should_behave_like "updated journalized associated object"
end
describe "WHEN the user has created a message" do
let(:associated_instance) { FactoryGirl.build(:message) }
let(:associated_class) { Message }
let(:associations) { [:author] }
it_should_behave_like "created journalized associated object"
end
describe "WHEN the user has worked on message" do
let(:associated_instance) { FactoryGirl.build(:message) }
let(:associated_class) { Message }
let(:associations) { [:author] }
it_should_behave_like "updated journalized associated object"
end
describe "WHEN the user has created a time entry" do
let(:associated_instance) { FactoryGirl.build(:time_entry, :project => project,
:issue => issue,
:hours => 2,
:activity => FactoryGirl.create(:time_entry_activity)) }
let(:associated_class) { TimeEntry }
let(:associations) { [:user] }
it_should_behave_like "created journalized associated object"
end
describe "WHEN the user has worked on time_entry" do
let(:associated_instance) { FactoryGirl.build(:time_entry, :project => project,
:issue => issue,
:hours => 2,
:activity => FactoryGirl.create(:time_entry_activity)) }
let(:associated_class) { TimeEntry }
let(:associations) { [:user] }
it_should_behave_like "updated journalized associated object"
end
describe "WHEN the user has commented" do
let(:news) { FactoryGirl.create(:news, :author => user) }
let(:associated_instance) { Comment.new(:commented => news,
:comments => "lorem") }
let(:associated_class) { Comment }
let(:associations) { [:author] }
it_should_behave_like "created associated object"
end
describe "WHEN the user is a member of a project" do
before do
member #saving
user.destroy
end
it { Member.find_by_id(member.id).should be_nil }
it { Role.find_by_id(role.id).should == role }
it { Project.find_by_id(project.id).should == project }
end
describe "WHEN the user is watching something" do
let(:watched) { FactoryGirl.create(:wiki_content) }
let(:watch) { Watcher.new(:user => user,
:watchable => watched) }
before do
watch.save
user.destroy
end
it { Watcher.find_by_id(watch.id).should be_nil }
end
describe "WHEN the user has a token created" do
let(:token) { Token.new(:user => user,
:action => "feeds",
:value => "loremipsum") }
before do
token.save!
user.destroy
end
it { Token.find_by_id(token.id).should be_nil }
end
describe "WHEN the user has created a private query" do
let(:query) { FactoryGirl.build(:private_query, :user => user) }
before do
query.save!
user.destroy
end
it { Query.find_by_id(query.id).should be_nil }
end
describe "WHEN the user has created a public query" do
let(:associated_instance) { FactoryGirl.build(:public_query) }
let(:associated_class) { Query }
let(:associations) { [:user] }
it_should_behave_like "created associated object"
end
describe "WHEN the user has created a changeset" do
let(:repository) { FactoryGirl.create(:repository) }
let(:associated_instance) { FactoryGirl.build(:changeset, :repository_id => repository.id,
:committer => user.login) }
let(:associated_class) { Changeset }
let(:associations) { [:user] }
before do
Setting.enabled_scm = Setting.enabled_scm << "Filesystem"
end
it_should_behave_like "created journalized associated object"
end
describe "WHEN the user has updated a changeset" do
let(:repository) { FactoryGirl.create(:repository) }
let(:associated_instance) { FactoryGirl.build(:changeset, :repository_id => repository.id,
:committer => user2.login) }
let(:associated_class) { Changeset }
let(:associations) { [:user] }
before do
Setting.enabled_scm = Setting.enabled_scm << "Filesystem"
User.current = user2
associated_instance.user = user2
associated_instance.save!
User.current = user # in order to have the content journal created by the user
associated_instance.reload
associated_instance.user = user
associated_instance.save!
user.destroy
associated_instance.reload
end
it { associated_class.find_by_id(associated_instance.id).should == associated_instance }
it "should replace the user on all associations" do
associated_instance.user.should be_nil
end
it { associated_instance.journals.first.user.should == user2 }
it "should update first journal changes" do
associated_instance.journals.first.changes["user_id"].last.should == user2.id
end
it { associated_instance.journals.last.user.should == substitute_user }
it "should update second journal changes" do
associated_instance.journals.last.changes["user_id"].last.should == substitute_user.id
end
end
describe "WHEN the user is assigned an issue category" do
let(:issue_category) { FactoryGirl.build(:issue_category, :assigned_to => user,
:project => project) }
before do
FactoryGirl.create(:member, :principal => user,
:project => project,
:roles => [role])
issue_category.save!
user.destroy
issue_category.reload
end
it { IssueCategory.find_by_id(issue_category.id).should == issue_category }
it { issue_category.assigned_to.should be_nil }
end
end

@ -0,0 +1,69 @@
require File.expand_path(File.dirname(__FILE__) + "/../spec_helper.rb")
describe User do
let(:user) { FactoryGirl.build(:user) }
let(:project) { FactoryGirl.create(:project_with_trackers) }
let(:role) { FactoryGirl.create(:role) }
let(:member) { FactoryGirl.build(:member, :project => project,
:roles => [role],
:principal => user) }
let(:issue_status) { FactoryGirl.create(:issue_status) }
let(:issue) { FactoryGirl.build(:issue, :tracker => project.trackers.first,
:author => user,
:project => project,
:status => issue_status) }
describe :assigned_issues do
before do
user.save!
end
describe "WHEN the user has an issue assigned" do
before do
member.save!
issue.assigned_to = user
issue.save
end
it { user.assigned_issues.should == [issue] }
end
describe "WHEN the user has no issue assigned" do
before do
member.save
issue.save
end
it { user.assigned_issues.should == [] }
end
end
describe :watches do
before do
user.save!
end
describe "WHEN the user is watching" do
let(:watcher) { Watcher.new(:watchable => issue,
:user => user) }
before do
issue.save
watcher.save
end
it { user.watches.should == [watcher] }
end
describe "WHEN the user isn't watching" do
before do
issue.save
end
it { user.watches.should == [] }
end
end
end

@ -0,0 +1,72 @@
require File.dirname(__FILE__) + '/../spec_helper'
describe WikiMenuItem do
before(:each) do
@project = FactoryGirl.create(:project, :enabled_module_names => %w[activity])
@current = FactoryGirl.create(:user, :login => "user1", :mail => "user1@users.com")
User.stub!(:current).and_return(@current)
end
it 'should create a default wiki menu item when enabling the wiki' do
WikiMenuItem.all.should_not be_any
@project.enabled_modules << EnabledModule.new(:name => 'wiki')
@project.reload
wiki_item = @project.wiki.wiki_menu_items.first
wiki_item.name.should eql 'Wiki'
wiki_item.title.should eql 'Wiki'
wiki_item.options[:index_page].should eql true
wiki_item.options[:new_wiki_page].should eql true
end
it 'should change title when a wikipage is renamed' do
wikipage = FactoryGirl.create(:wiki_page, :title => 'Oldtitle')
menu_item_1 = FactoryGirl.create(:wiki_menu_item, :wiki_id => wikipage.wiki.id,
:name => 'Item 1',
:title => 'Oldtitle')
wikipage.title = 'Newtitle'
wikipage.save
menu_item_1.reload
menu_item_1.title.should == wikipage.title
end
describe 'it should destroy' do
before(:each) do
@project.enabled_modules << EnabledModule.new(:name => 'wiki')
@project.reload
@menu_item_1 = FactoryGirl.create(:wiki_menu_item, :wiki_id => @project.wiki.id,
:name => 'Item 1',
:title => 'Item 1')
@menu_item_2 = FactoryGirl.create(:wiki_menu_item, :wiki_id => @project.wiki.id,
:name => 'Item 2',
:parent_id => @menu_item_1.id,
:title => 'Item 2')
end
it 'all children when deleting the parent' do
@menu_item_1.destroy
expect {WikiMenuItem.find(@menu_item_1.id)}.to raise_error(ActiveRecord::RecordNotFound)
expect {WikiMenuItem.find(@menu_item_2.id)}.to raise_error(ActiveRecord::RecordNotFound)
end
describe 'all items when destroying' do
it 'the associated project' do
@project.destroy
WikiMenuItem.all.should_not be_any
end
it 'the associated wiki' do
@project.wiki.destroy
WikiMenuItem.all.should_not be_any
end
end
end
end

@ -0,0 +1,39 @@
Journal.class_eval do
def identical?(o)
return false unless self.class === o
original = self.attributes
recreated = o.attributes
original.except!("created_at")
original["changed_data"].except!("created_on")
recreated.except!("created_at")
recreated["changed_data"].except!("created_on")
original.identical?(recreated)
end
end
Hash.class_eval do
def identical?(o)
return false unless self.class === o
(o.keys + keys).uniq.all? do |key|
(o[key].identical?(self[key]))
end
end
end
Array.class_eval do
def identical?(o)
return false unless self.class === o
all? do |ea|
(o.any? {|other_each| other_each.identical?(ea) })
end
end
end
Object.class_eval do
def identical?(o)
self == o
end
end

@ -0,0 +1,48 @@
Spec::Matchers.define :have_exactly_one_selected_menu_item_in do |menu|
match do |actual|
failure_message(menu, actual) == nil
end
failure_message_for_should do |actual|
failure_message(menu, actual)
end
description do
"have exactly one selected menu item in #{menu}"
end
failure_message_for_should_not do |actual|
raise "You should not use this matcher for should_not matches"
end
def failure_message(menu, actual)
menu_selector = HTML::Selector.new(selector_for_menu(menu))
menu_item_selector = HTML::Selector.new('a.selected')
html = HTML::Document.new(actual.is_a?(String) ? actual : actual.body)
menu_matches = menu_selector.select(html.root)
if menu_matches.size == 1
menu_item_matches = menu_item_selector.select(menu_matches.first)
if menu_item_matches.size == 0
"Expected to find exactly one selected menu item in #{menu}, but found none."
elsif menu_item_matches.size > 1
"Expected to find exactly one selected menu item in #{menu}, but found #{menu_item_matches.size}."
else
nil
end
else
"Expected to find #{menu.inspect} in document, but didn't."
end
end
def selector_for_menu(menu)
case menu
when :project_menu
'#main-menu'
else
raise ArgumentError, 'Unknown menu identifier'
end
end
end

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save