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