diff --git a/lib/watir/elements/element.rb b/lib/watir/elements/element.rb index 7dc07dd9e..a6f6950fd 100644 --- a/lib/watir/elements/element.rb +++ b/lib/watir/elements/element.rb @@ -18,6 +18,17 @@ class Element attr_accessor :keyword attr_reader :selector + # https://www.w3.org/TR/html52/single-page.html#casesensitivity + CASE_INSENSITIVE_ATTRIBUTES = %i[accept accept_charset align alink axis + bgcolor charset checked clear codetype + color compact declare defer dir direction + disabled enctype face frame hreflang + http_equiv lang language link media + method multiple nohref noresize noshade + nowrap readonly rel rev rules scope + scrolling selected shape target text + type valign valuetype vlink].freeze + # # temporarily add :id and :class_name manually since they're no longer specified in the HTML spec. # diff --git a/lib/watir/locators/element/selector_builder/xpath.rb b/lib/watir/locators/element/selector_builder/xpath.rb index 005a5d4d4..596b0a300 100644 --- a/lib/watir/locators/element/selector_builder/xpath.rb +++ b/lib/watir/locators/element/selector_builder/xpath.rb @@ -12,6 +12,7 @@ class XPath def build(selector) @selector = selector + @valid_attributes = build_valid_attributes @built = (@selector.keys & CAN_NOT_BUILD).each_with_object({}) do |key, hash| hash[key] = @selector.delete(key) @@ -55,10 +56,7 @@ def predicate_expression(key, val) end def predicate_conversion(key, regexp) - # type attributes can be upper case - downcase them - # https://github.com/watir/watir/issues/72 - downcase = key == :type || regexp.casefold? - + downcase = case_insensitive_attribute?(key) || regexp.casefold? lhs = lhs_for(key, downcase) results = RegexpDisassembler.new(regexp).substrings @@ -72,8 +70,10 @@ def predicate_conversion(key, regexp) add_to_matching(key, regexp, results) - results.map { |substring| - "contains(#{lhs}, '#{substring}')" + results.map { |rhs| + rhs = "'#{rhs}'" + rhs = XpathSupport.downcase(rhs) if downcase + "contains(#{lhs}, #{rhs})" }.flatten.compact.join(' and ') end @@ -236,9 +236,34 @@ def equal_pair(key, value) negate_xpath ? "not(#{expression})" : expression else - "#{lhs_for(key, key == :type)}=#{XpathSupport.escape value}" + downcase = case_insensitive_attribute?(key) + + lhs = lhs_for(key, downcase) + rhs = XpathSupport.escape(value) + rhs = XpathSupport.downcase(rhs) if downcase + + "#{lhs}=#{rhs}" + end + end + + def build_valid_attributes + tag_name = @selector[:tag_name] + if tag_name.is_a?(String) && Watir.tag_to_class[tag_name.to_sym] + Watir.tag_to_class[tag_name.to_sym].attribute_list + else + Watir::HTMLElement.attribute_list end end + + def case_insensitive_attribute?(attribute) + # type attributes can be upper case - downcase them + # https://github.com/watir/watir/issues/72 + return true if attribute == :type + return true if Watir::Element::CASE_INSENSITIVE_ATTRIBUTES.include?(attribute) && + @valid_attributes.include?(attribute) + + false + end end end end diff --git a/lib/watir/locators/text_field/selector_builder/xpath.rb b/lib/watir/locators/text_field/selector_builder/xpath.rb index 57227db7b..600c93ecf 100644 --- a/lib/watir/locators/text_field/selector_builder/xpath.rb +++ b/lib/watir/locators/text_field/selector_builder/xpath.rb @@ -36,7 +36,9 @@ def type_string(type) def negative_type_text Watir::TextField::NON_TEXT_TYPES.map { |type| - "#{lhs_for(:type, true)}!=#{SelectorBuilder::XpathSupport.escape type}" + lhs = lhs_for(:type, true) + rhs = SelectorBuilder::XpathSupport.downcase(SelectorBuilder::XpathSupport.escape(type)) + "#{lhs}!=#{rhs}" }.join(' and ') end end diff --git a/spec/unit/selector_builder/button_spec.rb b/spec/unit/selector_builder/button_spec.rb index f25b70ef6..b41457d7f 100644 --- a/spec/unit/selector_builder/button_spec.rb +++ b/spec/unit/selector_builder/button_spec.rb @@ -7,10 +7,10 @@ let(:uppercase) { 'ABCDEFGHIJKLMNOPQRSTUVWXYZÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞŸŽŠŒ' } let(:lowercase) { 'abcdefghijklmnopqrstuvwxyzàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿžšœ' } let(:default_types) do - "translate(@type,'#{uppercase}','#{lowercase}')='button' or" \ -" translate(@type,'#{uppercase}','#{lowercase}')='reset' or"\ -" translate(@type,'#{uppercase}','#{lowercase}')='submit' or"\ -" translate(@type,'#{uppercase}','#{lowercase}')='image'" + "translate(@type,'#{uppercase}','#{lowercase}')=translate('button','#{uppercase}','#{lowercase}') or " \ + "translate(@type,'#{uppercase}','#{lowercase}')=translate('reset','#{uppercase}','#{lowercase}') or "\ + "translate(@type,'#{uppercase}','#{lowercase}')=translate('submit','#{uppercase}','#{lowercase}') or "\ + "translate(@type,'#{uppercase}','#{lowercase}')=translate('image','#{uppercase}','#{lowercase}')" end describe '#build' do @@ -36,9 +36,10 @@ it 'locates input or button element with specified type' do selector = {type: 'reset'} + type = "translate('reset','#{uppercase}','#{lowercase}')" built = {xpath: ".//*[(local-name()='button' and " \ -"translate(@type,'#{uppercase}','#{lowercase}')='reset') or " \ -"(local-name()='input' and (translate(@type,'#{uppercase}','#{lowercase}')='reset'))]"} +"translate(@type,'#{uppercase}','#{lowercase}')=#{type}) or " \ +"(local-name()='input' and (translate(@type,'#{uppercase}','#{lowercase}')=#{type}))]"} expect(selector_builder.build(selector)).to eq built end diff --git a/spec/unit/selector_builder/element_spec.rb b/spec/unit/selector_builder/element_spec.rb index acd9de8f9..3e60675dc 100644 --- a/spec/unit/selector_builder/element_spec.rb +++ b/spec/unit/selector_builder/element_spec.rb @@ -574,7 +574,10 @@ selector = {action: /ME/i} built = {xpath: './/*[contains(translate(@action,' \ "'ABCDEFGHIJKLMNOPQRSTUVWXYZÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞŸŽŠŒ'," \ -"'abcdefghijklmnopqrstuvwxyzàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿžšœ'), 'me')]"} +"'abcdefghijklmnopqrstuvwxyzàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿžšœ'), " \ +"translate('me'," \ +"'ABCDEFGHIJKLMNOPQRSTUVWXYZÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞŸŽŠŒ'," \ +"'abcdefghijklmnopqrstuvwxyzàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿžšœ'))]"} expect(selector_builder.build(selector)).to eq built end @@ -762,5 +765,51 @@ expect(build_selector).to eq built end end + + context 'with case-insensitive attributes' do + it 'respects case when locating uknown element with uknown attribute' do + expect(selector_builder.build(hreflang: 'en')).to eq(xpath: ".//*[@hreflang='en']") + expect(selector_builder.build(hreflang: /en/)).to eq(xpath: ".//*[contains(@hreflang, 'en')]") + end + + it 'ignores case when locating uknown element with defined attribute' do + lhs = 'translate(@lang,' \ + "'ABCDEFGHIJKLMNOPQRSTUVWXYZÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞŸŽŠŒ'," \ + "'abcdefghijklmnopqrstuvwxyzàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿžšœ')" + rhs = "translate('en'," \ + "'ABCDEFGHIJKLMNOPQRSTUVWXYZÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞŸŽŠŒ'," \ + "'abcdefghijklmnopqrstuvwxyzàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿžšœ')" + expect(selector_builder.build(lang: 'en')).to eq(xpath: ".//*[#{lhs}=#{rhs}]") + expect(selector_builder.build(lang: /en/)).to eq(xpath: ".//*[contains(#{lhs}, #{rhs})]") + expect(selector_builder.build(tag_name: /a/, lang: 'en')) + .to eq(xpath: ".//*[contains(local-name(), 'a')][#{lhs}=#{rhs}]") + expect(selector_builder.build(tag_name: /a/, lang: /en/)) + .to eq(xpath: ".//*[contains(local-name(), 'a')][contains(#{lhs}, #{rhs})]") + end + + it 'ignores case when attribute is defined for element' do + lhs = 'translate(@hreflang,' \ + "'ABCDEFGHIJKLMNOPQRSTUVWXYZÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞŸŽŠŒ'," \ + "'abcdefghijklmnopqrstuvwxyzàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿžšœ')" + rhs = "translate('en'," \ + "'ABCDEFGHIJKLMNOPQRSTUVWXYZÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞŸŽŠŒ'," \ + "'abcdefghijklmnopqrstuvwxyzàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿžšœ')" + expect(selector_builder.build(tag_name: 'a', hreflang: 'en')) + .to eq(xpath: ".//*[local-name()='a'][#{lhs}=#{rhs}]") + expect(selector_builder.build(tag_name: 'a', hreflang: /en/)) + .to eq(xpath: ".//*[local-name()='a'][contains(#{lhs}, #{rhs})]") + end + + it 'respects case when attribute is not defined for element' do + expect(selector_builder.build(tag_name: 'table', hreflang: 'en')) + .to eq(xpath: ".//*[local-name()='table'][@hreflang='en']") + expect(selector_builder.build(tag_name: 'table', hreflang: /en/)) + .to eq(xpath: ".//*[local-name()='table'][contains(@hreflang, 'en')]") + expect(selector_builder.build(tag_name: /a/, hreflang: 'en')) + .to eq(xpath: ".//*[contains(local-name(), 'a')][@hreflang='en']") + expect(selector_builder.build(tag_name: /a/, hreflang: /en/)) + .to eq(xpath: ".//*[contains(local-name(), 'a')][contains(@hreflang, 'en')]") + end + end end end diff --git a/spec/unit/selector_builder/text_field_spec.rb b/spec/unit/selector_builder/text_field_spec.rb index b185d4112..6ae7a3b23 100644 --- a/spec/unit/selector_builder/text_field_spec.rb +++ b/spec/unit/selector_builder/text_field_spec.rb @@ -7,18 +7,18 @@ let(:uppercase) { 'ABCDEFGHIJKLMNOPQRSTUVWXYZÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞŸŽŠŒ' } let(:lowercase) { 'abcdefghijklmnopqrstuvwxyzàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿžšœ' } let(:negative_types) do - "translate(@type,'#{uppercase}','#{lowercase}')!='file' and "\ -"translate(@type,'#{uppercase}','#{lowercase}')!='radio' and " \ -"translate(@type,'#{uppercase}','#{lowercase}')!='checkbox' and " \ -"translate(@type,'#{uppercase}','#{lowercase}')!='submit' and " \ -"translate(@type,'#{uppercase}','#{lowercase}')!='reset' and " \ -"translate(@type,'#{uppercase}','#{lowercase}')!='image' and " \ -"translate(@type,'#{uppercase}','#{lowercase}')!='button' and " \ -"translate(@type,'#{uppercase}','#{lowercase}')!='hidden' and " \ -"translate(@type,'#{uppercase}','#{lowercase}')!='range' and " \ -"translate(@type,'#{uppercase}','#{lowercase}')!='color' and " \ -"translate(@type,'#{uppercase}','#{lowercase}')!='date' and " \ -"translate(@type,'#{uppercase}','#{lowercase}')!='datetime-local'" + "translate(@type,'#{uppercase}','#{lowercase}')!=translate('file','#{uppercase}','#{lowercase}') and "\ + "translate(@type,'#{uppercase}','#{lowercase}')!=translate('radio','#{uppercase}','#{lowercase}') and " \ + "translate(@type,'#{uppercase}','#{lowercase}')!=translate('checkbox','#{uppercase}','#{lowercase}') and " \ + "translate(@type,'#{uppercase}','#{lowercase}')!=translate('submit','#{uppercase}','#{lowercase}') and " \ + "translate(@type,'#{uppercase}','#{lowercase}')!=translate('reset','#{uppercase}','#{lowercase}') and " \ + "translate(@type,'#{uppercase}','#{lowercase}')!=translate('image','#{uppercase}','#{lowercase}') and " \ + "translate(@type,'#{uppercase}','#{lowercase}')!=translate('button','#{uppercase}','#{lowercase}') and " \ + "translate(@type,'#{uppercase}','#{lowercase}')!=translate('hidden','#{uppercase}','#{lowercase}') and " \ + "translate(@type,'#{uppercase}','#{lowercase}')!=translate('range','#{uppercase}','#{lowercase}') and " \ + "translate(@type,'#{uppercase}','#{lowercase}')!=translate('color','#{uppercase}','#{lowercase}') and " \ + "translate(@type,'#{uppercase}','#{lowercase}')!=translate('date','#{uppercase}','#{lowercase}') and " \ + "translate(@type,'#{uppercase}','#{lowercase}')!=translate('datetime-local','#{uppercase}','#{lowercase}')" end describe '#build' do @@ -33,7 +33,7 @@ it 'specified text field type that is text' do selector = {type: 'text'} built = {xpath: ".//*[local-name()='input']" \ -"[translate(@type,'#{uppercase}','#{lowercase}')='text']"} +"[translate(@type,'#{uppercase}','#{lowercase}')=translate('text','#{uppercase}','#{lowercase}')]"} expect(selector_builder.build(selector)).to eq built end @@ -41,7 +41,7 @@ it 'specified text field type that is not text' do selector = {type: 'number'} built = {xpath: ".//*[local-name()='input']" \ -"[translate(@type,'#{uppercase}','#{lowercase}')='number']"} +"[translate(@type,'#{uppercase}','#{lowercase}')=translate('number','#{uppercase}','#{lowercase}')]"} expect(selector_builder.build(selector)).to eq built end