diff options
author | Rémy Coutable <remy@rymai.me> | 2018-01-11 11:20:52 +0000 |
---|---|---|
committer | Rémy Coutable <remy@rymai.me> | 2018-01-11 11:20:52 +0000 |
commit | 6f7ab3010788e0ef11c8f86d9bbad5a8c78331dd (patch) | |
tree | 3ab01053e2f5a2a906b73406d7da8b3fc6904828 | |
parent | a733c26e13600ceb6ba3b897b5d826d1a84284c3 (diff) | |
parent | f4a694efd8f0dd9c84ab9bbbad1ead4238e0c99f (diff) | |
download | gitlab-ce-6f7ab3010788e0ef11c8f86d9bbad5a8c78331dd.tar.gz |
Merge branch 'qa/gb/validate-qa-selectors' into 'master'
Validate GitLab QA selectors
Closes gitlab-qa#58
See merge request gitlab-org/gitlab-ce!16109
32 files changed, 692 insertions, 16 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4f47d3f0171..6977ac6a5c9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -623,6 +623,18 @@ qa:internal: - bundle install - bundle exec rspec +qa:selectors: + <<: *dedicated-runner + <<: *except-docs + stage: test + variables: + SETUP_DB: "false" + services: [] + script: + - cd qa/ + - bundle install + - bundle exec bin/qa Test::Sanity::Selectors + coverage: <<: *dedicated-runner <<: *except-docs-and-qa diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 99e7f3b568d..46727811be4 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -1,4 +1,4 @@ -%header.navbar.navbar-gitlab +%header.navbar.navbar-gitlab.qa-navbar %a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content .container-fluid .header-content @@ -43,7 +43,7 @@ = todos_count_format(todos_pending_count) %li.header-user.dropdown = link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do - = image_tag avatar_icon(current_user, 23), width: 23, height: 23, class: "header-user-avatar" + = image_tag avatar_icon(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar" = sprite_icon('angle-down', css_class: 'caret-down') .dropdown-menu-nav.dropdown-menu-align-right %ul diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 4013181da9c..74532eba298 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -1,5 +1,5 @@ %ul.list-unstyled.navbar-sub-nav - = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects" }) do + = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects qa-projects-dropdown" }) do %a{ href: "#", data: { toggle: "dropdown" } } Projects = sprite_icon('angle-down', css_class: 'caret-down') @@ -7,7 +7,7 @@ = render "layouts/nav/projects_dropdown/show" = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "hidden-xs" }) do - = link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do + = link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups qa-groups-link', title: 'Groups' do Groups = nav_link(path: 'dashboard#activity', html_options: { class: "visible-lg" }) do @@ -59,7 +59,7 @@ %li.line-separator.hidden-xs - if current_user.admin? = nav_link(controller: 'admin/dashboard') do - = link_to admin_root_path, class: 'admin-icon', title: 'Admin area', aria: { label: "Admin area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = link_to admin_root_path, class: 'admin-icon qa-admin-area-link', title: 'Admin area', aria: { label: "Admin area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = sprite_icon('admin', size: 18) - if Gitlab::Sherlock.enabled? %li diff --git a/app/views/layouts/nav/projects_dropdown/_show.html.haml b/app/views/layouts/nav/projects_dropdown/_show.html.haml index 32a24c101fc..59becb043d3 100644 --- a/app/views/layouts/nav/projects_dropdown/_show.html.haml +++ b/app/views/layouts/nav/projects_dropdown/_show.html.haml @@ -1,9 +1,9 @@ - project_meta = { id: @project.id, name: @project.name, namespace: @project.name_with_namespace, web_url: project_path(@project), avatar_url: @project.avatar_url } if @project&.persisted? .projects-dropdown-container - .project-dropdown-sidebar + .project-dropdown-sidebar.qa-projects-dropdown-sidebar %ul = nav_link(path: 'dashboard/projects#index') do - = link_to dashboard_projects_path do + = link_to dashboard_projects_path, class: 'qa-your-projects-link' do = _('Your projects') = nav_link(path: 'projects#starred') do = link_to starred_dashboard_projects_path do diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index 1fa3a3041fd..abd07d71bcc 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -226,7 +226,7 @@ = link_to edit_project_path(@project), class: 'shortcuts-tree' do .nav-icon-container = sprite_icon('settings') - %span.nav-item-name + %span.nav-item-name.qa-settings-item Settings %ul.sidebar-sub-level-items @@ -58,6 +58,10 @@ module QA module Integration autoload :Mattermost, 'qa/scenario/test/integration/mattermost' end + + module Sanity + autoload :Selectors, 'qa/scenario/test/sanity/selectors' + end end end @@ -68,6 +72,9 @@ module QA # module Page autoload :Base, 'qa/page/base' + autoload :View, 'qa/page/view' + autoload :Element, 'qa/page/element' + autoload :Validator, 'qa/page/validator' module Main autoload :Login, 'qa/page/main/login' diff --git a/qa/qa/page/admin/settings.rb b/qa/qa/page/admin/settings.rb index 39e2f2062ad..1904732aee6 100644 --- a/qa/qa/page/admin/settings.rb +++ b/qa/qa/page/admin/settings.rb @@ -2,6 +2,13 @@ module QA module Page module Admin class Settings < Page::Base + ## + # TODO, define all selectors required by this page object + # + # See gitlab-org/gitlab-qa#154 + # + view 'app/views/admin/application_settings/show.html.haml' + def enable_hashed_storage scroll_to 'legend', text: 'Repository Storage' check 'Create new projects using hashed storage paths' diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb index 99eba02b6e3..ea4c920c82c 100644 --- a/qa/qa/page/base.rb +++ b/qa/qa/page/base.rb @@ -5,6 +5,9 @@ module QA class Base include Capybara::DSL include Scenario::Actable + extend SingleForwardable + + def_delegators :evaluator, :view, :views def refresh visit current_url @@ -37,9 +40,39 @@ module QA page.within(selector) { yield } if block_given? end + def click_element(name) + find(Page::Element.new(name).selector_css).click + end + def self.path raise NotImplementedError end + + def self.evaluator + @evaluator ||= Page::Base::DSL.new + end + + def self.errors + if views.empty? + return ["Page class does not have views / elements defined!"] + end + + views.map(&:errors).flatten + end + + class DSL + attr_reader :views + + def initialize + @views = [] + end + + def view(path, &block) + Page::View.evaluate(&block).tap do |view| + @views.push(Page::View.new(path, view.elements)) + end + end + end end end end diff --git a/qa/qa/page/dashboard/groups.rb b/qa/qa/page/dashboard/groups.rb index 083d2e1ab16..e853e0d85e0 100644 --- a/qa/qa/page/dashboard/groups.rb +++ b/qa/qa/page/dashboard/groups.rb @@ -2,6 +2,15 @@ module QA module Page module Dashboard class Groups < Page::Base + view 'app/views/shared/groups/_search_form.html.haml' do + element :groups_filter, 'search_field_tag :filter' + element :groups_filter_placeholder, 'Filter by name...' + end + + view 'app/views/dashboard/_groups_head.html.haml' do + element :new_group_button, 'link_to _("New group")' + end + def filter_by_name(name) fill_in 'Filter by name...', with: name end diff --git a/qa/qa/page/dashboard/projects.rb b/qa/qa/page/dashboard/projects.rb index 7ed27da6d89..71255b18362 100644 --- a/qa/qa/page/dashboard/projects.rb +++ b/qa/qa/page/dashboard/projects.rb @@ -2,6 +2,8 @@ module QA module Page module Dashboard class Projects < Page::Base + view 'app/views/dashboard/projects/index.html.haml' + def go_to_project(name) find_link(text: name).click end diff --git a/qa/qa/page/element.rb b/qa/qa/page/element.rb new file mode 100644 index 00000000000..9944a39ce07 --- /dev/null +++ b/qa/qa/page/element.rb @@ -0,0 +1,32 @@ +module QA + module Page + class Element + attr_reader :name + + def initialize(name, pattern = nil) + @name = name + @pattern = pattern || selector + end + + def selector + "qa-#{@name.to_s.tr('_', '-')}" + end + + def selector_css + ".#{selector}" + end + + def expression + if @pattern.is_a?(String) + @_regexp ||= Regexp.new(Regexp.escape(@pattern)) + else + @pattern + end + end + + def matches?(line) + !!(line =~ expression) + end + end + end +end diff --git a/qa/qa/page/group/new.rb b/qa/qa/page/group/new.rb index 53fdaaed078..48b71a7c883 100644 --- a/qa/qa/page/group/new.rb +++ b/qa/qa/page/group/new.rb @@ -2,6 +2,17 @@ module QA module Page module Group class New < Page::Base + view 'app/views/shared/_group_form.html.haml' do + element :group_path_field, 'text_field :path' + element :group_name_field, 'text_field :name' + element :group_description_field, 'text_area :description' + end + + view 'app/views/groups/new.html.haml' do + element :create_group_button, "submit 'Create group'" + element :visibility_radios, 'visibility_level:' + end + def set_path(path) fill_in 'group_path', with: path fill_in 'group_name', with: path diff --git a/qa/qa/page/group/show.rb b/qa/qa/page/group/show.rb index 0a16c07d64b..37ed3b35bce 100644 --- a/qa/qa/page/group/show.rb +++ b/qa/qa/page/group/show.rb @@ -2,6 +2,13 @@ module QA module Page module Group class Show < Page::Base + ## + # TODO, define all selectors required by this page object + # + # See gitlab-org/gitlab-qa#154 + # + view 'app/views/groups/show.html.haml' + def go_to_subgroup(name) click_link name end diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb index f88325f408b..7b4c1603017 100644 --- a/qa/qa/page/main/login.rb +++ b/qa/qa/page/main/login.rb @@ -2,6 +2,18 @@ module QA module Page module Main class Login < Page::Base + view 'app/views/devise/passwords/edit.html.haml' do + element :password_field, 'password_field :password' + element :password_confirmation, 'password_field :password_confirmation' + element :change_password_button, 'submit "Change your password"' + end + + view 'app/views/devise/sessions/_new_base.html.haml' do + element :login_field, 'text_field :login' + element :passowrd_field, 'password_field :password' + element :sign_in_button, 'submit "Sign in"' + end + def initialize wait('.application', time: 500) end diff --git a/qa/qa/page/main/oauth.rb b/qa/qa/page/main/oauth.rb index e746cff0a80..6f548148363 100644 --- a/qa/qa/page/main/oauth.rb +++ b/qa/qa/page/main/oauth.rb @@ -2,6 +2,10 @@ module QA module Page module Main class OAuth < Page::Base + view 'app/views/doorkeeper/authorizations/new.html.haml' do + element :authorization_button, 'submit_tag "Authorize"' + end + def needs_authorization? page.current_url.include?('/oauth') end diff --git a/qa/qa/page/mattermost/login.rb b/qa/qa/page/mattermost/login.rb index 8ffd4fdad13..9b21300ea3c 100644 --- a/qa/qa/page/mattermost/login.rb +++ b/qa/qa/page/mattermost/login.rb @@ -2,6 +2,13 @@ module QA module Page module Mattermost class Login < Page::Base + ## + # TODO, define all selectors required by this page object + # + # See gitlab-org/gitlab-qa#154 + # + view 'app/views/projects/mattermosts/new.html.haml' + def sign_in_using_oauth click_link class: 'btn btn-custom-login gitlab' diff --git a/qa/qa/page/mattermost/main.rb b/qa/qa/page/mattermost/main.rb index 4b8fc28e53f..bc2f9acc729 100644 --- a/qa/qa/page/mattermost/main.rb +++ b/qa/qa/page/mattermost/main.rb @@ -2,6 +2,13 @@ module QA module Page module Mattermost class Main < Page::Base + ## + # TODO, define all selectors required by this page object + # + # See gitlab-org/gitlab-qa#154 + # + view 'app/views/projects/mattermosts/new.html.haml' + def initialize visit(Runtime::Scenario.mattermost_address) end diff --git a/qa/qa/page/menu/admin.rb b/qa/qa/page/menu/admin.rb index 07fe40fda3a..40da4a53e8a 100644 --- a/qa/qa/page/menu/admin.rb +++ b/qa/qa/page/menu/admin.rb @@ -2,6 +2,13 @@ module QA module Page module Menu class Admin < Page::Base + ## + # TODO, define all selectors required by this page object + # + # See gitlab-org/gitlab-qa#154 + # + view 'app/views/admin/dashboard/index.html.haml' + def go_to_license click_link 'License' end diff --git a/qa/qa/page/menu/main.rb b/qa/qa/page/menu/main.rb index b94c2c6c23d..f8978b8a5f7 100644 --- a/qa/qa/page/menu/main.rb +++ b/qa/qa/page/menu/main.rb @@ -2,19 +2,40 @@ module QA module Page module Menu class Main < Page::Base + view 'app/views/layouts/header/_default.html.haml' do + element :navbar + element :user_avatar + element :user_menu, '.dropdown-menu-nav' + element :user_sign_out_link, 'link_to "Sign out"' + end + + view 'app/views/layouts/nav/_dashboard.html.haml' do + element :admin_area_link + element :projects_dropdown + element :groups_link + end + + view 'app/views/layouts/nav/projects_dropdown/_show.html.haml' do + element :projects_dropdown_sidebar + element :your_projects_link + end + def go_to_groups - within_top_menu { click_link 'Groups' } + within_top_menu { click_element :groups_link } end def go_to_projects within_top_menu do - click_link 'Projects' - click_link 'Your projects' + click_element :projects_dropdown + end + + page.within('.qa-projects-dropdown-sidebar') do + click_element :your_projects_link end end def go_to_admin_area - within_top_menu { find('.admin-icon').click } + within_top_menu { click_element :admin_area_link } end def sign_out @@ -24,20 +45,20 @@ module QA end def has_personal_area? - page.has_selector?('.header-user-dropdown-toggle') + page.has_selector?('.qa-user-avatar') end private def within_top_menu - page.within('.navbar') do + page.within('.qa-navbar') do yield end end def within_user_menu within_top_menu do - find('.header-user-dropdown-toggle').click + click_element :user_avatar page.within('.dropdown-menu-nav') do yield diff --git a/qa/qa/page/menu/side.rb b/qa/qa/page/menu/side.rb index 6c25aba4bac..1df4e0c2429 100644 --- a/qa/qa/page/menu/side.rb +++ b/qa/qa/page/menu/side.rb @@ -2,6 +2,12 @@ module QA module Page module Menu class Side < Page::Base + view 'app/views/layouts/nav/sidebar/_project.html.haml' do + element :settings_item + element :repository_link, "title: 'Repository'" + element :top_level_items, '.sidebar-top-level-items' + end + def click_repository_setting hover_setting do click_link('Repository') @@ -12,7 +18,7 @@ module QA def hover_setting within_sidebar do - find('.nav-item-name', text: 'Settings').hover + find('.qa-settings-item').hover yield end diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb index b31bec27b59..a87313b2cb9 100644 --- a/qa/qa/page/project/new.rb +++ b/qa/qa/page/project/new.rb @@ -2,6 +2,13 @@ module QA module Page module Project class New < Page::Base + ## + # TODO, define all selectors required by this page object + # + # See gitlab-org/gitlab-qa#154 + # + view 'app/views/projects/new.html.haml' + def choose_test_namespace find('#s2id_project_namespace_id').click find('.select2-result-label', text: Runtime::Namespace.name).click diff --git a/qa/qa/page/project/settings/deploy_keys.rb b/qa/qa/page/project/settings/deploy_keys.rb index 4028b8cccc5..a8d6f09777c 100644 --- a/qa/qa/page/project/settings/deploy_keys.rb +++ b/qa/qa/page/project/settings/deploy_keys.rb @@ -3,6 +3,13 @@ module QA module Project module Settings class DeployKeys < Page::Base + ## + # TODO, define all selectors required by this page object + # + # See gitlab-org/gitlab-qa#154 + # + view 'app/views/projects/deploy_keys/edit.html.haml' + def fill_key_title(title) fill_in 'deploy_key_title', with: title end diff --git a/qa/qa/page/project/settings/repository.rb b/qa/qa/page/project/settings/repository.rb index 034b0d09c1c..524d87c6be9 100644 --- a/qa/qa/page/project/settings/repository.rb +++ b/qa/qa/page/project/settings/repository.rb @@ -5,6 +5,13 @@ module QA class Repository < Page::Base include Common + ## + # TODO, define all selectors required by this page object + # + # See gitlab-org/gitlab-qa#154 + # + view 'app/views/projects/settings/repository/show.html.haml' + def expand_deploy_keys(&block) expand('.qa-expand-deploy-keys') do DeployKeys.perform(&block) diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb index 3b2bac84f3f..90967e6cc15 100644 --- a/qa/qa/page/project/show.rb +++ b/qa/qa/page/project/show.rb @@ -2,6 +2,13 @@ module QA module Page module Project class Show < Page::Base + ## + # TODO, define all selectors required by this page object + # + # See gitlab-org/gitlab-qa#154 + # + view 'app/views/projects/show.html.haml' + def choose_repository_clone_http find('#clone-dropdown').click diff --git a/qa/qa/page/validator.rb b/qa/qa/page/validator.rb new file mode 100644 index 00000000000..117d8d4db67 --- /dev/null +++ b/qa/qa/page/validator.rb @@ -0,0 +1,52 @@ +module QA + module Page + class Validator + ValidationError = Class.new(StandardError) + + Error = Struct.new(:page, :message) do + def to_s + "Error: #{page} - #{message}" + end + end + + def initialize(constant) + @module = constant + end + + def constants + @consts ||= @module.constants.map do |const| + @module.const_get(const) + end + end + + def descendants + @descendants ||= constants.map do |const| + case const + when Class + const if const < Page::Base + when Module + Page::Validator.new(const).descendants + end + end + + @descendants.flatten.compact + end + + def errors + [].tap do |errors| + descendants.each do |page| + page.errors.each do |message| + errors.push(Error.new(page.name, message)) + end + end + end + end + + def validate! + return if errors.none? + + raise ValidationError, 'Page views / elements validation error!' + end + end + end +end diff --git a/qa/qa/page/view.rb b/qa/qa/page/view.rb new file mode 100644 index 00000000000..6635e1ce039 --- /dev/null +++ b/qa/qa/page/view.rb @@ -0,0 +1,55 @@ +module QA + module Page + class View + attr_reader :path, :elements + + def initialize(path, elements) + @path = path + @elements = elements + end + + def pathname + @pathname ||= Pathname.new(File.join(__dir__, '../../../', @path)) + .cleanpath.expand_path + end + + def errors + unless pathname.readable? + return ["Missing view partial `#{pathname}`!"] + end + + ## + # Reduce required elements by streaming view and making assertions on + # elements' existence. + # + @missing ||= @elements.dup.tap do |elements| + File.foreach(pathname.to_s) do |line| + elements.reject! { |element| element.matches?(line) } + end + end + + @missing.map do |missing| + "Missing element `#{missing.name}` in `#{pathname}` view partial!" + end + end + + def self.evaluate(&block) + Page::View::DSL.new.tap do |evaluator| + evaluator.instance_exec(&block) if block_given? + end + end + + class DSL + attr_reader :elements + + def initialize + @elements = [] + end + + def element(name, pattern = nil) + @elements.push(Page::Element.new(name, pattern)) + end + end + end + end +end diff --git a/qa/qa/scenario/test/sanity/selectors.rb b/qa/qa/scenario/test/sanity/selectors.rb new file mode 100644 index 00000000000..c87eb5f3dfb --- /dev/null +++ b/qa/qa/scenario/test/sanity/selectors.rb @@ -0,0 +1,54 @@ +module QA + module Scenario + module Test + module Sanity + class Selectors < Scenario::Template + include Scenario::Bootable + + PAGES = [QA::Page].freeze + + def perform(*) + validators = PAGES.map do |pages| + Page::Validator.new(pages) + end + + validators.map(&:errors).flatten.tap do |errors| + break if errors.none? + + warn <<~EOS + GitLab QA sanity selectors validation test detected problems + with your merge request! + + The purpose of this test is to make sure that GitLab QA tests, + that are entirely black-box, click-driven scenarios, do match + pages structure / layout in GitLab CE / EE repositories. + + It looks like you have changed views / pages / selectors, and + these are now out of sync with what we have defined in `qa/` + directory. + + Please update the code in `qa/` directory to make it match + current changes in this merge request. + + For more help see documentation in `qa/page/README.md` file or + ask for help on #qa channel on Slack (GitLab Team only). + + If you are not a Team Member, and you still need help to + contribute, please open an issue in GitLab QA issue tracker. + + Please see errors described below. + + EOS + + warn errors + end + + validators.each(&:validate!) + + puts 'Views / selectors validation passed!' + end + end + end + end + end +end diff --git a/qa/spec/page/base_spec.rb b/qa/spec/page/base_spec.rb new file mode 100644 index 00000000000..287adf35c46 --- /dev/null +++ b/qa/spec/page/base_spec.rb @@ -0,0 +1,63 @@ +describe QA::Page::Base do + describe 'page helpers' do + it 'exposes helpful page helpers' do + expect(subject).to respond_to :refresh, :wait, :scroll_to + end + end + + describe '.view', 'DSL for defining view partials' do + subject do + Class.new(described_class) do + view 'path/to/some/view.html.haml' do + element :something, 'string pattern' + element :something_else, /regexp pattern/ + end + + view 'path/to/some/_partial.html.haml' do + element :something, 'string pattern' + end + end + end + + it 'makes it possible to define page views' do + expect(subject.views.size).to eq 2 + expect(subject.views).to all(be_an_instance_of QA::Page::View) + end + + it 'populates views objects with data about elements' do + subject.views.first.elements.tap do |elements| + expect(elements.size).to eq 2 + expect(elements).to all(be_an_instance_of QA::Page::Element) + expect(elements.map(&:name)).to eq [:something, :something_else] + end + end + end + + describe '.errors' do + let(:view) { double('view') } + + context 'when page has views and elements defined' do + before do + allow(described_class).to receive(:views) + .and_return([view]) + + allow(view).to receive(:errors).and_return(['some error']) + end + + it 'iterates views composite and returns errors' do + expect(described_class.errors).to eq ['some error'] + end + end + + context 'when page has no views and elements defined' do + before do + allow(described_class).to receive(:views).and_return([]) + end + + it 'appends an error about missing views / elements block' do + expect(described_class.errors) + .to include 'Page class does not have views / elements defined!' + end + end + end +end diff --git a/qa/spec/page/element_spec.rb b/qa/spec/page/element_spec.rb new file mode 100644 index 00000000000..8598c57ad34 --- /dev/null +++ b/qa/spec/page/element_spec.rb @@ -0,0 +1,51 @@ +describe QA::Page::Element do + describe '#selector' do + it 'transforms element name into QA-specific selector' do + expect(described_class.new(:sign_in_button).selector) + .to eq 'qa-sign-in-button' + end + end + + describe '#selector_css' do + it 'transforms element name into QA-specific clickable css selector' do + expect(described_class.new(:sign_in_button).selector_css) + .to eq '.qa-sign-in-button' + end + end + + context 'when pattern is an expression' do + subject { described_class.new(:something, /button 'Sign in'/) } + + it 'matches when there is a match' do + expect(subject.matches?("button 'Sign in'")).to be true + end + + it 'does not match if pattern is not present' do + expect(subject.matches?("button 'Sign out'")).to be false + end + end + + context 'when pattern is a string' do + subject { described_class.new(:something, 'button') } + + it 'matches when there is match' do + expect(subject.matches?('some button in the view')).to be true + end + + it 'does not match if pattern is not present' do + expect(subject.matches?('text_field :name')).to be false + end + end + + context 'when pattern is not provided' do + subject { described_class.new(:some_name) } + + it 'matches when QA specific selector is present' do + expect(subject.matches?('some qa-some-name selector')).to be true + end + + it 'does not match if QA selector is not there' do + expect(subject.matches?('some_name selector')).to be false + end + end +end diff --git a/qa/spec/page/validator_spec.rb b/qa/spec/page/validator_spec.rb new file mode 100644 index 00000000000..02822d7d18f --- /dev/null +++ b/qa/spec/page/validator_spec.rb @@ -0,0 +1,79 @@ +describe QA::Page::Validator do + describe '#constants' do + subject do + described_class.new(QA::Page::Project) + end + + it 'returns all constants that are module children' do + expect(subject.constants) + .to include QA::Page::Project::New, QA::Page::Project::Settings + end + end + + describe '#descendants' do + subject do + described_class.new(QA::Page::Project) + end + + it 'recursively returns all descendants that are page objects' do + expect(subject.descendants) + .to include QA::Page::Project::New, QA::Page::Project::Settings::Repository + end + + it 'does not return modules that aggregate page objects' do + expect(subject.descendants) + .not_to include QA::Page::Project::Settings + end + end + + context 'when checking validation errors' do + let(:view) { spy('view') } + + before do + allow(QA::Page::Admin::Settings) + .to receive(:views).and_return([view]) + end + + subject do + described_class.new(QA::Page::Admin) + end + + context 'when there are no validation errors' do + before do + allow(view).to receive(:errors).and_return([]) + end + + describe '#errors' do + it 'does not return errors' do + expect(subject.errors).to be_empty + end + end + + describe '#validate!' do + it 'does not raise error' do + expect { subject.validate! }.not_to raise_error + end + end + end + + context 'when there are validation errors' do + before do + allow(view).to receive(:errors) + .and_return(['some error', 'another error']) + end + + describe '#errors' do + it 'returns errors' do + expect(subject.errors.count).to eq 2 + end + end + + describe '#validate!' do + it 'raises validation error' do + expect { subject.validate! } + .to raise_error described_class::ValidationError + end + end + end + end +end diff --git a/qa/spec/page/view_spec.rb b/qa/spec/page/view_spec.rb new file mode 100644 index 00000000000..aedbc3863a7 --- /dev/null +++ b/qa/spec/page/view_spec.rb @@ -0,0 +1,70 @@ +describe QA::Page::View do + let(:element) do + double('element', name: :something, pattern: /some element/) + end + + subject { described_class.new('some/file.html', [element]) } + + describe '.evaluate' do + it 'evaluates a block and returns a DSL object' do + results = described_class.evaluate do + element :something, 'my pattern' + element :something_else, /another pattern/ + end + + expect(results.elements.size).to eq 2 + end + end + + describe '#pathname' do + it 'returns an absolute and clean path to the view' do + expect(subject.pathname.to_s).not_to include 'qa/page/' + expect(subject.pathname.to_s).to include 'some/file.html' + end + end + + describe '#errors' do + context 'when view partial is present' do + before do + allow(subject.pathname).to receive(:readable?) + .and_return(true) + end + + context 'when pattern is found' do + before do + allow(File).to receive(:foreach) + .and_yield('some element').once + allow(element).to receive(:matches?) + .with('some element').and_return(true) + end + + it 'walks through the view and asserts on elements existence' do + expect(subject.errors).to be_empty + end + end + + context 'when pattern has not been found' do + before do + allow(File).to receive(:foreach) + .and_yield('some element').once + allow(element).to receive(:matches?) + .with('some element').and_return(false) + end + + it 'returns an array of errors related to missing elements' do + expect(subject.errors).not_to be_empty + expect(subject.errors.first) + .to match %r(Missing element `.*` in `.*/some/file.html` view) + end + end + end + + context 'when view partial has not been found' do + it 'returns an error when it is not able to find the partial' do + expect(subject.errors).to be_one + expect(subject.errors.first) + .to match %r(Missing view partial `.*/some/file.html`!) + end + end + end +end diff --git a/qa/spec/scenario/test/sanity/selectors_spec.rb b/qa/spec/scenario/test/sanity/selectors_spec.rb new file mode 100644 index 00000000000..45d21d54955 --- /dev/null +++ b/qa/spec/scenario/test/sanity/selectors_spec.rb @@ -0,0 +1,40 @@ +describe QA::Scenario::Test::Sanity::Selectors do + let(:validator) { spy('validator') } + + before do + stub_const('QA::Page::Validator', validator) + end + + context 'when there are errors detected' do + before do + allow(validator).to receive(:errors).and_return(['some error']) + end + + it 'outputs information about errors' do + expect { described_class.perform } + .to output(/some error/).to_stderr + + expect { described_class.perform } + .to output(/electors validation test detected problems/) + .to_stderr + end + end + + context 'when there are no errors detected' do + before do + allow(validator).to receive(:errors).and_return([]) + end + + it 'processes pages module' do + described_class.perform + + expect(validator).to have_received(:new).with(QA::Page) + end + + it 'triggers validation' do + described_class.perform + + expect(validator).to have_received(:validate!).at_least(:once) + end + end +end |