Skip to content

Commit

Permalink
Respect/ignore case on certain attributes defined by HTML spec
Browse files Browse the repository at this point in the history
Per https://www.w3.org/TR/html52/single-page.html#casesensitivity

This commit additionally ensures that Watir ignores case of passed
selector string for attribute which case is expected to be ignored.

Closes #507
  • Loading branch information
p0deje committed Feb 14, 2019
1 parent 2d8db09 commit 0b9d713
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 29 deletions.
11 changes: 11 additions & 0 deletions lib/watir/elements/element.rb
Expand Up @@ -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.
#
Expand Down
39 changes: 32 additions & 7 deletions lib/watir/locators/element/selector_builder/xpath.rb
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion lib/watir/locators/text_field/selector_builder/xpath.rb
Expand Up @@ -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
Expand Down
13 changes: 7 additions & 6 deletions spec/unit/selector_builder/button_spec.rb
Expand Up @@ -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
Expand All @@ -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

Expand Down
51 changes: 50 additions & 1 deletion spec/unit/selector_builder/element_spec.rb
Expand Up @@ -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
Expand Down Expand Up @@ -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
28 changes: 14 additions & 14 deletions spec/unit/selector_builder/text_field_spec.rb
Expand Up @@ -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
Expand All @@ -33,15 +33,15 @@
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

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
Expand Down

0 comments on commit 0b9d713

Please sign in to comment.