require 'spec_helper'

describe Watir::Locators::Element::Locator do
  include LocatorSpecHelper

  describe "finds a single element" do
    describe "by delegating to Selenium" do
      SELENIUM_SELECTORS.each do |loc|
        it "delegates to Selenium's #{loc} locator" do
          expect_one(loc, "bar").and_return(element(tag_name: "div"))
          match = %i[link link_text partial_link_text].include?(loc) ? :to : :to_not
          msg = /:#{loc} locator is deprecated\. Use :visible_text instead/
          expect { locate_one loc => "bar" }.send(match, output(msg).to_stdout_from_any_process)
        end
      end
    end

    describe "with selectors not supported by Selenium" do
      it "handles selector with tag name and a single attribute" do
        expect_one :xpath, ".//div[@title='foo']"

        locate_one tag_name: "div",
                   title: "foo"
      end

      it "handles selector with no tag name and and a single attribute" do
        expect_one :xpath, ".//*[@title='foo']"

        locate_one title: "foo"
      end

      it "handles single quotes in the attribute string" do
        expect_one :xpath, %{.//*[@title=concat('foo and ',"'",'bar',"'",'')]}

        locate_one title: "foo and 'bar'"
      end

      it "handles selector with tag name and multiple attributes" do
        expect_one :xpath, ".//div[@title='foo' and @dir='bar']"

        locate_one [:tag_name, "div",
                    :title   , "foo",
                    :dir     , 'bar']
      end

      it "handles selector with no tag name and multiple attributes" do
        expect_one :xpath, ".//*[@dir='foo' and @title='bar']"

        locate_one [:dir,   "foo",
                    :title, "bar"]
      end

      it "handles selector with attribute presence" do
        expect_one :xpath, ".//*[@data-view]"

        locate_one [:data_view, true]
      end

      it "handles selector with attribute absence" do
        expect_one :xpath, ".//*[not(@data-view)]"

        locate_one [:data_view, false]
      end

      it "handles selector with class attribute presence" do
        expect_one :xpath, ".//*[@class]"

        locate_one class: true
      end

      it "handles selector with multiple classes in array" do
        expect_one :xpath, ".//*[(contains(concat(' ', @class, ' '), ' a ') and contains(concat(' ', @class, ' '), ' b '))]"

        locate_one class: ["a", "b"]
      end

      it "handles selector with multiple classes in string" do
        expect_one :xpath, ".//*[contains(concat(' ', @class, ' '), ' a b ')]"

        msg = /:class locator to locate multiple classes with a String value is deprecated\. Use Array instead/
        expect { locate_one class: "a b" }.to output(msg).to_stdout_from_any_process
      end

      it "handles selector with tag_name and xpath" do
          elements = [
              element(tag_name: "div", attributes: { class: "foo" }),
              element(tag_name: "span", attributes: { class: "foo" }),
              element(tag_name: "div", attributes: { class: "foo" })
          ]

          expect_all(:xpath, './/*[@class="foo"]').and_return(elements)

          selector = {
              xpath: './/*[@class="foo"]',
              tag_name: 'span'
          }

          expect(locate_one(selector).tag_name).to eq 'span'
      end

      it "handles custom attributes" do
        elements = [
            element(tag_name: "div", attributes: { custom_attribute: "foo" }),
            element(tag_name: "span", attributes: { custom_attribute: "foo" }),
            element(tag_name: "div", attributes: { custom_attribute: "foo" })
        ]

        expect_one(:xpath, ".//span[@custom-attribute='foo']").and_return(elements[1])

        selector = {
            custom_attribute: 'foo',
            tag_name: 'span'
        }

        expect(locate_one(selector).tag_name).to eq 'span'
      end
    end

    describe "with special cased selectors" do
      it "normalizes space for :text" do
        expect_one :xpath, ".//div[normalize-space()='foo']"
        locate_one tag_name: "div",
                   text: "foo"
      end

      it "translates :caption to :text" do
        expect_one :xpath, ".//div[normalize-space()='foo']"

        locate_one tag_name: "div",
                   caption: "foo"
      end

      it "handles data-* attributes" do
        expect_one :xpath, ".//div[@data-name='foo']"

        locate_one tag_name: "div",
                   data_name: "foo"
      end

      it "handles aria-* attributes" do
        expect_one :xpath, ".//div[@aria-label='foo']"

        locate_one tag_name: "div",
                   aria_label: "foo"
      end

      it "normalizes space for the :href attribute" do
        expect_one :xpath, ".//a[normalize-space(@href)='foo']"

        selector = {
          tag_name: "a",
          href: "foo"
        }

        locate_one selector, Watir::Anchor.attributes
      end

      it "wraps :type attribute with translate() for upper case values" do
        translated_type = "translate(@type,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')"
        expect_one :xpath, ".//input[#{translated_type}='file']"

        selector = [
          :tag_name, "input",
          :type    , "file",
        ]

        locate_one selector, Watir::Input.attributes
      end

      it "uses the corresponding <label>'s @for attribute or parent::label when locating by label" do
        translated_type = "translate(@type,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')"
        expect_one :xpath, ".//input[#{translated_type}='text' and (@id=//label[normalize-space()='foo']/@for or parent::label[normalize-space()='foo'])]"

        selector = [
          :tag_name, "input",
          :type    , "text",
          :label   , "foo"
        ]

        locate_one selector, Watir::Input.attributes
      end

      it "uses label attribute if it is valid for element" do
        expect_one :xpath, ".//option[@label='foo']"

        selector = { tag_name: "option", label: "foo" }
        locate_one selector, Watir::Option.attributes
      end

      it "translates ruby attribute names to content attribute names" do
        expect_one :xpath, ".//meta[@http-equiv='foo']"

        selector = {
          tag_name: "meta",
          http_equiv: "foo"
        }

        locate_one selector, Watir::Meta.attributes

        # TODO: check edge cases
      end
    end

    describe "with regexp selectors" do
      it "handles selector with tag name and a single regexp attribute" do
        elements = [
          element(tag_name: "div", attributes: { class: "foo" }),
          element(tag_name: "div", attributes: { class: "foob"})
        ]

        expect_all(:xpath, "(.//div)[contains(@class, 'oob')]").and_return(elements)

        expect(locate_one(tag_name: "div", class: /oob/)).to eq elements[1]
      end

      it "handles :tag_name, :index and a single regexp attribute" do
        elements = [
          element(tag_name: "div", attributes: { class: "foo" }),
          element(tag_name: "div", attributes: { class: "foo" })
        ]

        expect_all(:xpath, "(.//div)[contains(@class, 'foo')]").and_return(elements)

        selector = {
          tag_name: "div",
          class: /foo/,
          index: 1
        }

        expect(locate_one(selector)).to eq elements[1]
      end

      it "handles :xpath and :index selectors" do
        elements = [
          element(tag_name: "div", attributes: { class: "foo" }),
          element(tag_name: "div", attributes: { class: "foo" })
        ]

        expect_all(:xpath, './/div[@class="foo"]').and_return(elements)

        selector = {
          xpath: './/div[@class="foo"]',
          index: 1
        }

        expect(locate_one(selector)).to eq elements[1]
      end

      it "handles :css and :index selectors" do
        elements = [
          element(tag_name: "div", attributes: { class: "foo" }),
          element(tag_name: "div", attributes: { class: "foo" })
        ]

        expect_all(:css, 'div[class="foo"]').and_return(elements)

        selector = {
          css: 'div[class="foo"]',
          index: 1
        }

        expect(locate_one(selector)).to eq elements[1]
      end

      it "handles mix of string and regexp attributes" do
        elements = [
          element(tag_name: "div", attributes: { dir: "foo", title: "bar" }),
          element(tag_name: "div", attributes: { dir: "foo", title: "baz" })
        ]

        expect_all(:xpath, "(.//div[@dir='foo'])[contains(@title, 'baz')]").and_return(elements)

        selector = {
          tag_name: "div",
          dir: "foo",
          title: /baz/
        }

        expect(locate_one(selector)).to eq elements[1]
      end

      it "handles data-* attributes with regexp" do
        elements = [
          element(tag_name: "div", attributes: { :'data-automation-id' => "foo" }),
          element(tag_name: "div", attributes: { :'data-automation-id' => "bar" })
        ]

        expect_all(:xpath, "(.//div)[contains(@data-automation-id, 'bar')]").and_return(elements)

        selector = {
          tag_name: "div",
          data_automation_id: /bar/
        }

        expect(locate_one(selector)).to eq elements[1]
      end

      it "handles :label => /regexp/ selector" do
        label_elements = [
          element(tag_name: "label", text: "foo", attributes: { 'for' => "bar"}),
          element(tag_name: "label", text: "foob", attributes: { 'for' => "baz"})
        ]
        div_elements = [element(tag_name: "div")]

        expect_all(:tag_name, "label").ordered.and_return(label_elements)
        expect_one(:xpath, ".//div[@id='baz']").ordered.and_return(div_elements.first)

        allow(browser).to receive(:ensure_context).and_return(nil)
        allow(browser).to receive(:execute_script).and_return('foo', 'foob')

        expect(locate_one(tag_name: "div", label: /oob/)).to eq div_elements.first
      end

      it "returns nil when no label matching the regexp is found" do
        expect_all(:tag_name, "label").and_return([])
        expect(locate_one(tag_name: "div", label: /foo/)).to be_nil
      end
    end

    it "finds all if :index is given" do
      # or could we use XPath indeces reliably instead?
      elements = [
        element(tag_name: "div"),
        element(tag_name: "div")
      ]

      expect_all(:xpath, ".//div[@dir='foo']").and_return(elements)

      selector = {
        tag_name: "div",
        dir: "foo",
        index: 1
      }

      expect(locate_one(selector)).to eq elements[1]
    end

    it "returns nil if found element didn't match the selector tag_name" do
      expect_all(:xpath, "//div").and_return([element(tag_name: "div")])

      selector = {
        tag_name: "input",
        xpath: "//div"
      }

      expect(locate_one(selector, Watir::Input.attributes)).to be_nil
    end

    describe "errors" do
      it "raises a TypeError if :index is not a Integer" do
        expect { locate_one(tag_name: "div", index: "bar") }.to \
        raise_error(TypeError, %[expected Integer, got "bar":String])
      end

      it "raises a TypeError if selector value is not a String, Regexp or Boolean" do
        num_type = RUBY_VERSION[/^\d+\.(\d+)/, 1].to_i >= 4 ? 'Integer' : 'Fixnum'
        expect { locate_one(tag_name: 123) }.to \
        raise_error(TypeError, %[expected one of [Array, String, Regexp, TrueClass, FalseClass, Symbol], got 123:#{num_type}])
      end
    end
  end

  describe "finds several elements" do
    describe "by delegating to Selenium" do
      SELENIUM_SELECTORS.each do |loc|
        it "delegates to Selenium's #{loc} locator" do
          expect_all(loc, "bar").and_return([element(tag_name: "div")])
          match = %i[link link_text partial_link_text].include?(loc) ? :to : :to_not
          msg = /:#{loc} locator is deprecated\. Use :visible_text instead/
          expect { locate_all loc => "bar" }.send(match, output(msg).to_stdout_from_any_process)
        end
      end
    end

    describe "with an empty selector" do
      it "finds all when an empty selctor is given" do
        expect_all :xpath, './/*'
        locate_all({})
      end
    end

    describe "with selectors not supported by Selenium" do
      it "handles selector with tag name and a single attribute" do
        expect_all :xpath, ".//div[@dir='foo']"
        locate_all tag_name: "div",
                   dir: "foo"
      end

      it "handles selector with tag name and multiple attributes" do
        expect_all :xpath, ".//div[@dir='foo' and @title='bar']"
        locate_all [:tag_name, "div",
                    :dir     , "foo",
                    :title   , 'bar']
      end

      it "handles selector with class attribute presence" do
        expect_all :xpath, ".//*[@class]"

        locate_all class: true
      end

      it "handles selector with multiple classes in array" do
        expect_all :xpath, ".//*[(contains(concat(' ', @class, ' '), ' a ') and contains(concat(' ', @class, ' '), ' b '))]"

        locate_all class: ["a", "b"]
      end

      it "handles selector with multiple classes in string" do
        expect_all :xpath, ".//*[contains(concat(' ', @class, ' '), ' a b ')]"

        msg = /Using the :class locator to locate multiple classes with a String value is deprecated\. Use Array instead/
        expect { locate_all class: "a b" }.to output(msg).to_stdout_from_any_process
      end
    end

    describe "with regexp selectors" do
      it "handles selector with tag name and a single regexp attribute" do
        elements = [
          element(tag_name: "div", attributes: { class: "foo" }),
          element(tag_name: "div", attributes: { class: "foob"}),
          element(tag_name: "div", attributes: { class: "doob"}),
          element(tag_name: "div", attributes: { class: "noob"})
        ]

        expect_all(:xpath, "(.//div)[contains(@class, 'oob')]").and_return(elements)
        expect(locate_all(tag_name: "div", class: /oob/)).to eq elements.last(3)
      end

      it "handles mix of string and regexp attributes" do
        elements = [
          element(tag_name: "div", attributes: { dir: "foo", title: "bar" }),
          element(tag_name: "div", attributes: { dir: "foo", title: "baz" }),
          element(tag_name: "div", attributes: { dir: "foo", title: "bazt"})
        ]

        expect_all(:xpath, "(.//div[@dir='foo'])[contains(@title, 'baz')]").and_return(elements)

        selector = {
          tag_name: "div",
          dir: "foo",
          title: /baz/
        }

        expect(locate_all(selector)).to eq elements.last(2)
      end

      context "and xpath" do
        it "converts a leading run of regexp literals to a contains() expression" do
          elements = [
            element(tag_name: "div", attributes: { class: "foo" }),
            element(tag_name: "div", attributes: { class: "foob" }),
            element(tag_name: "div", attributes: { class: "bar" })
          ]

          expect_all(:xpath, "(.//div)[contains(@class, 'fo')]").and_return(elements.first(2))

          expect(locate_one(tag_name: "div", class: /fo.b$/)).to eq elements[1]
        end

        it "converts a trailing run of regexp literals to a contains() expression" do
          elements = [
            element(tag_name: "div", attributes: { class: "foo" }),
            element(tag_name: "div", attributes: { class: "foob" })
          ]

          expect_all(:xpath, "(.//div)[contains(@class, 'b')]").and_return(elements.last(1))

          expect(locate_one(tag_name: "div", class: /^fo.b/)).to eq elements[1]
        end

        it "converts a leading and a trailing run of regexp literals to a contains() expression" do
          elements = [
            element(tag_name: "div", attributes: { class: "foo" }),
            element(tag_name: "div", attributes: { class: "foob" })
          ]

          expect_all(:xpath, "(.//div)[contains(@class, 'fo') and contains(@class, 'b')]").
            and_return(elements.last(1))

          expect(locate_one(tag_name: "div", class: /fo.b/)).to eq elements[1]
        end

        it "does not try to convert case insensitive expressions" do
          elements = [
            element(tag_name: "div", attributes: { class: "foo" }),
            element(tag_name: "div", attributes: { class: "foob"})
          ]

          expect_all(:xpath, ".//div").and_return(elements.last(1))

          expect(locate_one(tag_name: "div", class: /FOOB/i)).to eq elements[1]
        end

        it "does not try to convert expressions containing '|'" do
          elements = [
            element(tag_name: "div", attributes: { class: "foo" }),
            element(tag_name: "div", attributes: { class: "foob"})
          ]

          expect_all(:xpath, ".//div").and_return(elements.last(1))

          expect(locate_one(tag_name: "div", class: /x|b/)).to eq elements[1]
        end
      end
    end

    describe "errors" do
      it "raises ArgumentError if :index is given" do
        expect { locate_all(tag_name: "div", index: 1) }.to \
        raise_error(ArgumentError, "can't locate all elements by :index")
      end
    end
  end

end