diff --git a/config/initializers/rack_mini_profiler.rb b/config/initializers/rack_mini_profiler.rb
index 726867ed..7513a43c 100644
--- a/config/initializers/rack_mini_profiler.rb
+++ b/config/initializers/rack_mini_profiler.rb
@@ -1,2 +1,2 @@
Rack::MiniProfiler.config.start_hidden = true
-Rack::MiniProfiler.config.toggle_shortcut = "Alt+Ctrl+P"
\ No newline at end of file
+Rack::MiniProfiler.config.toggle_shortcut = "Alt+Ctrl+P"
diff --git a/spec/controllers/stories_controller_spec.rb b/spec/controllers/stories_controller_spec.rb
index b9b9a28c..5b5499c9 100644
--- a/spec/controllers/stories_controller_spec.rb
+++ b/spec/controllers/stories_controller_spec.rb
@@ -1,130 +1,219 @@
require "rails_helper"
-RSpec.describe StoriesController, type: :controller do
- render_views
-
- let!(:project) { FactoryBot.create(:project) }
+RSpec.describe "managing stories", js: true do
+ let(:user) { FactoryBot.create(:user) }
+ let(:project) { FactoryBot.create(:project) }
let!(:story) { FactoryBot.create(:story, project: project) }
before do
- @request.env["devise.mapping"] = Devise.mappings[:user]
- user = FactoryBot.create(:user)
- sign_in user
+ login_as(user, scope: :user)
end
- describe "#new" do
- it "redirects to the new page" do
- get :new, params: {id: story.id, project_id: project.id}
- expect(response).to render_template :new
- end
+ it "allows me to add a story" do
+ visit project_path(id: project.id)
+ click_link "Add a Story"
+ fill_in "story[title]", with: "As a user, I want to add stories"
+ fill_in "story[description]", with: "This story allows users to add stories."
+ click_button "Create"
+ expect(Story.count).to eq 2
end
- describe "#edit" do
- before do
- get :edit, params: {id: story.id, project_id: project.id}
+ it "allows me to clone a story" do
+ visit project_path(id: project.id)
+ within_story_row(story) do
+ click_button "More actions"
+ click_link "Clone"
end
+ expect(page.find("#story_title").value).to eq story.title
+ expect(page.find("#story_description").value).to eq story.description
+ click_button "Create"
+ expect(Story.count).to eq 2
+ end
- it "redirects to the edit page" do
- expect(response).to render_template :edit
- end
+ it "allows me to edit a story" do
+ visit project_path(id: project.id)
+ click_button "More actions"
+ click_link "Edit"
+ fill_in "story[title]", with: "As a user, I want to edit stories"
+ click_button "Save Changes"
+ expect(page).to have_content "Story updated!"
+ end
+
+ it "allows me to delete a story" do
+ visit project_path(id: project.id)
- it "shows the fields for the story" do
- expect(assigns(:story)).to eq story
+ expect(page).to have_text story.title
+
+ accept_confirm do
+ click_button "More actions"
+ click_link "Delete"
end
+
+ expect(page).not_to have_text story.title
+ expect(Story.count).to eq 0
end
- describe "#create" do
- context "with valid attributes" do
- let(:valid_params) { FactoryBot.attributes_for(:story) }
+ it "does not allow me to bulk delete stories when there are none selected" do
+ visit project_path(id: project.id)
+ expect(page).to have_selector("#bulk_delete[aria-disabled='true']")
+ expect(page).to have_selector("#bulk_delete[disabled]")
+ end
- it "creates a new story" do
- expect {
- post :create, params: {project_id: project.id, story: valid_params}
- }.to change(Story, :count).by(1)
- end
+ it "allows me to bulk delete stories when one or more stories are selected" do
+ visit project_path(id: project.id)
- it "redirects to the project path" do
- post :create, params: {project_id: project.id, story: valid_params}
+ within_story_row(story) { check(option: story.id.to_s) }
+ expect(page).to have_no_selector("#bulk_delete[aria-disabled='true']")
+ expect(page).to have_no_selector("#bulk_delete[disabled]")
+ expect(page).to have_button("Bulk Delete (1 Story)")
+ click_button("Bulk Delete (1 Story)")
+ expect(Story.count).to eq 0
+ end
- expect(response).to redirect_to project_path(project.id)
- end
+ it "shows a preview of the description while typing" do
+ visit project_path(id: project.id)
+ click_link "Add a Story"
+ fill_in "story[title]", with: "As a user, I want to add stories"
+
+ desc = <<~DESC
+ This story allows users to add stories.
+
+ some
+ code
+
+ DESC
+
+ fill_in "story[description]", with: desc
+
+ within(".story_preview .content") do
+ expect(page).to have_selector("p", text: "This story allows users to add stories.")
+ expect(page).to have_selector("pre", text: "some\ncode")
end
- context "with invalid attributes" do
- let(:invalid_attributes) { {title: ""} }
+ click_button "Create"
- before do
- post :create, params: {project_id: project.id, story: invalid_attributes}
- end
+ expect(page).to have_text(project.title)
- it "stays on the new template page" do
- expect(response).to render_template :new
- end
+ story = Story.last
+ within_story_row(story) do
+ click_button "More actions"
+ click_link "Edit"
end
- end
- describe "#destroy" do
- it "deletes the story" do
- expect {
- delete :destroy, params: {id: story.id, project_id: project.id}
- }.to change(Story, :count).by(-1)
+ expect(page).to have_text("Edit Story")
+
+ within(".story_preview .content") do
+ expect(page).to have_selector("p", text: "This story allows users to add stories.")
+ expect(page).to have_selector("pre", text: "some\ncode")
end
end
- describe "#show" do
- before do
- get :show, params: {id: story.id, project_id: project.id}
- end
+ it "can move stories between siblings" do
+ project2 = FactoryBot.create(:project, parent: project)
+ project3 = FactoryBot.create(:project, parent: project)
+ story = FactoryBot.create(:story, project: project2)
- it "redirects to the show page" do
- expect(response).to render_template :show
- end
+ visit project_path(id: project2.id)
+
+ within_story_row(story) do
+ # move to options are hidden
+ expect(page).not_to have_text project3.title
- it "shows the attributes for the right story" do
- expect(assigns(:story)).to eq story
+ click_button "More actions"
+ click_button "Move to"
+
+ # only one option is to move to since there's only one sibling
+ expect(page).to have_selector ".move-story .dropdown > form", count: 1
+
+ # confirm moving the story
+ accept_confirm do
+ click_button project3.title
+ end
end
+
+ expect(page).to have_text "Story moved"
+ expect(page).not_to have_text story.title
+ expect(story.reload.project).to eq project3
end
- describe "#show failure" do
- it "Throws 404 if the project_id doesn't match the story's project" do
- expect {
- get :show, params: {id: story.id, project_id: project.id + 1}
- }.to raise_error(ActionController::RoutingError)
+ # see issue #9 on github
+ it "preserves order of stories when editing" do
+ empty_project = FactoryBot.create(:project)
+ visit project_path(id: empty_project.id)
+ click_link "Add a Story"
+ fill_in "Title", with: "Story 1"
+ fill_in "Description (Markdown)", with: "desc"
+ click_button "Create"
+
+ # check that it adds a position for new stories
+ story1 = Story.last
+ expect(story1.position).to be 1
+
+ click_link "Add a Story"
+ fill_in "Title", with: "Story 2"
+ fill_in "Description (Markdown)", with: "desc"
+ click_button "Create"
+
+ story2 = Story.last
+ expect(story2.position).to be 2
+
+ # check that the order is not broken after edit for stories with no position
+ story1.update_attribute(:position, nil)
+ story2.update_attribute(:position, nil)
+
+ within("#story_#{story1.id}") do
+ click_button "More actions"
+ click_link "Edit"
end
- end
- describe "#update" do
- it "updates the story" do
- put :update, params: {id: story.id,
- project_id: project.id,
- story: {title: "New Story"}}
+ expect(page).to have_text("Edit Story")
+
+ fill_in "Description (Markdown)", with: "desc2"
+
+ click_button "Save Changes"
+
+ expect(page).to have_text("Story updated!")
- expect(story.reload.title).to eq "New Story"
+ within("#stories") do
+ expect(find("tr:nth-child(1)")).to have_text story1.title
+ expect(find("tr:nth-child(2)")).to have_text story2.title
end
- end
- describe "#bulk_destroy" do
- it "deletes multiple stories" do
- ids = [99, 100]
- ids.each do |id|
- FactoryBot.create(:story, id: id, project: project)
- end
+ # check that stories with nil position are listed first
+
+ click_link "Add a Story"
+ fill_in "Title", with: "Story 3"
+ fill_in "Description (Markdown)", with: "desc"
+ click_button "Create"
- expect {
- post :bulk_destroy, params: {ids: ids}, format: :json
- }.to change(Story, :count).by(-2)
+ story3 = Story.last
+ expect(story3.position).to be 1
+
+ within("#stories") do
+ expect(find("tr:nth-child(1)")).to have_text story1.title
+ expect(find("tr:nth-child(2)")).to have_text story2.title
+ expect(find("tr:nth-child(3)")).to have_text story3.title
end
end
- describe "#move" do
- it "does not allow moving stories to non-sibling projects" do
- project2 = FactoryBot.create(:project, parent: project)
- project3 = FactoryBot.create(:project)
- story = FactoryBot.create(:story, project: project2)
+ it "allows sorting stories", js: true do
+ FactoryBot.create(:story, project: project)
+ story3 = FactoryBot.create(:story, project: project)
- put :move, params: {project_id: project2.id, story_id: story.id, to_project: project3.id}
- expect(flash[:error]).to eq "Selected project does not exist or is not a sibling."
- expect(response).to redirect_to project2
- end
+ visit project_path(project)
+
+ first = find(".project-table__cell", text: story.title)
+ last = find(".project-table__cell", text: story3.title)
+
+ # The use expect_any_instance_of is discouraged by the RSpec team, but the drag_to
+ # method is not always consistent on the distance it drags elements and the order
+ # is not always the expected. So I'm using expect_any_instance_of on purpose here.
+ expect_any_instance_of(ProjectsController).to receive(:sort_stories)
+
+ last.drag_to(first, delay: 0, html5: false)
+
+ sleep(1)
+
+ expect(page).not_to have_selector(".project-table.sorting")
end
end
diff --git a/spec/features/projects_manage_spec.rb b/spec/features/projects_manage_spec.rb
index 7aa34e0d..2ac0b7e5 100644
--- a/spec/features/projects_manage_spec.rb
+++ b/spec/features/projects_manage_spec.rb
@@ -21,195 +21,185 @@
expect(Project.count).to eq 1
end
- it "allows me to delete a project", js: false do
- visit project_path(id: project.id)
- click_button "Delete Project"
- expect(Project.count).to eq 0
- end
-
- it "allows me to delete a project" do
- visit project_path(id: project.id)
- accept_confirm do
- click_button "Delete Project"
+ context "when the project is archived" do
+ before do
+ project.toggle_archived!
+ visit project_path(id: project.id)
end
- expect(page).not_to have_content "Delete Project"
- expect(Project.count).to eq 0
- end
-
- it "allows editing the project's title inline" do
- visit project_path(id: project.id)
-
- within ".dashboard-title" do
- click_button project.title
- expect(page).to have_selector("form")
-
- click_button "Cancel"
- expect(page).to have_selector("form", count: 0)
-
- click_button project.title
- expect(page).to have_selector(" form")
-
- fill_in "project_title", with: "New Title"
- click_button "Save"
- expect(page).to have_selector("form", count: 0)
+ it "doesn't allow me to change the project" do
+ expect(page).to have_selector(:link_or_button, "Edit or Delete Project", disabled: true)
+ expect(page).to have_selector(:link_or_button, "Add Sub-Project", disabled: true)
+ expect(page).to have_selector(:link_or_button, "Generate Action Plan", disabled: true)
+ end
- project.reload
- expect(project.title).to eq "New Title"
+ it "allow me to clone the project" do
+ expect(page).to have_selector(:link_or_button, "Clone Project", disabled: false)
end
end
- context "Sub-Projects" do
- it "allows me to add sub projects" do
+ context "when the project is unarchived" do
+ before do
visit project_path(id: project.id)
- click_link "Add Sub-Project"
- fill_in "project[title]", with: "Super Sub Project"
- click_button "Create"
- expect(page).to have_content "Project created!"
- expect(current_path).to eq projects_path
end
- it "lists available sub projects with a link" do
- sub_project_one = FactoryBot.create(:project, title: "Sub-project 1", parent_id: project.id)
- sub_project_two = FactoryBot.create(:project, title: "Sub-project 2", parent_id: project.id)
- project_two = FactoryBot.create(:project)
- visit projects_path
-
- within(".project-card", text: project.title) do
- expect(page).to have_link(project.title)
- expect(page).to_not have_link(project_two.title)
+ it "allows me to edit a project" do
+ click_link "Edit or Delete Project"
+ fill_in "project[title]", with: "New Project"
+ click_button "Save Changes"
+ expect(page).to have_content "Project updated!"
+ end
- expect(page).to have_link(sub_project_one.title)
- expect(page).to have_link(sub_project_two.title)
+ it "allows me to delete a project" do
+ click_link "Edit or Delete Project"
+ accept_confirm do
+ click_link "Delete Project"
end
+ expect(page).not_to have_content "Edit or Delete Project"
+ expect(Project.count).to eq 0
end
- it "allow me to visit sub-projects with a link" do
- sub_project_one = FactoryBot.create(:project, title: "Sub-project 1", parent_id: project.id)
- visit projects_path
+ it "allows me to delete a project when javascript is not enabled", js: false do
+ click_link "Edit or Delete Project"
+ click_link "Delete Project"
+ expect(Project.count).to eq 0
+ end
+
+ context "when the project has subprojects" do
+ it "allows me to add sub projects" do
+ click_link "Add Sub-Project"
+ fill_in "project[title]", with: "Super Sub Project"
+ click_button "Create"
+ expect(page).to have_content "Project created!"
+ expect(current_path).to eq projects_path
+ end
- within(".project-card", text: project.title) do
- expect(page).to have_link(project.title)
- expect(page).to have_link(sub_project_one.title)
+ it "lists available sub projects with a link" do
+ sub_project_one = FactoryBot.create(:project, title: "Sub-project 1", parent_id: project.id)
+ sub_project_two = FactoryBot.create(:project, title: "Sub-project 2", parent_id: project.id)
+ project_two = FactoryBot.create(:project)
+ visit projects_path
- click_link sub_project_one.title
+ within(".project-card", text: project.title) do
+ expect(page).to have_link(project.title)
+ expect(page).to_not have_link(project_two.title)
+
+ expect(page).to have_link(sub_project_one.title)
+ expect(page).to have_link(sub_project_two.title)
+ end
end
- expect(current_path).to eq project_path(sub_project_one)
- end
- end
- context "import & Export" do
- before do
- project.stories.create(title: "php upgrade", description: "quick php upgrade")
- end
+ it "allow me to visit sub-projects with a link" do
+ sub_project_one = FactoryBot.create(:project, title: "Sub-project 1", parent_id: project.id)
+ visit projects_path
- it "allows me to export a CSV", js: false do
- visit project_path(id: project.id)
- find("#import-export").click
+ within(".project-card", text: project.title) do
+ expect(page).to have_link(project.title)
+ expect(page).to have_link(sub_project_one.title)
- click_on "Export"
- expect(page.response_headers["Content-Type"]).to eql "text/csv"
- expect(page.source).to include("php upgrade")
+ click_link sub_project_one.title
+ end
+ expect(current_path).to eq project_path(sub_project_one)
+ end
end
- it "allows me to export a CSV" do
- visit project_path(id: project.id)
- find("#import-export").click
+ context "when importing and exporting" do
+ before do
+ project.stories.create(title: "php upgrade", description: "quick php upgrade")
+ end
- click_on "Export"
- expect(page.source).to include("php upgrade")
- end
+ it "allows me to export a CSV", js: false do
+ find("#import-export").click
- it "allows me to import a CSV" do
- visit project_path(id: project.id)
- find("#import-export").click
- page.attach_file("file", (Rails.root + "spec/fixtures/test.csv").to_s)
- click_on "Import"
- expect(project.stories.count).to be > 1
- expect(project.stories.map(&:title).join).to include("php upgrade")
- expect(page.text).to include("success")
- expect(page.current_path).to eql project_path(project.id)
- end
+ click_on "Export"
+ expect(page.response_headers["Content-Type"]).to eql "text/csv"
+ expect(page.source).to include("php upgrade")
+ end
- it "allows me to update existing stories on import" do
- csv_path = (Rails.root + "tmp/stories.csv").to_s
- story = project.stories.first
- csv_content = "id,title,description,position\n#{story.id},#{story.title},blank!,#{story.position}"
- File.write(csv_path, csv_content)
+ it "allows me to export a CSV" do
+ visit project_path(id: project.id)
+ find("#import-export").click
- story_count = project.stories.count
- visit project_path(id: project.id)
- find("#import-export").click
- page.attach_file("file", csv_path)
- click_on "Import"
- expect(project.stories.count).to be story_count
- expect(project.stories.map(&:description).join).to_not include("quick")
- end
- end
+ click_on "Export"
+ expect(page.source).to include("php upgrade")
+ end
- context "when archiving", js: true do
- it "allows me to archive a project" do
- visit project_path(id: project.id)
- click_link "Archive Project"
- expect(page).to have_content "Unarchive Project"
- expect(page).to have_content "archived"
- expect(project.reload).to be_archived
- end
+ it "allows me to import a CSV" do
+ find("#import-export").click
+ page.attach_file("file", (Rails.root + "spec/fixtures/test.csv").to_s)
+ click_on "Import"
+ expect(project.stories.count).to be > 1
+ expect(project.stories.map(&:title).join).to include("php upgrade")
+ expect(page.text).to include("success")
+ expect(page.current_path).to eql project_path(project.id)
+ end
- it "archives sub projects" do
- sub_project = FactoryBot.create(:project, parent: project)
- visit project_path(id: project.id)
- click_link "Archive Project"
- expect(sub_project.reload).to be_archived
- end
- end
+ it "allows me to update existing stories on import" do
+ csv_path = (Rails.root + "tmp/stories.csv").to_s
+ story = project.stories.first
+ csv_content = "id,title,description,position\n#{story.id},#{story.title},blank!,#{story.position}"
+ File.write(csv_path, csv_content)
+
+ story_count = project.stories.count
+ visit project_path(id: project.id)
+ find("#import-export").click
+ page.attach_file("file", csv_path)
+ click_on "Import"
+ expect(project.stories.count).to be story_count
+ expect(project.stories.map(&:description).join).to_not include("quick")
+ end
- context "when unarchiving", js: true do
- it "allows me to unarchive a project" do
- project.toggle_archived!
- visit project_path(id: project.id)
- click_link "Unarchive Project"
- expect(page).to have_content "Archive Project"
- expect(project.reload).not_to be_archived
- end
- end
+ context "when archiving", js: true do
+ it "allows me to archive a project" do
+ click_link "Archive Project"
+ expect(page).to have_content "Unarchive Project"
+ expect(project.reload).to be_archived
+ end
- context "cloning", js: true do
- it "allows cloning a project" do
- visit project_path(id: project.id)
+ it "archives sub projects" do
+ sub_project = FactoryBot.create(:project, parent: project)
+ visit project_path(id: project.id)
+ click_link "Archive Project"
+ expect(sub_project.reload).to be_archived
+ end
+ end
- click_link "Clone Project"
+ context "cloning", js: true do
+ it "allows cloning a project" do
+ click_link "Clone Project"
- expect(page).to have_text("Clone project #{project.title}")
+ expect(page).to have_text("Clone project #{project.title}")
- fill_in :project_title, with: "Cloned Project"
+ fill_in :project_title, with: "Cloned Project"
- expect {
- click_button "Clone"
- }.to change(Project, :count).by(1)
+ expect {
+ click_button "Clone"
+ }.to change(Project, :count).by(1)
- expect(page).to have_text("Project cloned")
+ expect(page).to have_text("Project cloned")
- last_project = Project.last
- expect(last_project.id).not_to eq(project.id)
- expect(last_project.stories.count).to eq project.stories.count
+ last_project = Project.last
+ expect(last_project.id).not_to eq(project.id)
+ expect(last_project.stories.count).to eq project.stories.count
- expect(page).to have_text(last_project.title)
- end
+ expect(page).to have_text(last_project.title)
+ end
- it "defaults to same parent" do
- sub_project = FactoryBot.create(:project, parent: project)
+ it "defaults to same parent" do
+ sub_project = FactoryBot.create(:project, parent: project)
- # None if the project is a parent
- visit new_clone_project_path(project)
- expect(page).to have_select(:project_parent_id, selected: "None")
+ # None if the project is a parent
+ visit new_clone_project_path(project)
+ expect(page).to have_select(:project_parent_id, selected: "None")
- # The parent if the project is a sub project
- visit new_clone_project_path(sub_project)
- expect(page).to have_select(:project_parent_id, selected: project.title)
+ # The parent if the project is a sub project
+ visit new_clone_project_path(sub_project)
+ expect(page).to have_select(:project_parent_id, selected: project.title)
+ end
+ end
end
- context "picking sub-projects" do
+ context "when picking sub-projects" do
let!(:sub_project1) { FactoryBot.create(:project, parent: project) }
let!(:sub_project2) { FactoryBot.create(:project, parent: project) }
let!(:sub_project3) { FactoryBot.create(:project, parent: project) }
@@ -273,43 +263,57 @@
expect(page).to have_text "Can't clone sub-projects if it's not a parent project"
end
end
- end
- context "hierarchy sidebar" do
- context "with sub projects" do
- let!(:sub_project1) { FactoryBot.create(:project, parent: project) }
- let!(:sub_project2) { FactoryBot.create(:project, parent: project) }
-
- it "renders a sidebar" do
- visit project_path(project)
- within "aside.hierarchy" do
- expect(page).to have_selector("a", text: project.title)
- expect(page).to have_selector("a", text: sub_project1.title)
- expect(page).to have_selector("a", text: sub_project2.title)
+ context "hierarchy sidebar" do
+ context "with sub projects" do
+ let!(:sub_project1) { FactoryBot.create(:project, parent: project) }
+ let!(:sub_project2) { FactoryBot.create(:project, parent: project) }
+
+ it "renders a sidebar" do
+ visit project_path(project)
+ within "aside.hierarchy" do
+ expect(page).to have_selector("a", text: project.title)
+ expect(page).to have_selector("a", text: sub_project1.title)
+ expect(page).to have_selector("a", text: sub_project2.title)
+ end
+
+ visit project_path(sub_project1)
+ within "aside.hierarchy" do
+ expect(page).to have_selector("a", text: project.title)
+ expect(page).to have_selector("a", text: sub_project1.title)
+ expect(page).to have_selector("a", text: sub_project2.title)
+ end
+
+ visit project_path(sub_project2)
+ within "aside.hierarchy" do
+ expect(page).to have_selector("a", text: project.title)
+ expect(page).to have_selector("a", text: sub_project1.title)
+ expect(page).to have_selector("a", text: sub_project2.title)
+ end
end
+ end
- visit project_path(sub_project1)
- within "aside.hierarchy" do
- expect(page).to have_selector("a", text: project.title)
- expect(page).to have_selector("a", text: sub_project1.title)
- expect(page).to have_selector("a", text: sub_project2.title)
- end
+ context "with no sub projects" do
+ it "renders no sidebar" do
+ visit project_path(project)
- visit project_path(sub_project2)
- within "aside.hierarchy" do
- expect(page).to have_selector("a", text: project.title)
- expect(page).to have_selector("a", text: sub_project1.title)
- expect(page).to have_selector("a", text: sub_project2.title)
+ expect(page).not_to have_selector("aside.hierarchy")
end
end
end
+ end
- context "with no sub projects" do
- it "renders no sidebar" do
- visit project_path(project)
+ def download_csv_file(path)
+ visit project_path(id: project.id)
+ find("#import-export").click
+ click_on "Export"
+ csv = adjust_csv_descriptions(page.source)
+ File.write(path, csv)
+ end
- expect(page).not_to have_selector("aside.hierarchy")
- end
- end
+ def adjust_csv_descriptions(csv)
+ CSV.parse(csv, headers: true).each do |row|
+ row["description"] = "blank!"
+ end.to_csv
end
end