Commit 0f281c17 authored by Ramya Authappan's avatar Ramya Authappan

Merge branch 'qa-ml-approval-rules-tests' into 'master'

Add E2E test of approval rules

Closes gitlab-org/quality/testcases#115

See merge request gitlab-org/gitlab-ee!15795
parents 050e549a 88a6348b
...@@ -167,6 +167,18 @@ There are two supported methods of defining elements within a view. ...@@ -167,6 +167,18 @@ There are two supported methods of defining elements within a view.
Any existing `.qa-selector` class should be considered deprecated Any existing `.qa-selector` class should be considered deprecated
and we should prefer the `data-qa-selector` method of definition. and we should prefer the `data-qa-selector` method of definition.
### Exceptions
In some cases it might not be possible or worthwhile to add a selector.
Some UI components use external libraries, including some maintained by third parties.
Even if a library is maintained by GitLab, the selector sanity test only runs
on code within the GitLab project, so it's not possible to specify the path for
the view for code in a library.
In such rare cases it's reasonable to use CSS selectors in page object methods,
with a comment explaining why an `element` can't be added.
## Running the test locally ## Running the test locally
During development, you can run the `qa:selectors` test by running During development, you can run the `qa:selectors` test by running
......
...@@ -42,15 +42,21 @@ export default { ...@@ -42,15 +42,21 @@ export default {
<gl-loading-icon v-if="!hasLoaded" :size="2" /> <gl-loading-icon v-if="!hasLoaded" :size="2" />
<template v-else> <template v-else>
<div class="border-bottom"> <div class="border-bottom">
<slot v-if="isEmpty" name="fallback"> <fallback-rules /> </slot> <slot v-if="isEmpty" name="fallback">
<fallback-rules />
</slot>
<slot v-else name="rules"></slot> <slot v-else name="rules"></slot>
</div> </div>
<div v-if="settings.canEdit" class="border-bottom py-3 px-2"> <div v-if="settings.canEdit" class="border-bottom py-3 px-2">
<gl-loading-icon v-if="isLoading" /> <gl-loading-icon v-if="isLoading" />
<div v-if="settings.allowMultiRule" class="d-flex"> <div v-if="settings.allowMultiRule" class="d-flex">
<gl-button class="ml-auto btn-info btn-inverted" @click="openCreateModal(null)">{{ <gl-button
__('Add approval rule') class="ml-auto btn-info btn-inverted"
}}</gl-button> data-qa-selector="add_approvers_button"
@click="openCreateModal(null)"
>
{{ __('Add approval rule') }}
</gl-button>
</div> </div>
</div> </div>
<slot name="footer"></slot> <slot name="footer"></slot>
......
...@@ -229,6 +229,7 @@ export default { ...@@ -229,6 +229,7 @@ export default {
class="form-control" class="form-control"
name="name" name="name"
type="text" type="text"
data-qa-selector="rule_name_field"
/> />
<span class="invalid-feedback">{{ validation.name }}</span> <span class="invalid-feedback">{{ validation.name }}</span>
<span class="text-secondary">{{ s__('ApprovalRule|e.g. QA, Security, etc.') }}</span> <span class="text-secondary">{{ s__('ApprovalRule|e.g. QA, Security, etc.') }}</span>
...@@ -236,9 +237,7 @@ export default { ...@@ -236,9 +237,7 @@ export default {
</div> </div>
<div class="form-group col-sm-6"> <div class="form-group col-sm-6">
<label class="label-wrapper"> <label class="label-wrapper">
<span class="mb-2 bold inline"> <span class="mb-2 bold inline">{{ s__('ApprovalRule|No. approvals required') }}</span>
{{ s__('ApprovalRule|No. approvals required') }}
</span>
<input <input
v-model.number="approvalsRequired" v-model.number="approvalsRequired"
:class="{ 'is-invalid': validation.approvalsRequired }" :class="{ 'is-invalid': validation.approvalsRequired }"
...@@ -246,6 +245,7 @@ export default { ...@@ -246,6 +245,7 @@ export default {
name="approvals_required" name="approvals_required"
type="number" type="number"
:min="minApprovalsRequired" :min="minApprovalsRequired"
data-qa-selector="approvals_required_field"
/> />
<span class="invalid-feedback">{{ validation.approvalsRequired }}</span> <span class="invalid-feedback">{{ validation.approvalsRequired }}</span>
</label> </label>
...@@ -254,7 +254,7 @@ export default { ...@@ -254,7 +254,7 @@ export default {
<div class="form-group"> <div class="form-group">
<label class="label-bold">{{ s__('ApprovalRule|Approvers') }}</label> <label class="label-bold">{{ s__('ApprovalRule|Approvers') }}</label>
<div class="d-flex align-items-start"> <div class="d-flex align-items-start">
<div class="w-100"> <div class="w-100" data-qa-selector="member_select_field">
<approvers-select <approvers-select
v-model="approversToAdd" v-model="approversToAdd"
:project-id="settings.projectId" :project-id="settings.projectId"
...@@ -264,9 +264,14 @@ export default { ...@@ -264,9 +264,14 @@ export default {
/> />
<div class="invalid-feedback">{{ validation.approvers }}</div> <div class="invalid-feedback">{{ validation.approvers }}</div>
</div> </div>
<gl-button variant="success" class="btn-inverted prepend-left-8" @click="addSelection">{{ <gl-button
__('Add') variant="success"
}}</gl-button> class="btn-inverted prepend-left-8"
data-qa-selector="add_member_button"
@click="addSelection"
>
{{ __('Add') }}
</gl-button>
</div> </div>
</div> </div>
<div class="bordered-box overflow-auto h-12em"> <div class="bordered-box overflow-auto h-12em">
......
...@@ -215,6 +215,7 @@ export default { ...@@ -215,6 +215,7 @@ export default {
:class="{ 'btn-inverted': action.inverted }" :class="{ 'btn-inverted': action.inverted }"
size="sm" size="sm"
class="mr-3" class="mr-3"
data-qa-selector="approve_button"
@click="action.action" @click="action.action"
> >
<gl-loading-icon v-if="isApproving" inline /> <gl-loading-icon v-if="isApproving" inline />
......
...@@ -59,7 +59,7 @@ export default { ...@@ -59,7 +59,7 @@ export default {
</script> </script>
<template> <template>
<div> <div data-qa-selector="approvals_summary_content">
<strong>{{ message }}</strong> <strong>{{ message }}</strong>
<template v-if="hasApprovers"> <template v-if="hasApprovers">
<span>{{ s__('mrWidget|Approved by') }}</span> <span>{{ s__('mrWidget|Approved by') }}</span>
......
...@@ -111,6 +111,7 @@ module QA ...@@ -111,6 +111,7 @@ module QA
end end
module MergeRequest module MergeRequest
autoload :New, 'qa/ee/page/merge_request/new'
autoload :Show, 'qa/ee/page/merge_request/show' autoload :Show, 'qa/ee/page/merge_request/show'
end end
......
# frozen_string_literal: true
module QA
module EE
module Page
module MergeRequest
module New
def self.prepended(page)
page.module_eval do
view 'ee/app/assets/javascripts/approvals/components/app.vue' do
element :add_approvers_button
end
view 'ee/app/assets/javascripts/approvals/components/rule_form.vue' do
element :add_member_button
element :approvals_required_field
element :member_select_field
element :rule_name_field
end
def add_approval_rules(rules)
rules.each do |rule|
click_element :add_approvers_button
wait_for_animated_element :rule_name_field
fill_element :rule_name_field, rule[:name]
fill_element :approvals_required_field, rule[:approvals_required]
rule.key?(:users) && rule[:users].each do |user|
select_user_member user.username
click_element :add_member_button
end
rule.key?(:groups) && rule[:groups].each do |group|
select_group_member group.name
click_element :add_member_button
end
click_approvers_modal_ok_button
end
end
# The Add/Update approvers modal is a gitlab-ui component built on
# a bootstrap-vue component. It doesn't seem straightforward to
# add a data attribute to the 'Ok' button without overriding it
# So we break the rules and use a CSS selector instead of an element
def click_approvers_modal_ok_button
find("#mr-edit-approvals-create-modal footer button.btn-success").click
end
# Select2 is an external library, so we can't add our own selector
def select_user_member(name)
enter_member(name)
find('.select2-results .user-username', text: "@#{name}").click
end
def select_group_member(name)
enter_member(name)
find('.select2-results .group-name', text: "#{name}").click
end
private
def enter_member(name)
within_element(:member_select_field) do
find(".select2-input").set(name)
end
end
end
end
end
end
end
end
end
...@@ -45,76 +45,124 @@ module QA ...@@ -45,76 +45,124 @@ module QA
element :expand_report_button element :expand_report_button
end end
view 'ee/app/assets/javascripts/vue_shared/security_reports/components/modal_footer.vue' do view 'ee/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue' do
element :resolve_split_button element :approve_button
end end
def start_review view 'ee/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue' do
click_element :start_review element :approvals_summary_content
end end
def comment_now view 'ee/app/assets/javascripts/vue_shared/security_reports/components/modal_footer.vue' do
click_element :comment_now element :resolve_split_button
end end
end
end
def submit_pending_reviews def approvals_required_from
within_element :review_bar do approvals_content.match(/approvals? from (.*)/)[1]
click_element :review_preview_toggle end
click_element :submit_review
end
end
def discard_pending_reviews def approved?
within_element :review_bar do approvals_content =~ /Merge request approved/
click_element :discard_review end
end
click_element :modal_delete_pending_comments
end
def resolve_review_discussion def approvers
scroll_to_element :start_review within_element :approver_list do
check_element :resolve_review_discussion_checkbox all_elements(:approver).map { |item| item.find('img')['title'] }
end end
end
def unresolve_review_discussion def click_approve
check_element :unresolve_review_discussion_checkbox click_element :approve_button
end end
def approvers def start_review
within_element :approver_list do click_element :start_review
all_elements(:approver).map { |item| item.find('img')['title'] } end
end
end
def expand_vulnerability_report def comment_now
click_element :expand_report_button click_element :comment_now
end end
def click_vulnerability(name) def submit_pending_reviews
within_element :vulnerability_report_grouped do within_element :review_bar do
click_on name click_element :review_preview_toggle
end click_element :submit_review
end end
end
def resolve_vulnerability_with_mr(name) def discard_pending_reviews
expand_vulnerability_report within_element :review_bar do
click_vulnerability(name) click_element :discard_review
click_element :resolve_split_button end
end click_element :modal_delete_pending_comments
end
def has_vulnerability_report?(timeout: 60) def resolve_review_discussion
wait(reload: true, max: timeout, interval: 1) do scroll_to_element :start_review
finished_loading? check_element :resolve_review_discussion_checkbox
has_element?(:vulnerability_report_grouped, wait: 1) end
end
end
def has_detected_vulnerability_count_of?(expected) def unresolve_review_discussion
# Match text cut off in order to find both "1 vulnerability" and "X vulnerabilities" check_element :unresolve_review_discussion_checkbox
find_element(:vulnerability_report_grouped).has_content?("detected #{expected} vulnerabilit") end
end
def expand_vulnerability_report
click_element :expand_report_button
end
def click_vulnerability(name)
within_element :vulnerability_report_grouped do
click_on name
end
end
def resolve_vulnerability_with_mr(name)
expand_vulnerability_report
click_vulnerability(name)
click_element :resolve_split_button
end
def has_vulnerability_report?(timeout: 60)
wait(reload: true, max: timeout, interval: 1) do
finished_loading?
has_element?(:vulnerability_report_grouped, wait: 1)
end end
end end
def has_detected_vulnerability_count_of?(expected)
# Match text cut off in order to find both "1 vulnerability" and "X vulnerabilities"
find_element(:vulnerability_report_grouped).has_content?("detected #{expected} vulnerabilit")
end
def num_approvals_required
approvals_content.match(/Requires (\d+) more approvals/)[1].to_i
end
private
def approvals_content
# The approvals widget displays "Checking approval status" briefly
# while loading the widget, so before returning the text we wait
# for it to include terms from content we expect. The kinds
# of content we expect are:
#
# * Requires X more approvals from Quality, UX, and frontend.
# * Merge request approved
#
# It can also briefly display cached data while loading so we
# wait for it to update first
sleep 1
text = nil
wait(reload: false) do
text = find_element(:approvals_summary_content).text
text =~ /Requires|approved/
end
text
end
end end
end end
end end
......
...@@ -61,6 +61,10 @@ module QA ...@@ -61,6 +61,10 @@ module QA
end end
end end
def sign_out_if_signed_in
sign_out if has_personal_area?(wait: 0)
end
def click_settings_link def click_settings_link
retry_until(reload: false) do retry_until(reload: false) do
within_user_menu do within_user_menu do
......
...@@ -64,3 +64,5 @@ module QA ...@@ -64,3 +64,5 @@ module QA
end end
end end
end end
QA::Page::MergeRequest::New.prepend_if_ee('QA::EE::Page::MergeRequest::New')
...@@ -10,6 +10,7 @@ module QA ...@@ -10,6 +10,7 @@ module QA
end end
attribute :id attribute :id
attribute :name
def initialize def initialize
@path = Runtime::Namespace.name @path = Runtime::Namespace.name
...@@ -47,6 +48,11 @@ module QA ...@@ -47,6 +48,11 @@ module QA
super super
end end
def add_member(user, access_level = '30')
# 30 = developer access
post Runtime::API::Request.new(api_client, api_members_path).url, { user_id: user.id, access_level: access_level }
end
def api_get_path def api_get_path
"/groups/#{CGI.escape("#{sandbox.path}/#{path}")}" "/groups/#{CGI.escape("#{sandbox.path}/#{path}")}"
end end
......
...@@ -5,7 +5,8 @@ require 'securerandom' ...@@ -5,7 +5,8 @@ require 'securerandom'
module QA module QA
module Resource module Resource
class MergeRequest < Base class MergeRequest < Base
attr_accessor :id, attr_accessor :approval_rules,
:id,
:title, :title,
:description, :description,
:source_branch, :source_branch,
...@@ -46,6 +47,7 @@ module QA ...@@ -46,6 +47,7 @@ module QA
end end
def initialize def initialize
@approval_rules = nil
@title = 'QA test - merge request' @title = 'QA test - merge request'
@description = 'This is a test merge request' @description = 'This is a test merge request'
@source_branch = "qa-test-feature-#{SecureRandom.hex(8)}" @source_branch = "qa-test-feature-#{SecureRandom.hex(8)}"
...@@ -63,16 +65,17 @@ module QA ...@@ -63,16 +65,17 @@ module QA
project.visit! project.visit!
Page::Project::Show.perform(&:new_merge_request) Page::Project::Show.perform(&:new_merge_request)
Page::MergeRequest::New.perform do |page| Page::MergeRequest::New.perform do |new|
page.fill_title(@title) new.fill_title(@title)
page.fill_description(@description) new.fill_description(@description)
page.choose_milestone(@milestone) if @milestone new.choose_milestone(@milestone) if @milestone
page.assign_to_me if @assignee == 'me' new.assign_to_me if @assignee == 'me'
labels.each do |label| labels.each do |label|
page.select_label(label) new.select_label(label)
end end
new.add_approval_rules(approval_rules) if approval_rules
page.create_merge_request new.create_merge_request
end end
end end
......
...@@ -75,6 +75,11 @@ module QA ...@@ -75,6 +75,11 @@ module QA
super super
end end
def add_member(user, access_level = '30')
# 30 = developer access
post Runtime::API::Request.new(api_client, api_members_path).url, { user_id: user.id, access_level: access_level }
end
def api_get_path def api_get_path
"/projects/#{CGI.escape(path_with_namespace)}" "/projects/#{CGI.escape(path_with_namespace)}"
end end
...@@ -83,6 +88,10 @@ module QA ...@@ -83,6 +88,10 @@ module QA
"#{api_get_path}/repository/archive.#{type}" "#{api_get_path}/repository/archive.#{type}"
end end
def api_members_path
"#{api_get_path}/members"
end
def api_post_path def api_post_path
'/projects' '/projects'
end end
......
...@@ -9,6 +9,7 @@ module QA ...@@ -9,6 +9,7 @@ module QA
attr_writer :username, :password attr_writer :username, :password
attr_accessor :provider, :extern_uid attr_accessor :provider, :extern_uid
attribute :id
attribute :name attribute :name
attribute :email attribute :email
......
# frozen_string_literal: true
module QA
context 'Create' do
describe 'Approval rules' do
let(:approver1) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) }
let(:approver2) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_2, Runtime::Env.gitlab_qa_password_2) }
let(:project) do
Resource::Project.fabricate_via_api! { |project| project.name = "approval-rules" }
end
def login(user = nil)
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.perform { |login| login.sign_in_using_credentials(user) }
end
before do
project.add_member(approver1)
project.group.add_member(approver2)
Page::Main::Menu.perform(&:sign_out_if_signed_in)
login
end
it 'allows multiple approval rules with users and groups' do
# Create a merge request with 2 rules
merge_request = Resource::MergeRequest.fabricate_via_browser_ui! do |resource|
resource.title = 'Add a new feature'
resource.description = 'Great feature, much approval'
resource.project = project
resource.approval_rules = [
{
name: "user",
approvals_required: 1,
users: [approver1]
},
{
name: "group",
approvals_required: 1,
groups: [project.group]
}
]
end
Page::MergeRequest::Show.perform do |show|
expect(show.num_approvals_required).to eq(2)
expect(show.approvals_required_from).to include("user", "group")
end
# As approver1, approve the MR
Page::Main::Menu.perform(&:sign_out)
login(approver1)
merge_request.visit!
Page::MergeRequest::Show.perform do |show|
show.click_approve
end
# Confirm that an approval was granted but it is not yet fully approved
Page::MergeRequest::Show.perform do |show|
expect(show).not_to be_approved
expect(show.approvals_required_from).to include("group")
expect(show.approvals_required_from).not_to include("user")
end
# As approver2, approve the MR
Page::Main::Menu.perform(&:sign_out)
login(approver2)
merge_request.visit!
Page::MergeRequest::Show.perform do |show|
show.click_approve
end
# Confirm that the MR is fully approved
Page::MergeRequest::Show.perform do |show|
expect(show).to be_approved
end
# Merge the MR as the original user
Page::Main::Menu.perform(&:sign_out)
login
merge_request.visit!
Page::MergeRequest::Show.perform do |show|
show.merge!
end
expect(page).to have_content('The changes were merged')
end
end
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