Commit ace01246 authored by Vitali Tatarintev's avatar Vitali Tatarintev

Merge branch '328507-fj-add-security-menu' into 'master'

Add security & compliance menu

See merge request gitlab-org/gitlab!66464
parents eb6f4fd1 12cc8a84
= render_if_exists "layouts/nav/ee/security_link" # EE-specific
= render_if_exists "layouts/nav/ee/push_rules_link" # EE-specific = render_if_exists "layouts/nav/ee/push_rules_link" # EE-specific
- if group_sidebar_link?(:runners) - if group_sidebar_link?(:runners)
......
...@@ -11,5 +11,12 @@ module EE ...@@ -11,5 +11,12 @@ module EE
show_discover_project_security: show_discover_project_security?(project) show_discover_project_security: show_discover_project_security?(project)
}) })
end end
override :group_sidebar_context_data
def group_sidebar_context_data(group, user)
super.merge(
show_discover_group_security: show_discover_group_security?(group)
)
end
end end
end end
# frozen_string_literal: true # frozen_string_literal: true
module Groups::SecurityFeaturesHelper module Groups::SecurityFeaturesHelper
def group_level_security_dashboard_available?(group)
group.licensed_feature_available?(:security_dashboard)
end
def group_level_compliance_dashboard_available?(group) def group_level_compliance_dashboard_available?(group)
group.licensed_feature_available?(:group_level_compliance_dashboard) && group.licensed_feature_available?(:group_level_compliance_dashboard) &&
can?(current_user, :read_group_compliance_dashboard, group) can?(current_user, :read_group_compliance_dashboard, group)
...@@ -20,23 +16,6 @@ module Groups::SecurityFeaturesHelper ...@@ -20,23 +16,6 @@ module Groups::SecurityFeaturesHelper
group.enforced_group_managed_accounts? group.enforced_group_managed_accounts?
end end
def primary_group_level_security_feature_path(group)
if group_level_security_dashboard_available?(group)
group_security_dashboard_path(group)
elsif group_level_compliance_dashboard_available?(group)
group_security_compliance_dashboard_path(group)
elsif group_level_credentials_inventory_available?(group)
group_security_credentials_path(group)
elsif group_level_audit_events_available?(group)
group_audit_events_path(group)
end
end
def group_level_audit_events_available?(group)
group.licensed_feature_available?(:audit_events) &&
can?(current_user, :read_group_audit_events, group)
end
def group_level_security_dashboard_data(group) def group_level_security_dashboard_data(group)
{ {
projects_endpoint: expose_url(api_v4_groups_projects_path(id: group.id)), projects_endpoint: expose_url(api_v4_groups_projects_path(id: group.id)),
......
- main_path = primary_group_level_security_feature_path(@group)
- if main_path.present?
= nav_link(path: %w[dashboard#show vulnerabilities#index compliance_dashboards#show credentials#index audit_events#index]) do
= link_to main_path, data: { qa_selector: 'security_compliance_link' }, class: 'has-sub-items' do
.nav-icon-container
= sprite_icon('shield')
%span.nav-item-name
= _('Security & Compliance')
%ul.sidebar-sub-level-items{ data: { qa_selector: 'group_secure_submenu' } }
= nav_link(path: %w[dashboard#show vulnerabilities#index compliance_dashboards#show credentials#index audit_events#index], html_options: { class: "fly-out-top-item" } ) do
%span.fly-out-top-item-container
%strong.fly-out-top-item-name
= _('Security & Compliance')
%li.divider.fly-out-top-item
- if group_level_security_dashboard_available?(@group)
= nav_link(path: 'dashboard#show') do
= link_to group_security_dashboard_path(@group), title: _('Security Dashboard'), data: { qa_selector: 'security_dashboard_link' } do
%span= _('Security Dashboard')
- if group_level_security_dashboard_available?(@group)
= nav_link(path: 'vulnerabilities#index') do
= link_to group_security_vulnerabilities_path(@group), title: _('Vulnerability Report'), data: { qa_selector: 'vulnerability_report_link' } do
%span= _('Vulnerability Report')
- if group_level_compliance_dashboard_available?(@group)
= nav_link(path: 'compliance_dashboards#show') do
= link_to group_security_compliance_dashboard_path(@group), title: _('Compliance') do
%span= _('Compliance')
- if group_level_credentials_inventory_available?(@group)
= nav_link(path: 'credentials#index') do
= link_to group_security_credentials_path(@group), title: _('Credentials') do
%span= _('Credentials')
- if group_level_audit_events_available?(@group)
= nav_link(path: 'audit_events#index') do
= link_to group_audit_events_path(@group), title: _('Audit Events'), data: { qa_selector: 'audit_events_settings_link' } do
%span= _('Audit Events')
- elsif show_discover_group_security?(@group)
= nav_link(path: group_security_discover_path(@group)) do
= link_to group_security_discover_path(@group) do
.nav-icon-container
= sprite_icon('shield')
%span.nav-item-name
= _('Security')
...@@ -12,6 +12,7 @@ module EE ...@@ -12,6 +12,7 @@ module EE
insert_menu_before(::Sidebars::Groups::Menus::GroupInformationMenu, ::Sidebars::Groups::Menus::TrialExperimentMenu.new(context)) insert_menu_before(::Sidebars::Groups::Menus::GroupInformationMenu, ::Sidebars::Groups::Menus::TrialExperimentMenu.new(context))
insert_menu_after(::Sidebars::Groups::Menus::GroupInformationMenu, ::Sidebars::Groups::Menus::EpicsMenu.new(context)) insert_menu_after(::Sidebars::Groups::Menus::GroupInformationMenu, ::Sidebars::Groups::Menus::EpicsMenu.new(context))
insert_menu_after(::Sidebars::Groups::Menus::MergeRequestsMenu, ::Sidebars::Groups::Menus::SecurityComplianceMenu.new(context))
end end
end end
end end
......
# frozen_string_literal: true
module Sidebars
module Groups
module Menus
class SecurityComplianceMenu < ::Sidebars::Menu
override :configure_menu_items
def configure_menu_items
add_item(security_dashboard_menu_item)
add_item(vulnerability_report_menu_item)
add_item(compliance_menu_item)
add_item(credentials_menu_item)
add_item(audit_events_menu_item)
true
end
override :link
def link
return renderable_items.first.link if renderable_items.any?
group_security_discover_path(context.group)
end
override :title
def title
renderable_items.any? ? _('Security & Compliance') : _('Security')
end
override :sprite_icon
def sprite_icon
'shield'
end
override :render?
def render?
super || context.show_discover_group_security
end
override :active_routes
def active_routes
return {} if renderable_items.empty?
{ page: link }
end
private
def security_dashboard_menu_item
unless context.group.licensed_feature_available?(:security_dashboard)
return ::Sidebars::NilMenuItem.new(item_id: :security_dashboard)
end
::Sidebars::MenuItem.new(
title: _('Security Dashboard'),
link: group_security_dashboard_path(context.group),
active_routes: { path: 'dashboard#show' },
item_id: :security_dashboard
)
end
def vulnerability_report_menu_item
unless context.group.licensed_feature_available?(:security_dashboard)
return ::Sidebars::NilMenuItem.new(item_id: :vulnerability_report)
end
::Sidebars::MenuItem.new(
title: _('Vulnerability Report'),
link: group_security_vulnerabilities_path(context.group),
active_routes: { path: 'vulnerabilities#index' },
item_id: :vulnerability_report
)
end
def compliance_menu_item
unless group_level_compliance_dashboard_available?
return ::Sidebars::NilMenuItem.new(item_id: :compliance)
end
::Sidebars::MenuItem.new(
title: _('Compliance'),
link: group_security_compliance_dashboard_path(context.group),
active_routes: { path: 'compliance_dashboards#show' },
item_id: :compliance
)
end
def group_level_compliance_dashboard_available?
context.group.licensed_feature_available?(:group_level_compliance_dashboard) &&
can?(context.current_user, :read_group_compliance_dashboard, context.group)
end
def credentials_menu_item
unless group_level_credentials_inventory_available?
return ::Sidebars::NilMenuItem.new(item_id: :credentials)
end
::Sidebars::MenuItem.new(
title: _('Credentials'),
link: group_security_credentials_path(context.group),
active_routes: { path: 'credentials#index' },
item_id: :credentials
)
end
def group_level_credentials_inventory_available?
context.group.licensed_feature_available?(:credentials_inventory) &&
can?(context.current_user, :read_group_credentials_inventory, context.group) &&
context.group.enforced_group_managed_accounts?
end
def audit_events_menu_item
unless group_level_audit_events_available?
return ::Sidebars::NilMenuItem.new(item_id: :audit_events)
end
::Sidebars::MenuItem.new(
title: _('Audit Events'),
link: group_audit_events_path(context.group),
active_routes: { path: 'audit_events#index' },
item_id: :audit_events
)
end
def group_level_audit_events_available?
context.group.licensed_feature_available?(:audit_events) &&
can?(context.current_user, :read_group_audit_events, context.group)
end
end
end
end
end
...@@ -36,7 +36,7 @@ RSpec.describe 'Groups > Audit Events', :js do ...@@ -36,7 +36,7 @@ RSpec.describe 'Groups > Audit Events', :js do
end end
it 'has Audit Events button in head nav bar' do it 'has Audit Events button in head nav bar' do
visit group_security_dashboard_path(group) visit group_audit_events_path(group)
expect(page).to have_link('Audit Events') expect(page).to have_link('Audit Events')
end end
......
...@@ -13,23 +13,6 @@ RSpec.describe Groups::SecurityFeaturesHelper do ...@@ -13,23 +13,6 @@ RSpec.describe Groups::SecurityFeaturesHelper do
allow(helper).to receive(:can?).and_return(false) allow(helper).to receive(:can?).and_return(false)
end end
describe '#group_level_security_dashboard_available?' do
where(:security_dashboard_feature_enabled, :result) do
true | true
false | false
end
with_them do
before do
stub_licensed_features(security_dashboard: security_dashboard_feature_enabled)
end
it 'returns the expected result' do
expect(helper.group_level_security_dashboard_available?(group)).to eq(result)
end
end
end
describe '#group_level_security_dashboard_available?' do describe '#group_level_security_dashboard_available?' do
where(:group_level_compliance_dashboard_enabled, :read_group_compliance_dashboard_permission, :result) do where(:group_level_compliance_dashboard_enabled, :read_group_compliance_dashboard_permission, :result) do
false | false | false false | false | false
...@@ -75,84 +58,6 @@ RSpec.describe Groups::SecurityFeaturesHelper do ...@@ -75,84 +58,6 @@ RSpec.describe Groups::SecurityFeaturesHelper do
end end
end end
describe '#group_level_audit_events_available?' do
where(:audit_events_feature_enabled, :read_group_audit_events_permission, :result) do
true | false | false
true | true | true
false | false | false
false | true | false
end
with_them do
before do
stub_licensed_features(audit_events: audit_events_feature_enabled)
allow(helper).to receive(:can?).with(user, :read_group_audit_events, group)
.and_return(read_group_audit_events_permission)
end
it 'returns the expected result' do
expect(helper.group_level_audit_events_available?(group)).to eq(result)
end
end
end
describe '#primary_group_level_security_feature_path' do
subject { helper.primary_group_level_security_feature_path(group) }
context 'group_level_security_dashboard is available' do
before do
allow(helper).to receive(:group_level_security_dashboard_available?).with(group).and_return(true)
end
it 'returns path to security dashboard' do
expect(subject).to eq(group_security_dashboard_path(group))
end
end
context 'group_level_compliance_dashboard is available' do
before do
allow(helper).to receive(:group_level_compliance_dashboard_available?).with(group).and_return(true)
end
it 'returns path to compliance dashboard' do
expect(subject).to eq(group_security_compliance_dashboard_path(group))
end
end
context 'group_level_credentials_inventory is available' do
before do
allow(helper).to receive(:group_level_credentials_inventory_available?).with(group).and_return(true)
end
it 'returns path to credentials inventory dashboard' do
expect(subject).to eq(group_security_credentials_path(group))
end
end
context 'group_level_audit_events is available' do
before do
allow(helper).to receive(:group_level_audit_events_available?).with(group).and_return(true)
end
it 'returns path to audit events' do
expect(subject).to eq(group_audit_events_path(group))
end
end
context 'when no security features are available' do
before do
allow(helper).to receive(:group_level_security_dashboard_available?).with(group).and_return(false)
allow(helper).to receive(:group_level_compliance_dashboard_available?).with(group).and_return(false)
allow(helper).to receive(:group_level_credentials_inventory_available?).with(group).and_return(false)
allow(helper).to receive(:group_level_audit_events_available?).with(group).and_return(false)
end
it 'returns nil' do
expect(subject).to be_nil
end
end
end
describe '#group_level_security_dashboard_data' do describe '#group_level_security_dashboard_data' do
subject { helper.group_level_security_dashboard_data(group) } subject { helper.group_level_security_dashboard_data(group) }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Sidebars::Groups::Menus::SecurityComplianceMenu do
let_it_be(:owner) { create(:user) }
let_it_be_with_refind(:group) do
create(:group, :private).tap do |g|
g.add_owner(owner)
end
end
let(:user) { owner }
let(:show_group_discover_security) { false }
let(:context) { Sidebars::Groups::Context.new(current_user: user, container: group, show_discover_group_security: show_group_discover_security) }
let(:menu) { described_class.new(context) }
describe '#link' do
subject { menu.link }
context 'when menu has menu items' do
it 'returns first visible menu item link' do
expect(subject).to eq menu.renderable_items.first.link
end
end
context 'when menu does no have any menu item' do
let(:user) { nil }
it 'returns show group security page' do
expect(subject).to eq "/groups/#{group.full_path}/-/security/discover"
end
end
end
describe '#title' do
subject { menu.title }
specify do
is_expected.to eq 'Security & Compliance'
end
context 'when menu does not have any menu items' do
let(:user) { nil }
specify do
is_expected.to eq 'Security'
end
end
end
describe '#render?' do
subject { menu.render? }
it 'returns true if there are menu items' do
is_expected.to be true
end
context 'when there are no menu items' do
let(:user) { nil }
it 'returns false if there are no menu items' do
is_expected.to be false
end
context 'when show group discover security option is enabled' do
let(:show_group_discover_security) { true }
specify { is_expected.to be true }
end
end
end
describe 'Menu Items' do
subject { described_class.new(context).renderable_items.index { |e| e.item_id == item_id } }
shared_examples 'menu access rights' do
specify { is_expected.not_to be_nil }
describe 'when the user does not have access' do
let(:user) { nil }
specify { is_expected.to be_nil }
end
end
describe 'Security Dashboard' do
let(:item_id) { :security_dashboard }
context 'when security_dashboard feature is enabled' do
before do
stub_licensed_features(security_dashboard: true)
end
specify { is_expected.not_to be_nil }
end
context 'when security_dashboard feature is not enabled' do
specify { is_expected.to be_nil }
end
end
describe 'Vulnerability Report' do
let(:item_id) { :vulnerability_report }
context 'when security_dashboard feature is enabled' do
before do
stub_licensed_features(security_dashboard: true)
end
specify { is_expected.not_to be_nil }
end
context 'when security_dashboard feature is not enabled' do
specify { is_expected.to be_nil }
end
end
describe 'Compliance' do
let(:item_id) { :compliance }
context 'when group_level_compliance_dashboard feature is enabled' do
before do
stub_licensed_features(group_level_compliance_dashboard: true)
end
it_behaves_like 'menu access rights'
end
context 'when group_level_compliance_dashboard feature is not enabled' do
specify { is_expected.to be_nil }
end
end
describe 'Credentials' do
let(:item_id) { :credentials }
context 'when credentials_inventory feature is enabled' do
before do
stub_licensed_features(credentials_inventory: true)
end
context 'when group magement is not enforced' do
specify { is_expected.to be_nil }
end
context 'when group magement is enforced' do
before do
allow(group).to receive(:enforced_group_managed_accounts?).and_return(true)
end
it_behaves_like 'menu access rights'
end
end
context 'when credentials_inventory feature is not enabled' do
specify { is_expected.to be_nil }
end
end
describe 'Audit Events' do
let(:item_id) { :audit_events }
context 'when audit_events feature is enabled' do
before do
stub_licensed_features(audit_events: true)
end
it_behaves_like 'menu access rights'
end
context 'when audit_events feature is not enabled' do
before do
stub_licensed_features(audit_events: false)
end
specify { is_expected.to be_nil }
end
end
end
end
...@@ -175,128 +175,7 @@ RSpec.describe 'layouts/nav/sidebar/_group' do ...@@ -175,128 +175,7 @@ RSpec.describe 'layouts/nav/sidebar/_group' do
end end
end end
describe 'DevOps adoption link' do describe 'Security & Compliance menu' do
let!(:current_user) { create(:user) }
before do
group.add_maintainer(current_user)
allow(view).to receive(:current_user).and_return(current_user)
end
context 'DevOps adoption feature is available' do
before do
stub_licensed_features(group_level_devops_adoption: true)
end
it 'is visible' do
render
expect(rendered).to have_text 'DevOps adoption'
end
end
context 'DevOps adoption feature is not available' do
before do
stub_licensed_features(group_level_devops_adoption: false)
end
it 'is not visible' do
render
expect(rendered).not_to have_text 'DevOps adoption'
end
end
end
describe 'contribution analytics tab' do
let!(:current_user) { create(:user) }
before do
group.add_guest(current_user)
allow(view).to receive(:current_user).and_return(current_user)
end
context 'contribution analytics feature is available' do
before do
stub_licensed_features(contribution_analytics: true)
end
it 'is visible' do
render
expect(rendered).to have_text 'Contribution'
end
end
context 'contribution analytics feature is not available' do
before do
stub_licensed_features(contribution_analytics: false)
end
context 'we do not show promotions' do
before do
allow(LicenseHelper).to receive(:show_promotions?).and_return(false)
end
it 'is not visible' do
render
expect(rendered).not_to have_text 'Contribution'
end
end
end
context 'no license installed' do
before do
allow(License).to receive(:current).and_return(nil)
stub_application_setting(check_namespace_plan: false)
allow(view).to receive(:can?) { |*args| Ability.allowed?(*args) }
end
it 'is visible when there is no valid license but we show promotions' do
stub_licensed_features(contribution_analytics: false)
render
expect(rendered).to have_text 'Contribution'
end
end
it 'is visible' do
stub_licensed_features(contribution_analytics: true)
render
expect(rendered).to have_text 'Contribution'
end
describe 'group issue boards link' do
context 'when multiple issue board is disabled' do
it 'shows link text in singular' do
render
expect(rendered).to have_text 'Board'
end
end
context 'when multiple issue board is enabled' do
before do
stub_licensed_features(multiple_group_issue_boards: true)
end
it 'shows link text in plural' do
render
expect(rendered).to have_text 'Boards'
end
end
end
end
describe 'security dashboard tab' do
let(:group) { create(:group_with_plan, plan: :ultimate_plan) } let(:group) { create(:group_with_plan, plan: :ultimate_plan) }
before do before do
...@@ -433,6 +312,127 @@ RSpec.describe 'layouts/nav/sidebar/_group' do ...@@ -433,6 +312,127 @@ RSpec.describe 'layouts/nav/sidebar/_group' do
end end
end end
describe 'DevOps adoption link' do
let!(:current_user) { create(:user) }
before do
group.add_maintainer(current_user)
allow(view).to receive(:current_user).and_return(current_user)
end
context 'DevOps adoption feature is available' do
before do
stub_licensed_features(group_level_devops_adoption: true)
end
it 'is visible' do
render
expect(rendered).to have_text 'DevOps adoption'
end
end
context 'DevOps adoption feature is not available' do
before do
stub_licensed_features(group_level_devops_adoption: false)
end
it 'is not visible' do
render
expect(rendered).not_to have_text 'DevOps adoption'
end
end
end
describe 'contribution analytics tab' do
let!(:current_user) { create(:user) }
before do
group.add_guest(current_user)
allow(view).to receive(:current_user).and_return(current_user)
end
context 'contribution analytics feature is available' do
before do
stub_licensed_features(contribution_analytics: true)
end
it 'is visible' do
render
expect(rendered).to have_text 'Contribution'
end
end
context 'contribution analytics feature is not available' do
before do
stub_licensed_features(contribution_analytics: false)
end
context 'we do not show promotions' do
before do
allow(LicenseHelper).to receive(:show_promotions?).and_return(false)
end
it 'is not visible' do
render
expect(rendered).not_to have_text 'Contribution'
end
end
end
context 'no license installed' do
before do
allow(License).to receive(:current).and_return(nil)
stub_application_setting(check_namespace_plan: false)
allow(view).to receive(:can?) { |*args| Ability.allowed?(*args) }
end
it 'is visible when there is no valid license but we show promotions' do
stub_licensed_features(contribution_analytics: false)
render
expect(rendered).to have_text 'Contribution'
end
end
it 'is visible' do
stub_licensed_features(contribution_analytics: true)
render
expect(rendered).to have_text 'Contribution'
end
describe 'group issue boards link' do
context 'when multiple issue board is disabled' do
it 'shows link text in singular' do
render
expect(rendered).to have_text 'Board'
end
end
context 'when multiple issue board is enabled' do
before do
stub_licensed_features(multiple_group_issue_boards: true)
end
it 'shows link text in plural' do
render
expect(rendered).to have_text 'Boards'
end
end
end
end
describe 'wiki tab' do describe 'wiki tab' do
let(:can_read_wiki) { true } let(:can_read_wiki) { true }
......
...@@ -33,14 +33,6 @@ module QA ...@@ -33,14 +33,6 @@ module QA
element :billing_link element :billing_link
end end
view 'ee/app/views/layouts/nav/ee/_security_link.html.haml' do
element :security_compliance_link
element :group_secure_submenu
element :security_dashboard_link
element :vulnerability_report_link
element :audit_events_settings_link
end
view 'ee/app/views/layouts/nav/_group_insights_link.html.haml' do view 'ee/app/views/layouts/nav/_group_insights_link.html.haml' do
element :group_insights_link element :group_insights_link
end end
...@@ -53,14 +45,6 @@ module QA ...@@ -53,14 +45,6 @@ module QA
end end
end end
def go_to_audit_events_settings
hover_element(:security_compliance_link) do
within_submenu(:group_secure_submenu) do
click_element(:audit_events_settings_link)
end
end
end
def go_to_issue_boards def go_to_issue_boards
hover_issues do hover_issues do
within_submenu do within_submenu do
...@@ -116,17 +100,25 @@ module QA ...@@ -116,17 +100,25 @@ module QA
end end
def click_group_security_link def click_group_security_link
hover_element(:security_compliance_link) do hover_security_and_compliance do
within_submenu(:group_secure_submenu) do within_submenu do
click_element(:security_dashboard_link) click_element(:sidebar_menu_item_link, menu_item: 'Security Dashboard')
end end
end end
end end
def click_group_vulnerability_link def click_group_vulnerability_link
hover_element(:security_compliance_link) do hover_security_and_compliance do
within_submenu(:group_secure_submenu) do within_submenu do
click_element(:vulnerability_report_link) click_element(:sidebar_menu_item_link, menu_item: 'Vulnerability Report')
end
end
end
def go_to_audit_events
hover_security_and_compliance do
within_submenu do
click_element(:sidebar_menu_item_link, menu_item: 'Audit Events')
end end
end end
end end
...@@ -152,6 +144,17 @@ module QA ...@@ -152,6 +144,17 @@ module QA
end end
end end
end end
private
def hover_security_and_compliance
within_sidebar do
scroll_to_element(:sidebar_menu_link, menu_item: 'Security & Compliance')
find_element(:sidebar_menu_link, menu_item: 'Security & Compliance').hover
yield
end
end
end end
end end
end end
......
...@@ -11,7 +11,7 @@ module QA ...@@ -11,7 +11,7 @@ module QA
it 'logs audit events for UI operations' do it 'logs audit events for UI operations' do
wait_for_audit_events(expected_events, group) wait_for_audit_events(expected_events, group)
Page::Group::Menu.perform(&:go_to_audit_events_settings) Page::Group::Menu.perform(&:go_to_audit_events)
expected_events.each do |expected_event| expected_events.each do |expected_event|
# Sometimes the audit logs are not displayed in the UI # Sometimes the audit logs are not displayed in the UI
# right away so a refresh may be needed. # right away so a refresh may be needed.
......
...@@ -5,7 +5,7 @@ module QA ...@@ -5,7 +5,7 @@ module QA
RSpec.describe 'Manage' do RSpec.describe 'Manage' do
shared_examples 'audit event' do |expected_events| shared_examples 'audit event' do |expected_events|
it 'logs audit events for UI operations' do it 'logs audit events for UI operations' do
Page::Group::Menu.perform(&:go_to_audit_events_settings) Page::Group::Menu.perform(&:go_to_audit_events)
expected_events.each do |expected_event| expected_events.each do |expected_event|
expect(page).to have_text(expected_event) expect(page).to have_text(expected_event)
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment