diff options
21 files changed, 205 insertions, 30 deletions
@@ -411,6 +411,7 @@ group :test do gem 'concurrent-ruby', '~> 1.1' gem 'test-prof', '~> 0.10.0' gem 'rspec_junit_formatter' + gem 'guard-rspec' end gem 'octokit', '~> 4.9' diff --git a/Gemfile.lock b/Gemfile.lock index 5b7785b9475..15465cd6b03 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -446,6 +446,20 @@ GEM googleapis-common-protos-types (~> 1.0) gssapi (1.2.0) ffi (>= 1.0.1) + guard (2.15.1) + formatador (>= 0.2.4) + listen (>= 2.7, < 4.0) + lumberjack (>= 1.0.12, < 2.0) + nenv (~> 0.1) + notiffany (~> 0.0) + pry (>= 0.9.12) + shellany (~> 0.0) + thor (>= 0.18.1) + guard-compat (1.2.1) + guard-rspec (4.7.3) + guard (~> 2.1) + guard-compat (~> 1.1) + rspec (>= 2.99.0, < 4.0) haml (5.0.4) temple (>= 0.8.0) tilt @@ -561,6 +575,10 @@ GEM xml-simple licensee (8.9.2) rugged (~> 0.24) + listen (3.1.5) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + ruby_dep (~> 1.2) locale (2.1.2) lograge (0.10.0) actionpack (>= 4) @@ -570,6 +588,7 @@ GEM loofah (2.3.1) crass (~> 1.0.2) nokogiri (>= 1.5.9) + lumberjack (1.0.13) mail (2.7.1) mini_mime (>= 0.1.1) mail_room (0.9.1) @@ -598,6 +617,7 @@ GEM mustermann (~> 1.0.0) nakayoshi_fork (0.0.4) nap (1.1.0) + nenv (0.3.0) net-ldap (0.16.0) net-ntp (2.1.3) net-ssh (5.2.0) @@ -608,6 +628,9 @@ GEM mini_portile2 (~> 2.4.0) nokogumbo (1.5.0) nokogiri + notiffany (0.1.3) + nenv (~> 0.1) + shellany (~> 0.0) numerizer (0.1.1) oauth (0.5.4) oauth2 (1.4.1) @@ -898,6 +921,7 @@ GEM ruby-progressbar (1.10.1) ruby-saml (1.7.2) nokogiri (>= 1.5.10) + ruby_dep (1.5.0) ruby_parser (3.13.1) sexp_processor (~> 4.9) rubyntlm (0.6.2) @@ -939,6 +963,7 @@ GEM faraday (>= 0.7.6, < 1.0) settingslogic (2.0.9) sexp_processor (4.12.0) + shellany (0.0.1) shoulda-matchers (4.0.1) activesupport (>= 4.2.0) sidekiq (5.2.7) @@ -1193,6 +1218,7 @@ DEPENDENCIES graphql-docs (~> 1.6.0) grpc (~> 1.24.0) gssapi + guard-rspec haml_lint (~> 0.31.0) hamlit (~> 2.8.8) hangouts-chat (~> 0.0.5) diff --git a/Guardfile b/Guardfile new file mode 100644 index 00000000000..8a43f414ca9 --- /dev/null +++ b/Guardfile @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# More info at https://github.com/guard/guard#readme + +cmd = ENV['SPRING'] ? 'spring rspec' : 'bundle exec rspec' + +guard :rspec, cmd: cmd do + require "guard/rspec/dsl" + dsl = Guard::RSpec::Dsl.new(self) + + directories %w(app ee lib spec) + + # RSpec files + rspec = dsl.rspec + watch(rspec.spec_helper) { rspec.spec_dir } + watch(rspec.spec_support) { rspec.spec_dir } + watch(rspec.spec_files) + + # Ruby files + ruby = dsl.ruby + dsl.watch_spec_files_for(ruby.lib_files) + + # Rails files + rails = dsl.rails(view_extensions: %w(erb haml slim)) + dsl.watch_spec_files_for(rails.app_files) + dsl.watch_spec_files_for(rails.views) + + watch(rails.controllers) do |m| + [ + rspec.spec.call("routing/#{m[1]}_routing"), + rspec.spec.call("controllers/#{m[1]}_controller") + ] + end + + # Rails config changes + watch(rails.spec_helper) { rspec.spec_dir } + watch(rails.routes) { "#{rspec.spec_dir}/routing" } + watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" } + + # Capybara features specs + watch(rails.view_dirs) { |m| rspec.spec.call("features/#{m[1]}") } + watch(rails.layouts) { |m| rspec.spec.call("features/#{m[1]}") } +end diff --git a/app/assets/javascripts/boards/components/modal/index.vue b/app/assets/javascripts/boards/components/modal/index.vue index defa1f75ba2..618c2ada1f8 100644 --- a/app/assets/javascripts/boards/components/modal/index.vue +++ b/app/assets/javascripts/boards/components/modal/index.vue @@ -1,6 +1,7 @@ <script> /* global ListIssue */ import { urlParamsToObject } from '~/lib/utils/common_utils'; +import boardsStore from '~/boards/stores/boards_store'; import ModalHeader from './header.vue'; import ModalList from './list.vue'; import ModalFooter from './footer.vue'; @@ -109,7 +110,7 @@ export default { loadIssues(clearIssues = false) { if (!this.showAddIssuesModal) return false; - return gl.boardService + return boardsStore .getBacklog({ ...urlParamsToObject(this.filter.path), page: this.page, diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 1809288311a..c8ab47888d0 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -1,5 +1,5 @@ -# DANGER: Any changes to this file need to be reflected in issuables_list/components/issuable.vue! -%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue), data: { labels: issue.label_ids, id: issue.id } } +%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue), data: { labels: issue.label_ids, id: issue.id, qa_selector: 'issue', qa_issue_title: issue.title } } .issue-box - if @can_bulk_update .issue-check.hidden diff --git a/changelogs/unreleased/Update-boards-components-models-index-vue.yml b/changelogs/unreleased/Update-boards-components-models-index-vue.yml new file mode 100644 index 00000000000..e328c6ca0da --- /dev/null +++ b/changelogs/unreleased/Update-boards-components-models-index-vue.yml @@ -0,0 +1,5 @@ +--- +title: Remove all references to BoardsService in index.vue +merge_request: 20152 +author: nuwe1 +type: other diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md index c79642d8fed..fe3989474e6 100644 --- a/doc/development/testing_guide/best_practices.md +++ b/doc/development/testing_guide/best_practices.md @@ -44,6 +44,14 @@ bundle exec rspec bundle exec rspec spec/[path]/[to]/[spec].rb ``` +Use [guard](https://github.com/guard/guard) to continuously monitor for changes and only run matching tests: + +```sh +bundle exec guard +``` + +When using spring and guard together, use `SPRING=1 bundle exec guard` instead to make use of spring. + ### General guidelines - Use a single, top-level `describe ClassName` block. diff --git a/doc/development/testing_guide/end_to_end/page_objects.md b/doc/development/testing_guide/end_to_end/page_objects.md index 28111c18378..554995fa2e2 100644 --- a/doc/development/testing_guide/end_to_end/page_objects.md +++ b/doc/development/testing_guide/end_to_end/page_objects.md @@ -167,6 +167,65 @@ There are two supported methods of defining elements within a view. Any existing `.qa-selector` class should be considered deprecated and we should prefer the `data-qa-selector` method of definition. +### Dynamic element selection + +> Introduced in GitLab 12.5 + +A common occurrence in automated testing is selecting a single "one-of-many" element. +In a list of several items, how do you differentiate what you are selecting on? +The most common workaround for this is via text matching. Instead, a better practice is +by matching on that specific element by a unique identifier, rather than by text. + +We got around this by adding the `data-qa-*` extensible selection mechanism. + +#### Examples + +**Example 1** + +Given the following Rails view (using GitLab Issues as an example): + +```haml +%ul.issues-list + - @issues.each do |issue| + %li.issue{data: { qa_selector: 'issue', qa_issue_title: issue.title } }= link_to issue +``` + +We can select on that specific issue by matching on the Rails model. + +```ruby +class Page::Project::Issues::Index < Page::Base + def has_issue?(issue) + has_element? :issue, issue_title: issue + end +end +``` + +In our test, we can validate that this particular issue exists. + +```ruby +describe 'Issue' do + it 'has an issue titled "hello"' do + Page::Project::Issues::Index.perform do |index| + expect(index).to have_issue('hello') + end + end +end +``` + +**Example 2** + +*By an index...* + +```haml +%ol + - @some_model.each_with_index do |model, idx| + %li.model{ data: { qa_selector: 'model', qa_index: idx } } +``` + +```ruby +expect(the_page).to have_element(:model, index: 1) #=> select on the first model that appears in the list +``` + ### Exceptions In some cases it might not be possible or worthwhile to add a selector. diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb index c256a895718..ed4d33dc7a3 100644 --- a/qa/qa/page/base.rb +++ b/qa/qa/page/base.rb @@ -111,12 +111,18 @@ module QA element.select value end - def has_element?(name, text: nil, wait: Capybara.default_max_wait_time) - has_css?(element_selector_css(name), wait: wait, text: text) + def has_element?(name, **kwargs) + wait = kwargs[:wait] ? kwargs[:wait] && kwargs.delete(:wait) : Capybara.default_max_wait_time + text = kwargs[:text] ? kwargs[:text] && kwargs.delete(:text) : nil + + has_css?(element_selector_css(name, kwargs), text: text, wait: wait) end - def has_no_element?(name, text: nil, wait: Capybara.default_max_wait_time) - has_no_css?(element_selector_css(name), wait: wait, text: text) + def has_no_element?(name, **kwargs) + wait = kwargs[:wait] ? kwargs[:wait] && kwargs.delete(:wait) : Capybara.default_max_wait_time + text = kwargs[:text] ? kwargs[:text] && kwargs.delete(:text) : nil + + has_no_css?(element_selector_css(name, kwargs), wait: wait, text: text) end def has_text?(text) @@ -199,8 +205,8 @@ module QA scroll_to(element_selector_css(name), *args) end - def element_selector_css(name) - Page::Element.new(name).selector_css + def element_selector_css(name, *attributes) + Page::Element.new(name, *attributes).selector_css end def click_link_with_text(text) diff --git a/qa/qa/page/element.rb b/qa/qa/page/element.rb index 9e6fd2fdd4f..6bfdf98587b 100644 --- a/qa/qa/page/element.rb +++ b/qa/qa/page/element.rb @@ -28,7 +28,7 @@ module QA end def selector_css - %Q([data-qa-selector="#{@name}"],.#{selector}) + %Q([data-qa-selector="#{@name}"]#{additional_selectors},.#{selector}) end def expression @@ -42,6 +42,14 @@ module QA def matches?(line) !!(line =~ /["']#{name}['"]|#{expression}/) end + + private + + def additional_selectors + @attributes.dup.delete_if { |attr| attr == :pattern || attr == :required }.map do |key, value| + %Q([data-qa-#{key.to_s.tr('_', '-')}="#{value}"]) + end.join + end end end end diff --git a/qa/qa/page/project/issue/index.rb b/qa/qa/page/project/issue/index.rb index befee25b37a..a6ccee4353b 100644 --- a/qa/qa/page/project/issue/index.rb +++ b/qa/qa/page/project/issue/index.rb @@ -36,6 +36,10 @@ module QA def click_closed_issues_link click_element :closed_issues_link end + + def has_issue?(issue) + has_element? :issue, issue_title: issue.to_s + end end end end diff --git a/qa/qa/resource/issue.rb b/qa/qa/resource/issue.rb index 0817a9de06f..3bcff6a10ac 100644 --- a/qa/qa/resource/issue.rb +++ b/qa/qa/resource/issue.rb @@ -38,6 +38,10 @@ module QA end end + def to_s + @title + end + def api_get_path "/projects/#{project.id}/issues/#{id}" end diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb index 35cd64aa58e..69389672a6d 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb @@ -7,8 +7,7 @@ module QA QA::Runtime::Env.personal_access_token = QA::Runtime::Env.admin_personal_access_token unless QA::Runtime::Env.personal_access_token - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.perform(&:sign_in_using_admin_credentials) + Flow::Login.sign_in_as_admin end user = Resource::User.fabricate_via_api! do |user| @@ -20,9 +19,7 @@ module QA Page::Main::Menu.perform(&:sign_out) if Page::Main::Menu.perform { |p| p.has_personal_area?(wait: 0) } - Runtime::Browser.visit(:gitlab, Page::Main::Login) - - Page::Main::Login.perform(&:sign_in_using_credentials) + Flow::Login.sign_in project = Resource::Project.fabricate_via_api! do |resource| resource.name = 'xss-test-for-mentions-project' diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/close_issue_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/close_issue_spec.rb index 2bcc89cb338..dc7fa9f3859 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/close_issue_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/close_issue_spec.rb @@ -7,8 +7,7 @@ module QA let(:commit_message) { 'Closes' } before do - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.perform(&:sign_in_using_credentials) + Flow::Login.sign_in issue = Resource::Issue.fabricate_via_api! do |issue| issue.title = issue_title diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/collapse_comments_in_discussions_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/collapse_comments_in_discussions_spec.rb index ad70f6813fb..77fcc4e9b6a 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/collapse_comments_in_discussions_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/collapse_comments_in_discussions_spec.rb @@ -6,8 +6,7 @@ module QA let(:my_first_reply) { 'My first reply' } before do - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.perform(&:sign_in_using_credentials) + Flow::Login.sign_in issue = Resource::Issue.fabricate_via_api! do |issue| issue.title = 'issue title' diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/comment_issue_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/comment_issue_spec.rb index 0b1bd00ac8d..77489c0ecf5 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/comment_issue_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/comment_issue_spec.rb @@ -4,8 +4,7 @@ module QA context 'Plan' do describe 'Issue comments' do before do - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.perform(&:sign_in_using_credentials) + Flow::Login.sign_in issue = Resource::Issue.fabricate_via_api! do |issue| issue.title = 'issue title' diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb index 300bf59eba4..254efb741b3 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb @@ -6,18 +6,19 @@ module QA let(:issue_title) { 'issue title' } before do - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.perform(&:sign_in_using_credentials) + Flow::Login.sign_in end it 'user creates an issue' do - Resource::Issue.fabricate_via_browser_ui! do |issue| + issue = Resource::Issue.fabricate_via_browser_ui! do |issue| issue.title = issue_title end Page::Project::Menu.perform(&:click_issues) - expect(page).to have_content(issue_title) + Page::Project::Issue::Index.perform do |index| + expect(index).to have_issue(issue) + end end context 'when using attachments in comments', :object_storage do diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb index 317e31feea8..a4f6b0bb1bf 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb @@ -6,8 +6,7 @@ module QA let(:issue_title) { 'issue title' } before do - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.perform(&:sign_in_using_credentials) + Flow::Login.sign_in issue = Resource::Issue.fabricate_via_api! do |issue| issue.title = issue_title diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/issue_suggestions_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/issue_suggestions_spec.rb index c42c2cedde0..e15afd1f576 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/issue_suggestions_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/issue_suggestions_spec.rb @@ -6,8 +6,7 @@ module QA let(:issue_title) { 'Issue Lists are awesome' } before do - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.perform(&:sign_in_using_credentials) + Flow::Login.sign_in project = Resource::Project.fabricate_via_api! do |resource| resource.name = 'project-for-issue-suggestions' diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb index 45c14d0537c..b1a80ad75cd 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb @@ -4,8 +4,7 @@ module QA context 'Plan', :smoke do describe 'mention' do before do - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.perform(&:sign_in_using_credentials) + Flow::Login.sign_in @user = Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) diff --git a/qa/spec/page/element_spec.rb b/qa/spec/page/element_spec.rb index ff5e118cefa..3f64743ffac 100644 --- a/qa/spec/page/element_spec.rb +++ b/qa/spec/page/element_spec.rb @@ -117,5 +117,23 @@ describe QA::Page::Element do it 'properly translates to a data-qa-selector' do expect(subject.selector_css).to include(%q([data-qa-selector="my_element"])) end + + context 'additional selectors' do + let(:element) { described_class.new(:my_element, index: 3, another_match: 'something') } + let(:required_element) { described_class.new(:my_element, required: true, index: 3) } + + it 'matches on additional data-qa properties' do + expect(element.selector_css).to include(%q([data-qa-selector="my_element"][data-qa-index="3"])) + end + + it 'doesnt conflict with element requirement' do + expect(required_element).to be_required + expect(required_element.selector_css).not_to include(%q(data-qa-required)) + end + + it 'translates snake_case to kebab-case' do + expect(element.selector_css).to include(%q(data-qa-another-match)) + end + end end end |