diff --git a/app/views/projects/cycle_analytics/_empty_stage.html.haml b/app/views/projects/cycle_analytics/_empty_stage.html.haml
index b200ce22970389db7c5cc48b969adf8686abdf07..c3f95860e92375b46db2a01347c642a36869df2d 100644
--- a/app/views/projects/cycle_analytics/_empty_stage.html.haml
+++ b/app/views/projects/cycle_analytics/_empty_stage.html.haml
@@ -2,6 +2,6 @@
   .empty-stage
     .icon-no-data
       = custom_icon ('icon_no_data')
-    %h4 We don’t have enough data to show this stage.
+    %h4 We don't have enough data to show this stage.
     %p
       {{currentStage.emptyStageText}}
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
index 89e2e162b5bc1335334155c8a8bc34a8ec01380e..78e20f6f68ed96d53dcb0e73629e1a4bb089dae1 100644
--- a/app/views/projects/cycle_analytics/show.html.haml
+++ b/app/views/projects/cycle_analytics/show.html.haml
@@ -27,9 +27,9 @@
       .content-block
         .container-fluid
           .row
-            .col-sm-3.col-xs-12.column{ "v-for" => "item in state.summary" }
-              %h3.header {{item.value}}
-              %p.text {{item.title}}
+            .col-sm-3.col-xs-12.column{"v-for" => "item in state.summary"}
+              %h3.header {{ item.value }}
+              %p.text {{ item.title }}
             .col-sm-3.col-xs-12.column
               .dropdown.inline.js-ca-dropdown
                 %button.dropdown-menu-toggle{ "data-toggle" => "dropdown", :type => "button" }
diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5a55b179435acc1a870e92223ec3bd7b59f1b6e5
--- /dev/null
+++ b/spec/features/cycle_analytics_spec.rb
@@ -0,0 +1,128 @@
+require 'spec_helper'
+
+feature 'Cycle Analytics', feature: true, js: true do
+  include WaitForAjax
+
+  let(:project) { create(:project) }
+  let(:user) { create(:user) }
+  let(:guest) { create(:user) }
+  let(:project) { create(:project) }
+  let(:issue) {  create(:issue, project: project, created_at: 2.days.ago) }
+
+  context 'as an allowed user' do
+    context 'when project is new' do
+      before  do
+        project.team << [user, :master]
+        login_as(user)
+        visit namespace_project_cycle_analytics_path(project.namespace, project)
+        wait_for_ajax
+      end
+
+      it 'shows introductory message' do
+        expect(page).to have_content('Introducing Cycle Analytics')
+      end
+
+      it 'shows active stage with empty message' do
+        expect(page).to have_selector('.stage-nav-item.active', text: 'Issue')
+        expect(page).to have_content("We don't have enough data to show this stage.")
+      end
+    end
+
+    context "when there's cycle analytics data" do
+      before do
+        project.team << [user, :master]
+
+        allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue])
+        create_cycle
+        deploy_master
+
+        login_as(user)
+        visit namespace_project_cycle_analytics_path(project.namespace, project)
+      end
+
+      it 'shows data on each stage' do
+        expect_issue_to_be_present
+
+        click_stage('Plan')
+        expect(find('.stage-events')).to have_content(@merge_request.commits.last.title)
+
+        click_stage('Code')
+        expect_merge_request_to_be_present
+
+        click_stage('Test')
+        expect_build_to_be_present
+
+        click_stage('Review')
+        expect_merge_request_to_be_present
+
+        click_stage('Staging')
+        expect_build_to_be_present
+
+        click_stage('Production')
+        expect_issue_to_be_present
+      end
+    end
+  end
+
+  context "as a guest" do
+    before do
+      project.team << [user, :master]
+      project.team << [guest, :guest]
+
+      allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue])
+      create_cycle
+      deploy_master
+
+      login_as(guest)
+      visit namespace_project_cycle_analytics_path(project.namespace, project)
+      wait_for_ajax
+    end
+
+    it 'needs permissions to see restricted stages' do
+      expect(find('.stage-events')).to have_content(issue.title)
+
+      click_stage('Code')
+      expect(find('.stage-events')).to have_content('You need permission.')
+
+      click_stage('Review')
+      expect(find('.stage-events')).to have_content('You need permission.')
+    end
+  end
+
+  def expect_issue_to_be_present
+    expect(find('.stage-events')).to have_content(issue.title)
+    expect(find('.stage-events')).to have_content(issue.author.name)
+    expect(find('.stage-events')).to have_content("##{issue.iid}")
+  end
+
+  def expect_build_to_be_present
+    expect(find('.stage-events')).to have_content(@build.ref)
+    expect(find('.stage-events')).to have_content(@build.short_sha)
+    expect(find('.stage-events')).to have_content("##{@build.id}")
+  end
+
+  def expect_merge_request_to_be_present
+    expect(find('.stage-events')).to have_content(@merge_request.title)
+    expect(find('.stage-events')).to have_content(@merge_request.author.name)
+    expect(find('.stage-events')).to have_content("!#{@merge_request.iid}")
+  end
+
+  def create_cycle
+    milestone = create(:milestone, project: project)
+    issue.update(milestone: milestone)
+    @merge_request = create_merge_request_closing_issue(issue)
+
+    pipeline = create(:ci_empty_pipeline, status: 'created', project: project, ref: @merge_request.source_branch, sha: @merge_request.source_branch_sha)
+    pipeline.run
+
+    @build = create(:ci_build, pipeline: pipeline, status: :success, author: user)
+
+    merge_merge_requests_closing_issue(issue)
+    ProcessCommitWorker.new.perform(project.id, user.id, @merge_request.commits.last.to_hash)
+  end
+
+  def click_stage(stage_name)
+    find('.stage-nav li', text: stage_name).click
+    wait_for_ajax
+  end
+end