diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f64cbe..8a0803a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + - `have_fields_date_range` matcher for date range filter fields + - `within_table_footer` finder for table footer row + - `click_table_scope` action for clicking table scope links + - `table_header_selector` selector now accepts text and options (sortable, sort_direction, column) + - `have_table_header` matcher for table header columns + - `find_table_header` finder for table header columns + - `click_table_header` action for clicking sortable table headers + - `find_action_item` finder for action item elements + - `have_action_item_link` matcher for action item links (with optional href) + - `within_action_item_dropdown` finder for action item dropdown menu + - `have_status_tag` matcher for status tag elements + +### Changed + - `within_sidebar` now scopes within the sidebar section directly using `ancestor` + - `have_table_scope` now accepts an optional title as first positional argument and `selected:` keyword arg + ## [0.3.3] - 2020-04-17 ### Changed - `batch_action_selector`, `click_batch_action` finds element by link text diff --git a/lib/capybara/active_admin/actions/table.rb b/lib/capybara/active_admin/actions/table.rb index 25512f3..88d746c 100644 --- a/lib/capybara/active_admin/actions/table.rb +++ b/lib/capybara/active_admin/actions/table.rb @@ -16,6 +16,18 @@ def select_table_row(id: nil, index: nil) selector = %(input[id^="batch_action_item_"]) find_all(selector, minimum: index + 1)[index].click end + + # @param text [String] exact text of the scope to click. + def click_table_scope(text) + selector = "#{table_scopes_container_selector} > #{table_scope_selector}" + page.find(selector, exact_text: text).click + end + + # @param text [String] column header text. + # @param options [Hash] options passed to find_table_header. + def click_table_header(text, options = {}) + find_table_header(text, options).find('a').click + end end end end diff --git a/lib/capybara/active_admin/finders/layout.rb b/lib/capybara/active_admin/finders/layout.rb index aeeca1b..e20dbd8 100644 --- a/lib/capybara/active_admin/finders/layout.rb +++ b/lib/capybara/active_admin/finders/layout.rb @@ -16,11 +16,9 @@ def within_tab_body end def within_sidebar(title, exact: nil) - selector = sidebar_selector - - within(selector) do - within_panel(title, exact: exact) { yield } - end + opts = Util.options_with_text(title, exact: exact) + sidebar = page.find("#{sidebar_selector} .sidebar_section #{panel_title_selector}", **opts).ancestor('.sidebar_section') + within(sidebar) { yield } end def within_panel(title, exact: nil) @@ -35,6 +33,19 @@ def within_panel(title, exact: nil) def within_modal_dialog within(modal_dialog_selector) { yield } end + + # @param title [String] action item link text. + # @param exact [Boolean] whether to match the title exactly (default true). + # @return [Capybara::Node::Element] the found action item element. + def find_action_item(title, exact: true) + opts = exact ? { exact_text: title } : { text: title } + page.find(action_item_selector, **opts) + end + + # @yield within action item dropdown menu list. + def within_action_item_dropdown + within("#{action_item_selector} .dropdown_menu_list_wrapper") { yield } + end end end end diff --git a/lib/capybara/active_admin/finders/table.rb b/lib/capybara/active_admin/finders/table.rb index e974bb1..f777754 100644 --- a/lib/capybara/active_admin/finders/table.rb +++ b/lib/capybara/active_admin/finders/table.rb @@ -34,6 +34,23 @@ def find_table_row(id: nil, index: nil) find_all(selector, minimum: index + 1)[index] end + # @yield within table>tfoot>tr + def within_table_footer + within('tfoot > tr') { yield } + end + + # @param text [String] column header text. + # @param options [Hash] + # @option column [String, nil] column name override (defaults to text). + # @option sortable [Boolean] whether the column is sortable. + # @option sort_direction [String, nil] sort direction ('asc' or 'desc'). + # @return [Capybara::Node::Element] the found table header element. + def find_table_header(text, options = {}) + selector = table_header_selector(text, options) + opts = options.except(:column, :sortable, :sort_direction).merge(exact_text: text) + find(selector, **opts) + end + # @yield within table>tbody>tr>td def within_table_cell(name) cell = find_table_cell(name) diff --git a/lib/capybara/active_admin/matchers/form.rb b/lib/capybara/active_admin/matchers/form.rb index 1582a6f..9a4d0d2 100644 --- a/lib/capybara/active_admin/matchers/form.rb +++ b/lib/capybara/active_admin/matchers/form.rb @@ -33,6 +33,25 @@ def have_has_many_fields_for(association_name, options = {}) have_selector(selector, **options) end + # @param label [String] label text of the date range filter. + # @param options [Hash] + # @option from [String, nil] expected value of the "from" field. + # @option to [String, nil] expected value of the "to" field. + # @option exact [Boolean, nil] whether to match field values exactly. + # @example + # expect(page).to have_fields_date_range('Created At', from: '2020-01-01', to: '2020-12-31') + # + def have_fields_date_range(label, options = {}) + exact = options[:exact] + satisfy do |actual| + expect(actual).to have_selector('div.filter_date_range label', text: label) + container = actual.find('div.filter_date_range label', text: label).ancestor('div.filter_date_range') + base_name = container[:id].gsub(/_input\z/, '') + expect(container).to have_field("#{base_name}_gteq_datetime", with: options[:from].to_s, exact: exact) + expect(container).to have_field("#{base_name}_lteq_datetime", with: options[:to].to_s, exact: exact) + end + end + # @param text [String] button title # @param options [Hash] # @option selector [String, nil] optional selector to append diff --git a/lib/capybara/active_admin/matchers/layout.rb b/lib/capybara/active_admin/matchers/layout.rb index 3601ffd..871c672 100644 --- a/lib/capybara/active_admin/matchers/layout.rb +++ b/lib/capybara/active_admin/matchers/layout.rb @@ -44,6 +44,32 @@ def have_batch_action(title, exact: true) opts = Util.options_with_text(title, exact: exact) have_selector(selector, **opts) end + + # @param title [String] action item link text. + # @param exact [Boolean] whether to match the title exactly (default true). + # @param href [String, nil] expected href attribute of the link. + # @param options [Hash] additional options passed to have_selector. + # @example + # expect(page).to have_action_item_link('New User') + # expect(page).to have_action_item_link('New User', href: new_admin_user_path) + # + def have_action_item_link(title, exact: true, href: nil, **options) + opts = exact ? { exact_text: title } : { text: title } + opts.merge!(options) + selector = "#{action_item_selector} > a" + selector += "[href=\"#{href}\"]" if href.present? + have_selector(selector, **opts) + end + + # @param type [String, Symbol] status tag type (e.g. :yes, :no, :warning). + # @param options [Hash] additional options passed to have_selector (e.g. exact_text:). + # @example + # expect(page).to have_status_tag(:yes) + # expect(page).to have_status_tag(:error, exact_text: 'DOWN') + # + def have_status_tag(type, **options) + have_selector("span.status_tag.#{type}", **options) + end end end end diff --git a/lib/capybara/active_admin/matchers/table.rb b/lib/capybara/active_admin/matchers/table.rb index ba91752..f4eee6f 100644 --- a/lib/capybara/active_admin/matchers/table.rb +++ b/lib/capybara/active_admin/matchers/table.rb @@ -60,16 +60,34 @@ def have_table_scopes(options = {}) have_selector("#{table_scopes_container_selector} > #{table_scope_selector}", **options) end - # @param options [Hash] - # @option exact_text [String] title of scope - # @option counter [Integer,String,nil] counter value in brackets (nil if skipped) - # @option selected [Boolean] is scope active (default false) - def have_table_scope(options = {}) - active = options.delete(:active) + # @param title [String, nil] exact title of scope to match. + # @param selected [Boolean] whether to match selected (active) scope only (default false). + # @param options [Hash] additional options passed to have_selector. + def have_table_scope(title = nil, selected: false, **options) selector = "#{table_scopes_container_selector} > #{table_scope_selector}" - selector = active ? "#{selector}.selected" : "#{selector}:not(.selected)" + if title.nil? + selector = selected ? "#{selector}.selected" : "#{selector}:not(.selected)" + have_selector(selector, **options) + else + selector = selected ? "#{selector}.selected" : selector + have_selector(selector, exact_text: title.to_s, **options) + end + end - have_selector(selector, **options) + # @param text [String] column header text. + # @param options [Hash] + # @option column [String, nil] column name override (defaults to text). + # @option sortable [Boolean] whether the column is sortable. + # @option sort_direction [String, nil] sort direction ('asc' or 'desc'). + # @example + # expect(page).to have_table_header('Full Name') + # expect(page).to have_table_header('Full Name', sortable: true) + # expect(page).to have_table_header('Full Name', sort_direction: :asc) + # + def have_table_header(text, options = {}) + selector = table_header_selector(text, options) + opts = options.except(:column, :sortable, :sort_direction).merge(exact_text: text) + have_selector(selector, **opts) end end end diff --git a/lib/capybara/active_admin/selectors/table.rb b/lib/capybara/active_admin/selectors/table.rb index 10b84da..08c1dfc 100644 --- a/lib/capybara/active_admin/selectors/table.rb +++ b/lib/capybara/active_admin/selectors/table.rb @@ -22,9 +22,20 @@ def table_row_selector(record_id = nil) %(tbody > tr[id$="_#{record_id}"]) end + # @param text [String] column header text. + # @param options [Hash] + # @option column [String, nil] column name override (defaults to text). + # @option sortable [Boolean] whether column is sortable. + # @option sort_direction [String, nil] sort direction ('asc' or 'desc'). # @return selector. - def table_header_selector - 'thead > tr > th.col' + def table_header_selector(text = nil, options = {}) + return 'thead > tr > th.col' if text.nil? + + column = (options[:column] || text).to_s.tr(' ', '_').downcase + selector = "th.col.col-#{column}" + selector += '.sortable' if options[:sortable] + selector += ".sorted-#{options[:sort_direction].to_s.downcase}" if options[:sort_direction].present? + "thead > tr > #{selector}" end # @param column [String, nil] column name.