capybara 2.18.0 → 3.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.yardopts +1 -1
- data/History.md +945 -12
- data/License.txt +1 -1
- data/README.md +264 -90
- data/lib/capybara/config.rb +29 -57
- data/lib/capybara/cucumber.rb +2 -3
- data/lib/capybara/driver/base.rb +35 -18
- data/lib/capybara/driver/node.rb +40 -10
- data/lib/capybara/dsl.rb +10 -7
- data/lib/capybara/helpers.rb +70 -31
- data/lib/capybara/minitest/spec.rb +185 -83
- data/lib/capybara/minitest.rb +232 -112
- data/lib/capybara/node/actions.rb +274 -171
- data/lib/capybara/node/base.rb +42 -34
- data/lib/capybara/node/document.rb +15 -3
- data/lib/capybara/node/document_matchers.rb +19 -21
- data/lib/capybara/node/element.rb +362 -135
- data/lib/capybara/node/finders.rb +151 -137
- data/lib/capybara/node/matchers.rb +394 -209
- data/lib/capybara/node/simple.rb +59 -26
- data/lib/capybara/node/whitespace_normalizer.rb +81 -0
- data/lib/capybara/queries/active_element_query.rb +18 -0
- data/lib/capybara/queries/ancestor_query.rb +12 -9
- data/lib/capybara/queries/base_query.rb +39 -28
- data/lib/capybara/queries/current_path_query.rb +21 -27
- data/lib/capybara/queries/match_query.rb +14 -7
- data/lib/capybara/queries/selector_query.rb +659 -149
- data/lib/capybara/queries/sibling_query.rb +11 -9
- data/lib/capybara/queries/style_query.rb +45 -0
- data/lib/capybara/queries/text_query.rb +56 -38
- data/lib/capybara/queries/title_query.rb +8 -11
- data/lib/capybara/rack_test/browser.rb +114 -42
- data/lib/capybara/rack_test/css_handlers.rb +6 -4
- data/lib/capybara/rack_test/driver.rb +22 -17
- data/lib/capybara/rack_test/errors.rb +6 -0
- data/lib/capybara/rack_test/form.rb +93 -58
- data/lib/capybara/rack_test/node.rb +184 -81
- data/lib/capybara/rails.rb +3 -7
- data/lib/capybara/registration_container.rb +41 -0
- data/lib/capybara/registrations/drivers.rb +42 -0
- data/lib/capybara/registrations/patches/puma_ssl.rb +29 -0
- data/lib/capybara/registrations/servers.rb +66 -0
- data/lib/capybara/result.rb +97 -63
- data/lib/capybara/rspec/features.rb +17 -50
- data/lib/capybara/rspec/matcher_proxies.rb +52 -15
- data/lib/capybara/rspec/matchers/base.rb +113 -0
- data/lib/capybara/rspec/matchers/become_closed.rb +33 -0
- data/lib/capybara/rspec/matchers/compound.rb +88 -0
- data/lib/capybara/rspec/matchers/count_sugar.rb +37 -0
- data/lib/capybara/rspec/matchers/have_ancestor.rb +28 -0
- data/lib/capybara/rspec/matchers/have_current_path.rb +29 -0
- data/lib/capybara/rspec/matchers/have_selector.rb +69 -0
- data/lib/capybara/rspec/matchers/have_sibling.rb +27 -0
- data/lib/capybara/rspec/matchers/have_text.rb +33 -0
- data/lib/capybara/rspec/matchers/have_title.rb +29 -0
- data/lib/capybara/rspec/matchers/match_selector.rb +27 -0
- data/lib/capybara/rspec/matchers/match_style.rb +43 -0
- data/lib/capybara/rspec/matchers/spatial_sugar.rb +39 -0
- data/lib/capybara/rspec/matchers.rb +146 -310
- data/lib/capybara/rspec.rb +7 -11
- data/lib/capybara/selector/builders/css_builder.rb +84 -0
- data/lib/capybara/selector/builders/xpath_builder.rb +71 -0
- data/lib/capybara/selector/css.rb +85 -13
- data/lib/capybara/selector/definition/button.rb +68 -0
- data/lib/capybara/selector/definition/checkbox.rb +26 -0
- data/lib/capybara/selector/definition/css.rb +10 -0
- data/lib/capybara/selector/definition/datalist_input.rb +35 -0
- data/lib/capybara/selector/definition/datalist_option.rb +25 -0
- data/lib/capybara/selector/definition/element.rb +28 -0
- data/lib/capybara/selector/definition/field.rb +40 -0
- data/lib/capybara/selector/definition/fieldset.rb +14 -0
- data/lib/capybara/selector/definition/file_field.rb +13 -0
- data/lib/capybara/selector/definition/fillable_field.rb +33 -0
- data/lib/capybara/selector/definition/frame.rb +17 -0
- data/lib/capybara/selector/definition/id.rb +6 -0
- data/lib/capybara/selector/definition/label.rb +62 -0
- data/lib/capybara/selector/definition/link.rb +55 -0
- data/lib/capybara/selector/definition/link_or_button.rb +16 -0
- data/lib/capybara/selector/definition/option.rb +27 -0
- data/lib/capybara/selector/definition/radio_button.rb +27 -0
- data/lib/capybara/selector/definition/select.rb +81 -0
- data/lib/capybara/selector/definition/table.rb +109 -0
- data/lib/capybara/selector/definition/table_row.rb +21 -0
- data/lib/capybara/selector/definition/xpath.rb +5 -0
- data/lib/capybara/selector/definition.rb +280 -0
- data/lib/capybara/selector/filter.rb +2 -17
- data/lib/capybara/selector/filter_set.rb +80 -33
- data/lib/capybara/selector/filters/base.rb +50 -6
- data/lib/capybara/selector/filters/expression_filter.rb +8 -26
- data/lib/capybara/selector/filters/locator_filter.rb +29 -0
- data/lib/capybara/selector/filters/node_filter.rb +16 -12
- data/lib/capybara/selector/regexp_disassembler.rb +211 -0
- data/lib/capybara/selector/selector.rb +93 -210
- data/lib/capybara/selector/xpath_extensions.rb +17 -0
- data/lib/capybara/selector.rb +475 -523
- data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -0
- data/lib/capybara/selenium/atoms/isDisplayed.min.js +1 -0
- data/lib/capybara/selenium/atoms/src/getAttribute.js +161 -0
- data/lib/capybara/selenium/atoms/src/isDisplayed.js +454 -0
- data/lib/capybara/selenium/driver.rb +298 -267
- data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +117 -0
- data/lib/capybara/selenium/driver_specializations/edge_driver.rb +128 -0
- data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +84 -0
- data/lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb +26 -0
- data/lib/capybara/selenium/driver_specializations/safari_driver.rb +24 -0
- data/lib/capybara/selenium/extensions/file_input_click_emulation.rb +34 -0
- data/lib/capybara/selenium/extensions/find.rb +110 -0
- data/lib/capybara/selenium/extensions/html5_drag.rb +229 -0
- data/lib/capybara/selenium/extensions/modifier_keys_stack.rb +28 -0
- data/lib/capybara/selenium/extensions/scroll.rb +76 -0
- data/lib/capybara/selenium/node.rb +517 -145
- data/lib/capybara/selenium/nodes/chrome_node.rb +125 -0
- data/lib/capybara/selenium/nodes/edge_node.rb +110 -0
- data/lib/capybara/selenium/nodes/firefox_node.rb +136 -0
- data/lib/capybara/selenium/nodes/ie_node.rb +22 -0
- data/lib/capybara/selenium/nodes/safari_node.rb +118 -0
- data/lib/capybara/selenium/patches/atoms.rb +18 -0
- data/lib/capybara/selenium/patches/is_displayed.rb +16 -0
- data/lib/capybara/selenium/patches/logs.rb +45 -0
- data/lib/capybara/selenium/patches/pause_duration_fix.rb +9 -0
- data/lib/capybara/selenium/patches/persistent_client.rb +20 -0
- data/lib/capybara/server/animation_disabler.rb +80 -0
- data/lib/capybara/server/checker.rb +44 -0
- data/lib/capybara/server/middleware.rb +71 -0
- data/lib/capybara/server.rb +59 -67
- data/lib/capybara/session/config.rb +81 -67
- data/lib/capybara/session/matchers.rb +28 -20
- data/lib/capybara/session.rb +337 -365
- data/lib/capybara/spec/public/jquery.js +5 -5
- data/lib/capybara/spec/public/offset.js +6 -0
- data/lib/capybara/spec/public/test.js +151 -12
- data/lib/capybara/spec/session/accept_alert_spec.rb +12 -11
- data/lib/capybara/spec/session/accept_confirm_spec.rb +6 -5
- data/lib/capybara/spec/session/accept_prompt_spec.rb +10 -10
- data/lib/capybara/spec/session/active_element_spec.rb +31 -0
- data/lib/capybara/spec/session/all_spec.rb +161 -57
- data/lib/capybara/spec/session/ancestor_spec.rb +27 -24
- data/lib/capybara/spec/session/assert_all_of_selectors_spec.rb +68 -38
- data/lib/capybara/spec/session/assert_current_path_spec.rb +75 -0
- data/lib/capybara/spec/session/assert_selector_spec.rb +143 -0
- data/lib/capybara/spec/session/assert_style_spec.rb +26 -0
- data/lib/capybara/spec/session/assert_text_spec.rb +258 -0
- data/lib/capybara/spec/session/{assert_title.rb → assert_title_spec.rb} +22 -12
- data/lib/capybara/spec/session/attach_file_spec.rb +144 -69
- data/lib/capybara/spec/session/body_spec.rb +12 -13
- data/lib/capybara/spec/session/check_spec.rb +117 -55
- data/lib/capybara/spec/session/choose_spec.rb +64 -31
- data/lib/capybara/spec/session/click_button_spec.rb +231 -173
- data/lib/capybara/spec/session/click_link_or_button_spec.rb +55 -35
- data/lib/capybara/spec/session/click_link_spec.rb +93 -58
- data/lib/capybara/spec/session/current_scope_spec.rb +12 -11
- data/lib/capybara/spec/session/current_url_spec.rb +57 -39
- data/lib/capybara/spec/session/dismiss_confirm_spec.rb +4 -4
- data/lib/capybara/spec/session/dismiss_prompt_spec.rb +3 -2
- data/lib/capybara/spec/session/element/{assert_match_selector.rb → assert_match_selector_spec.rb} +11 -9
- data/lib/capybara/spec/session/element/match_css_spec.rb +18 -10
- data/lib/capybara/spec/session/element/match_xpath_spec.rb +9 -7
- data/lib/capybara/spec/session/element/matches_selector_spec.rb +71 -57
- data/lib/capybara/spec/session/evaluate_async_script_spec.rb +8 -7
- data/lib/capybara/spec/session/evaluate_script_spec.rb +29 -8
- data/lib/capybara/spec/session/execute_script_spec.rb +10 -8
- data/lib/capybara/spec/session/fill_in_spec.rb +134 -43
- data/lib/capybara/spec/session/find_button_spec.rb +25 -24
- data/lib/capybara/spec/session/find_by_id_spec.rb +10 -9
- data/lib/capybara/spec/session/find_field_spec.rb +37 -41
- data/lib/capybara/spec/session/find_link_spec.rb +46 -17
- data/lib/capybara/spec/session/find_spec.rb +260 -145
- data/lib/capybara/spec/session/first_spec.rb +80 -52
- data/lib/capybara/spec/session/frame/frame_title_spec.rb +23 -0
- data/lib/capybara/spec/session/frame/frame_url_spec.rb +23 -0
- data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +33 -20
- data/lib/capybara/spec/session/frame/within_frame_spec.rb +52 -32
- data/lib/capybara/spec/session/go_back_spec.rb +2 -1
- data/lib/capybara/spec/session/go_forward_spec.rb +2 -1
- data/lib/capybara/spec/session/has_all_selectors_spec.rb +31 -31
- data/lib/capybara/spec/session/has_ancestor_spec.rb +46 -0
- data/lib/capybara/spec/session/has_any_selectors_spec.rb +29 -0
- data/lib/capybara/spec/session/has_button_spec.rb +100 -13
- data/lib/capybara/spec/session/has_css_spec.rb +272 -137
- data/lib/capybara/spec/session/has_current_path_spec.rb +60 -61
- data/lib/capybara/spec/session/has_element_spec.rb +47 -0
- data/lib/capybara/spec/session/has_field_spec.rb +139 -59
- data/lib/capybara/spec/session/has_link_spec.rb +47 -6
- data/lib/capybara/spec/session/has_none_selectors_spec.rb +42 -40
- data/lib/capybara/spec/session/has_select_spec.rb +107 -72
- data/lib/capybara/spec/session/has_selector_spec.rb +120 -71
- data/lib/capybara/spec/session/has_sibling_spec.rb +50 -0
- data/lib/capybara/spec/session/has_table_spec.rb +183 -5
- data/lib/capybara/spec/session/has_text_spec.rb +106 -62
- data/lib/capybara/spec/session/has_title_spec.rb +20 -14
- data/lib/capybara/spec/session/has_xpath_spec.rb +57 -38
- data/lib/capybara/spec/session/{headers.rb → headers_spec.rb} +3 -2
- data/lib/capybara/spec/session/html_spec.rb +14 -6
- data/lib/capybara/spec/session/matches_style_spec.rb +37 -0
- data/lib/capybara/spec/session/node_spec.rb +1024 -153
- data/lib/capybara/spec/session/node_wrapper_spec.rb +39 -0
- data/lib/capybara/spec/session/refresh_spec.rb +12 -6
- data/lib/capybara/spec/session/reset_session_spec.rb +82 -35
- data/lib/capybara/spec/session/{response_code.rb → response_code_spec.rb} +2 -1
- data/lib/capybara/spec/session/save_and_open_page_spec.rb +3 -2
- data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +8 -12
- data/lib/capybara/spec/session/save_page_spec.rb +42 -55
- data/lib/capybara/spec/session/save_screenshot_spec.rb +16 -14
- data/lib/capybara/spec/session/screenshot_spec.rb +2 -2
- data/lib/capybara/spec/session/scroll_spec.rb +119 -0
- data/lib/capybara/spec/session/select_spec.rb +107 -81
- data/lib/capybara/spec/session/selectors_spec.rb +52 -19
- data/lib/capybara/spec/session/sibling_spec.rb +10 -10
- data/lib/capybara/spec/session/text_spec.rb +37 -21
- data/lib/capybara/spec/session/title_spec.rb +17 -5
- data/lib/capybara/spec/session/uncheck_spec.rb +43 -23
- data/lib/capybara/spec/session/unselect_spec.rb +39 -38
- data/lib/capybara/spec/session/visit_spec.rb +85 -53
- data/lib/capybara/spec/session/window/become_closed_spec.rb +24 -20
- data/lib/capybara/spec/session/window/current_window_spec.rb +5 -3
- data/lib/capybara/spec/session/window/open_new_window_spec.rb +5 -3
- data/lib/capybara/spec/session/window/switch_to_window_spec.rb +27 -22
- data/lib/capybara/spec/session/window/window_opened_by_spec.rb +12 -6
- data/lib/capybara/spec/session/window/window_spec.rb +97 -63
- data/lib/capybara/spec/session/window/windows_spec.rb +12 -10
- data/lib/capybara/spec/session/window/within_window_spec.rb +31 -86
- data/lib/capybara/spec/session/within_spec.rb +83 -44
- data/lib/capybara/spec/spec_helper.rb +54 -44
- data/lib/capybara/spec/test_app.rb +158 -43
- data/lib/capybara/spec/views/animated.erb +49 -0
- data/lib/capybara/spec/views/form.erb +163 -42
- data/lib/capybara/spec/views/frame_child.erb +4 -3
- data/lib/capybara/spec/views/frame_one.erb +2 -1
- data/lib/capybara/spec/views/frame_parent.erb +1 -1
- data/lib/capybara/spec/views/frame_two.erb +1 -1
- data/lib/capybara/spec/views/initial_alert.erb +2 -1
- data/lib/capybara/spec/views/layout.erb +10 -0
- data/lib/capybara/spec/views/obscured.erb +47 -0
- data/lib/capybara/spec/views/offset.erb +33 -0
- data/lib/capybara/spec/views/path.erb +2 -2
- data/lib/capybara/spec/views/popup_one.erb +1 -1
- data/lib/capybara/spec/views/popup_two.erb +1 -1
- data/lib/capybara/spec/views/react.erb +45 -0
- data/lib/capybara/spec/views/scroll.erb +21 -0
- data/lib/capybara/spec/views/spatial.erb +31 -0
- data/lib/capybara/spec/views/tables.erb +68 -1
- data/lib/capybara/spec/views/with_animation.erb +81 -0
- data/lib/capybara/spec/views/with_base_tag.erb +2 -2
- data/lib/capybara/spec/views/with_dragula.erb +24 -0
- data/lib/capybara/spec/views/with_fixed_header_footer.erb +2 -1
- data/lib/capybara/spec/views/with_hover.erb +3 -2
- data/lib/capybara/spec/views/with_hover1.erb +10 -0
- data/lib/capybara/spec/views/with_html.erb +69 -12
- data/lib/capybara/spec/views/with_html5_svg.erb +20 -0
- data/lib/capybara/spec/views/with_jquery_animation.erb +24 -0
- data/lib/capybara/spec/views/with_js.erb +30 -5
- data/lib/capybara/spec/views/with_jstree.erb +26 -0
- data/lib/capybara/spec/views/with_namespace.erb +21 -0
- data/lib/capybara/spec/views/with_scope.erb +2 -2
- data/lib/capybara/spec/views/with_scope_other.erb +6 -0
- data/lib/capybara/spec/views/with_shadow.erb +31 -0
- data/lib/capybara/spec/views/with_slow_unload.erb +2 -1
- data/lib/capybara/spec/views/with_sortable_js.erb +21 -0
- data/lib/capybara/spec/views/with_unload_alert.erb +1 -0
- data/lib/capybara/spec/views/with_windows.erb +1 -1
- data/lib/capybara/spec/views/within_frames.erb +5 -2
- data/lib/capybara/version.rb +2 -1
- data/lib/capybara/window.rb +36 -34
- data/lib/capybara.rb +134 -107
- data/spec/basic_node_spec.rb +60 -34
- data/spec/capybara_spec.rb +63 -88
- data/spec/counter_spec.rb +35 -0
- data/spec/css_builder_spec.rb +101 -0
- data/spec/css_splitter_spec.rb +38 -0
- data/spec/dsl_spec.rb +85 -64
- data/spec/filter_set_spec.rb +27 -9
- data/spec/fixtures/certificate.pem +25 -0
- data/spec/fixtures/key.pem +27 -0
- data/spec/fixtures/selenium_driver_rspec_failure.rb +6 -5
- data/spec/fixtures/selenium_driver_rspec_success.rb +6 -5
- data/spec/minitest_spec.rb +52 -7
- data/spec/minitest_spec_spec.rb +94 -63
- data/spec/per_session_config_spec.rb +14 -13
- data/spec/rack_test_spec.rb +194 -125
- data/spec/regexp_dissassembler_spec.rb +250 -0
- data/spec/result_spec.rb +111 -50
- data/spec/rspec/features_spec.rb +37 -31
- data/spec/rspec/scenarios_spec.rb +10 -8
- data/spec/rspec/shared_spec_matchers.rb +473 -422
- data/spec/rspec/views_spec.rb +5 -3
- data/spec/rspec_matchers_spec.rb +52 -11
- data/spec/rspec_spec.rb +109 -89
- data/spec/sauce_spec_chrome.rb +43 -0
- data/spec/selector_spec.rb +397 -68
- data/spec/selenium_spec_chrome.rb +187 -40
- data/spec/selenium_spec_chrome_remote.rb +96 -0
- data/spec/selenium_spec_edge.rb +60 -0
- data/spec/selenium_spec_firefox.rb +201 -41
- data/spec/selenium_spec_firefox_remote.rb +94 -0
- data/spec/selenium_spec_ie.rb +149 -0
- data/spec/selenium_spec_safari.rb +162 -0
- data/spec/server_spec.rb +213 -102
- data/spec/session_spec.rb +53 -16
- data/spec/shared_selenium_node.rb +79 -0
- data/spec/shared_selenium_session.rb +473 -122
- data/spec/spec_helper.rb +126 -7
- data/spec/whitespace_normalizer_spec.rb +54 -0
- data/spec/xpath_builder_spec.rb +93 -0
- metadata +355 -73
- data/.yard/templates_custom/default/class/html/selectors.erb +0 -38
- data/.yard/templates_custom/default/class/html/setup.rb +0 -17
- data/.yard/yard_extensions.rb +0 -78
- data/lib/capybara/query.rb +0 -7
- data/lib/capybara/rspec/compound.rb +0 -95
- data/lib/capybara/spec/session/assert_current_path.rb +0 -72
- data/lib/capybara/spec/session/assert_selector.rb +0 -148
- data/lib/capybara/spec/session/assert_text.rb +0 -234
- data/lib/capybara/spec/session/source_spec.rb +0 -0
- data/lib/capybara/spec/views/with_title.erb +0 -5
- data/spec/selenium_spec_marionette.rb +0 -127
|
@@ -1,38 +1,57 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'matrix'
|
|
4
|
+
|
|
2
5
|
module Capybara
|
|
3
6
|
module Queries
|
|
4
7
|
class SelectorQuery < Queries::BaseQuery
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
attr_reader :expression, :selector, :locator, :options
|
|
9
|
+
|
|
10
|
+
SPATIAL_KEYS = %i[above below left_of right_of near].freeze
|
|
11
|
+
VALID_KEYS = SPATIAL_KEYS + COUNT_KEYS +
|
|
12
|
+
%i[text id class style visible obscured exact exact_text normalize_ws match wait filter_set focused]
|
|
13
|
+
VALID_MATCH = %i[first smart prefer_exact one].freeze
|
|
14
|
+
|
|
15
|
+
def initialize(*args,
|
|
16
|
+
session_options:,
|
|
17
|
+
enable_aria_label: session_options.enable_aria_label,
|
|
18
|
+
enable_aria_role: session_options.enable_aria_role,
|
|
19
|
+
test_id: session_options.test_id,
|
|
20
|
+
selector_format: nil,
|
|
21
|
+
order: nil,
|
|
22
|
+
**options,
|
|
23
|
+
&filter_block)
|
|
11
24
|
@resolved_node = nil
|
|
12
|
-
@
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@
|
|
16
|
-
|
|
17
|
-
if
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@locator = args.shift
|
|
22
|
-
else
|
|
23
|
-
@selector = Selector.all.values.find { |s| s.match?(args[0]) }
|
|
24
|
-
@locator = args.shift
|
|
25
|
+
@resolved_count = 0
|
|
26
|
+
@options = options.dup
|
|
27
|
+
@order = order
|
|
28
|
+
@filter_cache = Hash.new { |hsh, key| hsh[key] = {} }
|
|
29
|
+
|
|
30
|
+
if @options[:text].is_a?(Regexp) && [true, false].include?(@options[:exact_text])
|
|
31
|
+
Capybara::Helpers.warn(
|
|
32
|
+
"Boolean 'exact_text' option is not supported when 'text' option is a Regexp - ignoring"
|
|
33
|
+
)
|
|
25
34
|
end
|
|
26
|
-
@selector ||= Selector.all[session_options.default_selector]
|
|
27
35
|
|
|
28
|
-
|
|
36
|
+
super(@options)
|
|
37
|
+
self.session_options = session_options
|
|
38
|
+
|
|
39
|
+
@selector = Selector.new(
|
|
40
|
+
find_selector(args[0].is_a?(Symbol) ? args.shift : args[0]),
|
|
41
|
+
config: {
|
|
42
|
+
enable_aria_label: enable_aria_label,
|
|
43
|
+
enable_aria_role: enable_aria_role,
|
|
44
|
+
test_id: test_id
|
|
45
|
+
},
|
|
46
|
+
format: selector_format
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
@locator = args.shift
|
|
50
|
+
@filter_block = filter_block
|
|
29
51
|
|
|
30
|
-
|
|
31
|
-
if session_options.exact_options and @selector == Selector.all[:option]
|
|
32
|
-
@options[:exact] = true
|
|
33
|
-
end
|
|
52
|
+
raise ArgumentError, "Unused parameters passed to #{self.class.name} : #{args}" unless args.empty?
|
|
34
53
|
|
|
35
|
-
@expression =
|
|
54
|
+
@expression = selector.call(@locator, **@options)
|
|
36
55
|
|
|
37
56
|
warn_exact_usage
|
|
38
57
|
|
|
@@ -40,202 +59,360 @@ module Capybara
|
|
|
40
59
|
end
|
|
41
60
|
|
|
42
61
|
def name; selector.name; end
|
|
43
|
-
def label; selector.label
|
|
44
|
-
|
|
45
|
-
def description
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
@description << " with id #{options[:id]}" if options[:id]
|
|
53
|
-
@description << " with classes [#{Array(options[:class]).join(',')}]" if options[:class]
|
|
54
|
-
@description << selector.description(options)
|
|
55
|
-
@description << " that also matches the custom filter block" if @filter_block
|
|
56
|
-
@description << " within #{@resolved_node.inspect}" if describe_within?
|
|
57
|
-
@description
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def matches_filters?(node)
|
|
61
|
-
if options[:text]
|
|
62
|
-
regexp = if options[:text].is_a?(Regexp)
|
|
63
|
-
options[:text]
|
|
64
|
-
else
|
|
65
|
-
if exact_text == true
|
|
66
|
-
/\A#{Regexp.escape(options[:text].to_s)}\z/
|
|
67
|
-
else
|
|
68
|
-
Regexp.escape(options[:text].to_s)
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
text_visible = visible
|
|
72
|
-
text_visible = :all if text_visible == :hidden
|
|
73
|
-
return false if not node.text(text_visible).match(regexp)
|
|
62
|
+
def label; selector.label || selector.name; end
|
|
63
|
+
|
|
64
|
+
def description(only_applied = false) # rubocop:disable Style/OptionalBooleanParameter
|
|
65
|
+
desc = +''
|
|
66
|
+
show_for = show_for_stage(only_applied)
|
|
67
|
+
|
|
68
|
+
if show_for[:any]
|
|
69
|
+
desc << 'visible ' if visible == :visible
|
|
70
|
+
desc << 'non-visible ' if visible == :hidden
|
|
74
71
|
end
|
|
75
72
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
73
|
+
desc << label.to_s
|
|
74
|
+
desc << " #{locator.inspect}" unless locator.nil?
|
|
75
|
+
|
|
76
|
+
if show_for[:any]
|
|
77
|
+
desc << " with#{' exact' if exact_text == true} text #{options[:text].inspect}" if options[:text]
|
|
78
|
+
desc << " with exact text #{exact_text}" if exact_text.is_a?(String)
|
|
81
79
|
end
|
|
82
80
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
desc << " with id #{options[:id]}" if options[:id]
|
|
82
|
+
desc << " with classes [#{Array(options[:class]).join(',')}]" if options[:class]
|
|
83
|
+
desc << ' that is focused' if options[:focused]
|
|
84
|
+
desc << ' that is not focused' if options[:focused] == false
|
|
85
|
+
|
|
86
|
+
desc << case options[:style]
|
|
87
|
+
when String
|
|
88
|
+
" with style attribute #{options[:style].inspect}"
|
|
89
|
+
when Regexp
|
|
90
|
+
" with style attribute matching #{options[:style].inspect}"
|
|
91
|
+
when Hash
|
|
92
|
+
" with styles #{options[:style].inspect}"
|
|
93
|
+
else ''
|
|
86
94
|
end
|
|
87
95
|
|
|
88
|
-
|
|
89
|
-
if options
|
|
90
|
-
|
|
91
|
-
elsif filter.default?
|
|
92
|
-
filter.matches?(node, filter.default)
|
|
93
|
-
else
|
|
94
|
-
true
|
|
96
|
+
%i[above below left_of right_of near].each do |spatial_filter|
|
|
97
|
+
if options[spatial_filter] && show_for[:spatial]
|
|
98
|
+
desc << " #{spatial_filter} #{options[spatial_filter] rescue '<ERROR>'}" # rubocop:disable Style/RescueModifier
|
|
95
99
|
end
|
|
96
100
|
end
|
|
97
101
|
|
|
98
|
-
|
|
99
|
-
node.session.using_wait_time(0){ @filter_block.call(node) }
|
|
100
|
-
else
|
|
101
|
-
@filter_block.call(node)
|
|
102
|
-
end unless @filter_block.nil?
|
|
102
|
+
desc << selector.description(node_filters: show_for[:node], **options)
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
desc << ' that also matches the custom filter block' if @filter_block && show_for[:node]
|
|
105
105
|
|
|
106
|
+
desc << " within #{@resolved_node.inspect}" if describe_within?
|
|
107
|
+
if locator.is_a?(String) && locator.start_with?('#', './/', '//') && !selector.raw_locator?
|
|
108
|
+
desc << "\nNote: It appears you may be passing a CSS selector or XPath expression rather than a locator. " \
|
|
109
|
+
"Please see the documentation for acceptable locator values.\n\n"
|
|
110
|
+
end
|
|
111
|
+
desc
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def applied_description
|
|
115
|
+
description(true)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def matches_filters?(node, node_filter_errors = [])
|
|
119
|
+
return true if (@resolved_node&.== node) && options[:allow_self]
|
|
120
|
+
|
|
121
|
+
matches_locator_filter?(node) &&
|
|
122
|
+
matches_system_filters?(node) &&
|
|
123
|
+
matches_spatial_filters?(node) &&
|
|
124
|
+
matches_node_filters?(node, node_filter_errors) &&
|
|
125
|
+
matches_filter_block?(node)
|
|
106
126
|
rescue *(node.respond_to?(:session) ? node.session.driver.invalid_element_errors : [])
|
|
107
|
-
|
|
127
|
+
false
|
|
108
128
|
end
|
|
109
129
|
|
|
110
130
|
def visible
|
|
111
|
-
case (vis = options.fetch(:visible){
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
131
|
+
case (vis = options.fetch(:visible) { default_visibility })
|
|
132
|
+
when true then :visible
|
|
133
|
+
when false then :all
|
|
134
|
+
else vis
|
|
115
135
|
end
|
|
116
136
|
end
|
|
117
137
|
|
|
118
138
|
def exact?
|
|
119
|
-
|
|
120
|
-
options.fetch(:exact, session_options.exact)
|
|
139
|
+
supports_exact? ? options.fetch(:exact, session_options.exact) : false
|
|
121
140
|
end
|
|
122
141
|
|
|
123
142
|
def match
|
|
124
143
|
options.fetch(:match, session_options.match)
|
|
125
144
|
end
|
|
126
145
|
|
|
127
|
-
def xpath(exact=nil)
|
|
128
|
-
exact =
|
|
146
|
+
def xpath(exact = nil)
|
|
147
|
+
exact = exact? if exact.nil?
|
|
129
148
|
expr = apply_expression_filters(@expression)
|
|
130
|
-
expr = if expr.respond_to?(:to_xpath)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
end
|
|
135
|
-
filtered_xpath(expr)
|
|
149
|
+
expr = exact ? expr.to_xpath(:exact) : expr.to_s if expr.respond_to?(:to_xpath)
|
|
150
|
+
expr = filtered_expression(expr)
|
|
151
|
+
expr = "(#{expr})[#{xpath_text_conditions}]" if try_text_match_in_expression?
|
|
152
|
+
expr
|
|
136
153
|
end
|
|
137
154
|
|
|
138
155
|
def css
|
|
139
|
-
|
|
156
|
+
filtered_expression(apply_expression_filters(@expression))
|
|
140
157
|
end
|
|
141
158
|
|
|
142
159
|
# @api private
|
|
143
160
|
def resolve_for(node, exact = nil)
|
|
161
|
+
applied_filters.clear
|
|
162
|
+
@filter_cache.clear
|
|
144
163
|
@resolved_node = node
|
|
164
|
+
@resolved_count += 1
|
|
165
|
+
|
|
145
166
|
node.synchronize do
|
|
146
|
-
children =
|
|
147
|
-
|
|
148
|
-
else
|
|
149
|
-
node.find_xpath(self.xpath(exact))
|
|
150
|
-
end.map do |child|
|
|
151
|
-
if node.is_a?(Capybara::Node::Base)
|
|
152
|
-
Capybara::Node::Element.new(node.session, child, node, self)
|
|
153
|
-
else
|
|
154
|
-
Capybara::Node::Simple.new(child)
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
Capybara::Result.new(children, self)
|
|
167
|
+
children = find_nodes_by_selector_format(node, exact).map(&method(:to_element))
|
|
168
|
+
Capybara::Result.new(ordered_results(children), self)
|
|
158
169
|
end
|
|
159
170
|
end
|
|
160
171
|
|
|
161
172
|
# @api private
|
|
162
173
|
def supports_exact?
|
|
163
|
-
@expression.respond_to? :to_xpath
|
|
174
|
+
return @expression.respond_to? :to_xpath if @selector.supports_exact?.nil?
|
|
175
|
+
|
|
176
|
+
@selector.supports_exact?
|
|
164
177
|
end
|
|
165
178
|
|
|
166
|
-
|
|
179
|
+
def failure_message
|
|
180
|
+
+"expected to find #{applied_description}" << count_message
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def negative_failure_message
|
|
184
|
+
+"expected not to find #{applied_description}" << count_message
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
private
|
|
188
|
+
|
|
189
|
+
def selector_format
|
|
190
|
+
@selector.format
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def matching_text
|
|
194
|
+
options[:text] || options[:exact_text]
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def text_fragments
|
|
198
|
+
(text = matching_text).is_a?(String) ? text.split : []
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def xpath_text_conditions
|
|
202
|
+
case (text = matching_text)
|
|
203
|
+
when String
|
|
204
|
+
text.split.map { |txt| XPath.contains(txt) }.reduce(&:&)
|
|
205
|
+
when Regexp
|
|
206
|
+
condition = XPath.current
|
|
207
|
+
condition = condition.uppercase if text.casefold?
|
|
208
|
+
Selector::RegexpDisassembler.new(text).alternated_substrings.map do |strs|
|
|
209
|
+
strs.flat_map(&:split).map { |str| condition.contains(str) }.reduce(:&)
|
|
210
|
+
end.reduce(:|)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def try_text_match_in_expression?
|
|
215
|
+
first_try? &&
|
|
216
|
+
matching_text &&
|
|
217
|
+
@resolved_node.is_a?(Capybara::Node::Base) &&
|
|
218
|
+
@resolved_node.session&.driver&.wait?
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def first_try?
|
|
222
|
+
@resolved_count == 1
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def show_for_stage(only_applied)
|
|
226
|
+
lambda do |stage = :any|
|
|
227
|
+
!only_applied || (stage == :any ? applied_filters.any? : applied_filters.include?(stage))
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def applied_filters
|
|
232
|
+
@applied_filters ||= []
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def find_selector(locator)
|
|
236
|
+
case locator
|
|
237
|
+
when Symbol then Selector[locator]
|
|
238
|
+
else Selector.for(locator)
|
|
239
|
+
end || Selector[session_options.default_selector]
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def find_nodes_by_selector_format(node, exact)
|
|
243
|
+
hints = {}
|
|
244
|
+
hints[:uses_visibility] = true unless visible == :all
|
|
245
|
+
hints[:texts] = text_fragments unless selector_format == :xpath
|
|
246
|
+
hints[:styles] = options[:style] if use_default_style_filter?
|
|
247
|
+
hints[:position] = true if use_spatial_filter?
|
|
248
|
+
|
|
249
|
+
case selector_format
|
|
250
|
+
when :css
|
|
251
|
+
if node.method(:find_css).arity == 1
|
|
252
|
+
node.find_css(css)
|
|
253
|
+
else
|
|
254
|
+
node.find_css(css, **hints)
|
|
255
|
+
end
|
|
256
|
+
when :xpath
|
|
257
|
+
if node.method(:find_xpath).arity == 1
|
|
258
|
+
node.find_xpath(xpath(exact))
|
|
259
|
+
else
|
|
260
|
+
node.find_xpath(xpath(exact), **hints)
|
|
261
|
+
end
|
|
262
|
+
else
|
|
263
|
+
raise ArgumentError, "Unknown format: #{selector_format}"
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def to_element(node)
|
|
268
|
+
if @resolved_node.is_a?(Capybara::Node::Base)
|
|
269
|
+
Capybara::Node::Element.new(@resolved_node.session, node, @resolved_node, self)
|
|
270
|
+
else
|
|
271
|
+
Capybara::Node::Simple.new(node)
|
|
272
|
+
end
|
|
273
|
+
end
|
|
167
274
|
|
|
168
275
|
def valid_keys
|
|
169
|
-
VALID_KEYS + custom_keys
|
|
276
|
+
(VALID_KEYS + custom_keys).uniq
|
|
170
277
|
end
|
|
171
278
|
|
|
172
|
-
def
|
|
173
|
-
|
|
174
|
-
|
|
279
|
+
def matches_node_filters?(node, errors)
|
|
280
|
+
applied_filters << :node
|
|
281
|
+
|
|
282
|
+
unapplied_options = options.keys - valid_keys
|
|
283
|
+
@selector.with_filter_errors(errors) do
|
|
284
|
+
node_filters.all? do |filter_name, filter|
|
|
285
|
+
next true unless apply_filter?(filter)
|
|
286
|
+
|
|
287
|
+
if filter.matcher?
|
|
288
|
+
unapplied_options.select { |option_name| filter.handles_option?(option_name) }.all? do |option_name|
|
|
289
|
+
unapplied_options.delete(option_name)
|
|
290
|
+
filter.matches?(node, option_name, options[option_name], @selector)
|
|
291
|
+
end
|
|
292
|
+
elsif options.key?(filter_name)
|
|
293
|
+
unapplied_options.delete(filter_name)
|
|
294
|
+
filter.matches?(node, filter_name, options[filter_name], @selector)
|
|
295
|
+
elsif filter.default?
|
|
296
|
+
filter.matches?(node, filter_name, filter.default, @selector)
|
|
297
|
+
else
|
|
298
|
+
true
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def matches_filter_block?(node)
|
|
305
|
+
return true unless @filter_block
|
|
306
|
+
|
|
307
|
+
if node.respond_to?(:session)
|
|
308
|
+
node.session.using_wait_time(0) { @filter_block.call(node) }
|
|
175
309
|
else
|
|
176
|
-
@
|
|
310
|
+
@filter_block.call(node)
|
|
177
311
|
end
|
|
178
312
|
end
|
|
179
313
|
|
|
314
|
+
def filter_set(name)
|
|
315
|
+
::Capybara::Selector::FilterSet[name]
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def node_filters
|
|
319
|
+
if options.key?(:filter_set)
|
|
320
|
+
filter_set(options[:filter_set])
|
|
321
|
+
else
|
|
322
|
+
@selector
|
|
323
|
+
end.node_filters
|
|
324
|
+
end
|
|
325
|
+
|
|
180
326
|
def expression_filters
|
|
181
327
|
filters = @selector.expression_filters
|
|
182
|
-
filters.merge
|
|
328
|
+
filters.merge filter_set(options[:filter_set]).expression_filters if options.key?(:filter_set)
|
|
183
329
|
filters
|
|
184
330
|
end
|
|
185
331
|
|
|
332
|
+
def ordered_results(results)
|
|
333
|
+
case @order
|
|
334
|
+
when :reverse
|
|
335
|
+
results.reverse
|
|
336
|
+
else
|
|
337
|
+
results
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
186
341
|
def custom_keys
|
|
187
342
|
@custom_keys ||= node_filters.keys + expression_filters.keys
|
|
188
343
|
end
|
|
189
344
|
|
|
190
345
|
def assert_valid_keys
|
|
191
|
-
super
|
|
192
346
|
unless VALID_MATCH.include?(match)
|
|
193
|
-
raise ArgumentError, "
|
|
347
|
+
raise ArgumentError, "Invalid option #{match.inspect} for :match, should be one of #{VALID_MATCH.map(&:inspect).join(', ')}"
|
|
194
348
|
end
|
|
195
|
-
end
|
|
196
349
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if options.has_key?(:class) && !custom_keys.include?(:class)
|
|
202
|
-
class_xpath = Array(options[:class]).map do |klass|
|
|
203
|
-
"contains(concat(' ',normalize-space(@class),' '),' #{klass} ')"
|
|
204
|
-
end.join(" and ")
|
|
205
|
-
expr = "#{expr}[#{class_xpath}]"
|
|
206
|
-
end
|
|
350
|
+
unhandled_options = @options.keys.reject do |option_name|
|
|
351
|
+
valid_keys.include?(option_name) ||
|
|
352
|
+
expression_filters.any? { |_name, ef| ef.handles_option? option_name } ||
|
|
353
|
+
node_filters.any? { |_name, nf| nf.handles_option? option_name }
|
|
207
354
|
end
|
|
208
|
-
|
|
355
|
+
|
|
356
|
+
return if unhandled_options.empty?
|
|
357
|
+
|
|
358
|
+
invalid_names = unhandled_options.map(&:inspect).join(', ')
|
|
359
|
+
valid_names = (valid_keys - [:allow_self]).map(&:inspect).join(', ')
|
|
360
|
+
raise ArgumentError, "Invalid option(s) #{invalid_names}, should be one of #{valid_names}"
|
|
209
361
|
end
|
|
210
362
|
|
|
211
|
-
def
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
363
|
+
def filtered_expression(expr)
|
|
364
|
+
conditions = {}
|
|
365
|
+
conditions[:id] = options[:id] if use_default_id_filter?
|
|
366
|
+
conditions[:class] = options[:class] if use_default_class_filter?
|
|
367
|
+
conditions[:style] = options[:style] if use_default_style_filter? && !options[:style].is_a?(Hash)
|
|
368
|
+
builder(expr).add_attribute_conditions(**conditions)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def use_default_id_filter?
|
|
372
|
+
options.key?(:id) && !custom_keys.include?(:id)
|
|
221
373
|
end
|
|
222
374
|
|
|
223
|
-
def
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
375
|
+
def use_default_class_filter?
|
|
376
|
+
options.key?(:class) && !custom_keys.include?(:class)
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def use_default_style_filter?
|
|
380
|
+
options.key?(:style) && !custom_keys.include?(:style)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def use_default_focused_filter?
|
|
384
|
+
options.key?(:focused) && !custom_keys.include?(:focused)
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def use_spatial_filter?
|
|
388
|
+
options.values_at(*SPATIAL_KEYS).compact.any?
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def apply_expression_filters(expression)
|
|
392
|
+
unapplied_options = options.keys - valid_keys
|
|
393
|
+
expression_filters.inject(expression) do |expr, (name, ef)|
|
|
394
|
+
next expr unless apply_filter?(ef)
|
|
395
|
+
|
|
396
|
+
if ef.matcher?
|
|
397
|
+
unapplied_options.select(&ef.method(:handles_option?)).inject(expr) do |memo, option_name|
|
|
398
|
+
unapplied_options.delete(option_name)
|
|
399
|
+
ef.apply_filter(memo, option_name, options[option_name], @selector)
|
|
400
|
+
end
|
|
401
|
+
elsif options.key?(name)
|
|
402
|
+
unapplied_options.delete(name)
|
|
403
|
+
ef.apply_filter(expr, name, options[name], @selector)
|
|
227
404
|
elsif ef.default?
|
|
228
|
-
ef.apply_filter(
|
|
405
|
+
ef.apply_filter(expr, name, ef.default, @selector)
|
|
229
406
|
else
|
|
230
|
-
|
|
407
|
+
expr
|
|
231
408
|
end
|
|
232
409
|
end
|
|
233
410
|
end
|
|
234
411
|
|
|
235
412
|
def warn_exact_usage
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
413
|
+
return unless options.key?(:exact) && !supports_exact?
|
|
414
|
+
|
|
415
|
+
warn "The :exact option only has an effect on queries using the XPath#is method. Using it with the query \"#{expression}\" has no effect."
|
|
239
416
|
end
|
|
240
417
|
|
|
241
418
|
def exact_text
|
|
@@ -243,9 +420,342 @@ module Capybara
|
|
|
243
420
|
end
|
|
244
421
|
|
|
245
422
|
def describe_within?
|
|
246
|
-
@resolved_node && !(@resolved_node
|
|
247
|
-
|
|
423
|
+
@resolved_node && !document?(@resolved_node) && !simple_root?(@resolved_node)
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def document?(node)
|
|
427
|
+
node.is_a?(::Capybara::Node::Document)
|
|
248
428
|
end
|
|
429
|
+
|
|
430
|
+
def simple_root?(node)
|
|
431
|
+
node.is_a?(::Capybara::Node::Simple) && node.path == '/'
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def apply_filter?(filter)
|
|
435
|
+
filter.format.nil? || (filter.format == selector_format)
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def matches_locator_filter?(node)
|
|
439
|
+
return true unless @selector.locator_filter && apply_filter?(@selector.locator_filter)
|
|
440
|
+
|
|
441
|
+
@selector.locator_filter.matches?(node, @locator, @selector, exact: exact?)
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def matches_system_filters?(node)
|
|
445
|
+
applied_filters << :system
|
|
446
|
+
|
|
447
|
+
matches_visibility_filters?(node) &&
|
|
448
|
+
matches_id_filter?(node) &&
|
|
449
|
+
matches_class_filter?(node) &&
|
|
450
|
+
matches_style_filter?(node) &&
|
|
451
|
+
matches_focused_filter?(node) &&
|
|
452
|
+
matches_text_filter?(node) &&
|
|
453
|
+
matches_exact_text_filter?(node)
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def matches_spatial_filters?(node)
|
|
457
|
+
applied_filters << :spatial
|
|
458
|
+
return true unless use_spatial_filter?
|
|
459
|
+
|
|
460
|
+
node_rect = Rectangle.new(node.initial_cache[:position] || node.rect)
|
|
461
|
+
|
|
462
|
+
if options[:above]
|
|
463
|
+
el_rect = rect_cache(options[:above])
|
|
464
|
+
return false unless node_rect.above? el_rect
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
if options[:below]
|
|
468
|
+
el_rect = rect_cache(options[:below])
|
|
469
|
+
return false unless node_rect.below? el_rect
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
if options[:left_of]
|
|
473
|
+
el_rect = rect_cache(options[:left_of])
|
|
474
|
+
return false unless node_rect.left_of? el_rect
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
if options[:right_of]
|
|
478
|
+
el_rect = rect_cache(options[:right_of])
|
|
479
|
+
return false unless node_rect.right_of? el_rect
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
if options[:near]
|
|
483
|
+
return false if node == options[:near]
|
|
484
|
+
|
|
485
|
+
el_rect = rect_cache(options[:near])
|
|
486
|
+
return false unless node_rect.near? el_rect
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
true
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
def matches_id_filter?(node)
|
|
493
|
+
return true unless use_default_id_filter? && options[:id].is_a?(Regexp)
|
|
494
|
+
|
|
495
|
+
options[:id].match? node[:id]
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def matches_class_filter?(node)
|
|
499
|
+
return true unless use_default_class_filter? && need_to_process_classes?
|
|
500
|
+
|
|
501
|
+
if options[:class].is_a? Regexp
|
|
502
|
+
options[:class].match? node[:class]
|
|
503
|
+
else
|
|
504
|
+
classes = (node[:class] || '').split
|
|
505
|
+
options[:class].select { |c| c.is_a? Regexp }.all? do |r|
|
|
506
|
+
classes.any? { |cls| r.match? cls }
|
|
507
|
+
end
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
def matches_focused_filter?(node)
|
|
512
|
+
return true unless use_default_focused_filter?
|
|
513
|
+
|
|
514
|
+
(node == node.session.active_element) == options[:focused]
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
def need_to_process_classes?
|
|
518
|
+
case options[:class]
|
|
519
|
+
when Regexp then true
|
|
520
|
+
when Array then options[:class].any?(Regexp)
|
|
521
|
+
else
|
|
522
|
+
false
|
|
523
|
+
end
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
def matches_style_filter?(node)
|
|
527
|
+
case options[:style]
|
|
528
|
+
when String, nil
|
|
529
|
+
true
|
|
530
|
+
when Regexp
|
|
531
|
+
options[:style].match? node[:style]
|
|
532
|
+
when Hash
|
|
533
|
+
matches_style?(node, options[:style])
|
|
534
|
+
end
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
def matches_style?(node, styles)
|
|
538
|
+
@actual_styles = node.initial_cache[:style] || node.style(*styles.keys)
|
|
539
|
+
styles.all? do |style, value|
|
|
540
|
+
if value.is_a? Regexp
|
|
541
|
+
value.match? @actual_styles[style.to_s]
|
|
542
|
+
else
|
|
543
|
+
@actual_styles[style.to_s] == value
|
|
544
|
+
end
|
|
545
|
+
end
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
def matches_text_filter?(node)
|
|
549
|
+
value = options[:text]
|
|
550
|
+
return true unless value
|
|
551
|
+
return matches_text_exactly?(node, value) if exact_text == true && !value.is_a?(Regexp)
|
|
552
|
+
|
|
553
|
+
regexp = value.is_a?(Regexp) ? value : Regexp.escape(value.to_s)
|
|
554
|
+
matches_text_regexp?(node, regexp)
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def matches_exact_text_filter?(node)
|
|
558
|
+
case exact_text
|
|
559
|
+
when String, Regexp
|
|
560
|
+
matches_text_exactly?(node, exact_text)
|
|
561
|
+
else
|
|
562
|
+
true
|
|
563
|
+
end
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def matches_visibility_filters?(node)
|
|
567
|
+
obscured = options[:obscured]
|
|
568
|
+
return (visible != :hidden) && (node.initial_cache[:visible] != false) && !node.obscured? if obscured == false
|
|
569
|
+
|
|
570
|
+
vis = case visible
|
|
571
|
+
when :visible
|
|
572
|
+
node.initial_cache[:visible] || (node.initial_cache[:visible].nil? && node.visible?)
|
|
573
|
+
when :hidden
|
|
574
|
+
# TODO: check why the 'visbile' cache spelling mistake wasn't caught in a test
|
|
575
|
+
# (node.initial_cache[:visible] == false) || (node.initial_cache[:visbile].nil? && !node.visible?)
|
|
576
|
+
(node.initial_cache[:visible] == false) || (node.initial_cache[:visible].nil? && !node.visible?)
|
|
577
|
+
else
|
|
578
|
+
true
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
vis && case obscured
|
|
582
|
+
when true
|
|
583
|
+
node.obscured?
|
|
584
|
+
when false
|
|
585
|
+
!node.obscured?
|
|
586
|
+
else
|
|
587
|
+
true
|
|
588
|
+
end
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
def matches_text_exactly?(node, value)
|
|
592
|
+
regexp = value.is_a?(Regexp) ? value : /\A#{Regexp.escape(value.to_s)}\z/
|
|
593
|
+
matches_text_regexp(node, regexp).then { |m| m&.pre_match == '' && m&.post_match == '' }
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
def normalize_ws
|
|
597
|
+
options.fetch(:normalize_ws, session_options.default_normalize_ws)
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
def matches_text_regexp(node, regexp)
|
|
601
|
+
text_visible = visible
|
|
602
|
+
text_visible = :all if text_visible == :hidden
|
|
603
|
+
node.text(text_visible, normalize_ws: normalize_ws).match(regexp)
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
def matches_text_regexp?(node, regexp)
|
|
607
|
+
!matches_text_regexp(node, regexp).nil?
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
def default_visibility
|
|
611
|
+
@selector.default_visibility(session_options.ignore_hidden_elements, options)
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
def builder(expr)
|
|
615
|
+
selector.builder(expr)
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def position_cache(key)
|
|
619
|
+
@filter_cache[key][:position] ||= key.rect
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
def rect_cache(key)
|
|
623
|
+
@filter_cache[key][:rect] ||= Rectangle.new(position_cache(key))
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
class Rectangle
|
|
627
|
+
attr_reader :top, :bottom, :left, :right
|
|
628
|
+
|
|
629
|
+
def initialize(position)
|
|
630
|
+
# rubocop:disable Style/RescueModifier
|
|
631
|
+
@top = position['top'] rescue position['y']
|
|
632
|
+
@bottom = position['bottom'] rescue (@top + position['height'])
|
|
633
|
+
@left = position['left'] rescue position['x']
|
|
634
|
+
@right = position['right'] rescue (@left + position['width'])
|
|
635
|
+
# rubocop:enable Style/RescueModifier
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
def distance(other)
|
|
639
|
+
distance = Float::INFINITY
|
|
640
|
+
|
|
641
|
+
line_segments.each do |ls1|
|
|
642
|
+
other.line_segments.each do |ls2|
|
|
643
|
+
distance = [
|
|
644
|
+
distance,
|
|
645
|
+
distance_segment_segment(*ls1, *ls2)
|
|
646
|
+
].min
|
|
647
|
+
end
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
distance
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
def above?(other)
|
|
654
|
+
bottom <= other.top
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
def below?(other)
|
|
658
|
+
top >= other.bottom
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
def left_of?(other)
|
|
662
|
+
right <= other.left
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
def right_of?(other)
|
|
666
|
+
left >= other.right
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
def near?(other)
|
|
670
|
+
distance(other) <= 50
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
protected
|
|
674
|
+
|
|
675
|
+
def line_segments
|
|
676
|
+
[
|
|
677
|
+
[Vector[top, left], Vector[top, right]],
|
|
678
|
+
[Vector[top, right], Vector[bottom, left]],
|
|
679
|
+
[Vector[bottom, left], Vector[bottom, right]],
|
|
680
|
+
[Vector[bottom, right], Vector[top, left]]
|
|
681
|
+
]
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
private
|
|
685
|
+
|
|
686
|
+
def distance_segment_segment(l1p1, l1p2, l2p1, l2p2)
|
|
687
|
+
# See http://geomalgorithms.com/a07-_distance.html
|
|
688
|
+
# rubocop:disable Naming/VariableName
|
|
689
|
+
u = l1p2 - l1p1
|
|
690
|
+
v = l2p2 - l2p1
|
|
691
|
+
w = l1p1 - l2p1
|
|
692
|
+
|
|
693
|
+
a = u.dot u
|
|
694
|
+
b = u.dot v
|
|
695
|
+
c = v.dot v
|
|
696
|
+
|
|
697
|
+
d = u.dot w
|
|
698
|
+
e = v.dot w
|
|
699
|
+
cap_d = (a * c) - (b**2)
|
|
700
|
+
sD = tD = cap_d
|
|
701
|
+
|
|
702
|
+
# compute the line parameters of the two closest points
|
|
703
|
+
if cap_d < Float::EPSILON # the lines are almost parallel
|
|
704
|
+
sN = 0.0 # force using point P0 on segment S1
|
|
705
|
+
sD = 1.0 # to prevent possible division by 0.0 later
|
|
706
|
+
tN = e
|
|
707
|
+
tD = c
|
|
708
|
+
else # get the closest points on the infinite lines
|
|
709
|
+
sN = (b * e) - (c * d)
|
|
710
|
+
tN = (a * e) - (b * d)
|
|
711
|
+
if sN.negative? # sc < 0 => the s=0 edge is visible
|
|
712
|
+
sN = 0
|
|
713
|
+
tN = e
|
|
714
|
+
tD = c
|
|
715
|
+
elsif sN > sD # sc > 1 => the s=1 edge is visible
|
|
716
|
+
sN = sD
|
|
717
|
+
tN = e + b
|
|
718
|
+
tD = c
|
|
719
|
+
end
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
if tN.negative? # tc < 0 => the t=0 edge is visible
|
|
723
|
+
tN = 0
|
|
724
|
+
# recompute sc for this edge
|
|
725
|
+
if (-d).negative?
|
|
726
|
+
sN = 0.0
|
|
727
|
+
elsif -d > a
|
|
728
|
+
sN = sD
|
|
729
|
+
else
|
|
730
|
+
sN = -d
|
|
731
|
+
sD = a
|
|
732
|
+
end
|
|
733
|
+
elsif tN > tD # tc > 1 => the t=1 edge is visible
|
|
734
|
+
tN = tD
|
|
735
|
+
# recompute sc for this edge
|
|
736
|
+
if (-d + b).negative?
|
|
737
|
+
sN = 0.0
|
|
738
|
+
elsif (-d + b) > a
|
|
739
|
+
sN = sD
|
|
740
|
+
else
|
|
741
|
+
sN = (-d + b)
|
|
742
|
+
sD = a
|
|
743
|
+
end
|
|
744
|
+
end
|
|
745
|
+
|
|
746
|
+
# finally do the division to get sc and tc
|
|
747
|
+
sc = sN.abs < Float::EPSILON ? 0.0 : sN / sD
|
|
748
|
+
tc = tN.abs < Float::EPSILON ? 0.0 : tN / tD
|
|
749
|
+
|
|
750
|
+
# difference of the two closest points
|
|
751
|
+
dP = w + (u * sc) - (v * tc)
|
|
752
|
+
|
|
753
|
+
Math.sqrt(dP.dot(dP))
|
|
754
|
+
# rubocop:enable Naming/VariableName
|
|
755
|
+
end
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
private_constant :Rectangle
|
|
249
759
|
end
|
|
250
760
|
end
|
|
251
761
|
end
|