insite 0.0.2 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/insite.rb +13 -7
- data/lib/insite/component/component.rb +454 -0
- data/lib/insite/component/component_collection.rb +112 -0
- data/lib/insite/component/component_instance_methods.rb +4 -0
- data/lib/insite/component/component_methods.rb +4 -0
- data/lib/insite/constants.rb +323 -31
- data/lib/insite/element/element.rb +147 -0
- data/lib/insite/element/element_collection.rb +102 -0
- data/lib/insite/element/generated/class_map.rb +244 -0
- data/lib/insite/element/generated/element_classes.rb +721 -0
- data/lib/insite/element/generated/element_instance_methods.rb +1594 -0
- data/lib/insite/errors.rb +2 -0
- data/lib/insite/examples/material_angular_io/components/angular_material_component.rb +13 -0
- data/lib/insite/examples/material_angular_io/components/example_viewer.rb +5 -0
- data/lib/insite/examples/material_angular_io/components/mat_chip.rb +29 -0
- data/lib/insite/examples/material_angular_io/components/mat_chip_list.rb +21 -0
- data/lib/insite/examples/material_angular_io/components/mat_form_field.rb +5 -0
- data/lib/insite/examples/material_angular_io/components/mat_icon.rb +5 -0
- data/lib/insite/examples/material_angular_io/components/mat_input.rb +5 -0
- data/lib/insite/examples/material_angular_io/components/mat_option.rb +3 -0
- data/lib/insite/examples/material_angular_io/components/mat_select.rb +15 -0
- data/lib/insite/examples/material_angular_io/components/mat_select_content.rb +13 -0
- data/lib/insite/examples/material_angular_io/components/no_selector.rb +3 -0
- data/lib/insite/examples/material_angular_io/pages.rb +20 -0
- data/lib/insite/examples/material_angular_io/site.rb +5 -0
- data/lib/insite/examples/material_angular_io/utils.rb +6 -0
- data/lib/insite/examples/material_angular_io/watir_mods.rb +54 -0
- data/lib/insite/examples/material_angular_io_site.rb +54 -0
- data/lib/insite/insite.rb +96 -11
- data/lib/insite/methods/common_methods.rb +26 -37
- data/lib/insite/methods/dom_methods.rb +73 -46
- data/lib/insite/page/defined_page.rb +37 -50
- data/lib/insite/page/undefined_page.rb +12 -1
- data/lib/insite/version.rb +1 -1
- metadata +69 -29
- data/lib/insite/element_container/element_container.rb +0 -42
- data/lib/insite/feature/feature.rb +0 -30
- data/lib/insite/widget/widget.rb +0 -346
- data/lib/insite/widget/widget_methods.rb +0 -4
data/lib/insite/errors.rb
CHANGED
@@ -6,10 +6,12 @@ module Insite
|
|
6
6
|
class Insite::Errors::BrowserNotOpenError < StandardError; end
|
7
7
|
class Insite::Errors::BrowserNotValidError < StandardError; end
|
8
8
|
class Insite::Errors::BrowserResponseError < StandardError; end
|
9
|
+
class Insite::Errors::ComponentReferenceError < StandardError; end
|
9
10
|
class Insite::Errors::PageConfigError < StandardError; end
|
10
11
|
class Insite::Errors::PageInitError < StandardError; end
|
11
12
|
class Insite::Errors::PageNavigationError < StandardError; end
|
12
13
|
class Insite::Errors::PageNavigationNotAllowedError < StandardError; end
|
14
|
+
class Insite::Errors::ComponentSelectorError < StandardError; end
|
13
15
|
class Insite::Errors::SiteInitError < StandardError; end
|
14
16
|
class Insite::Errors::WrongPageError < StandardError; end
|
15
17
|
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class AngularMaterialComponent < MaterialAngularIO::Component
|
2
|
+
attr_reader :ngcontent, :nghost
|
3
|
+
|
4
|
+
def ngcontent
|
5
|
+
@ngcontent ||=
|
6
|
+
attributes.keys.find { |k| k.match(/^_ngcontent-.*/) }
|
7
|
+
end
|
8
|
+
|
9
|
+
def nghost
|
10
|
+
@nghost ||=
|
11
|
+
attributes.keys.find { |k| k.match(/^_nghost-.*/) }
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require_relative 'angular_material_component'
|
2
|
+
|
3
|
+
class MatChip < MaterialAngularIO::Component
|
4
|
+
select_by tag_name: 'mat-chip'
|
5
|
+
|
6
|
+
def disabled?
|
7
|
+
element(class: 'mat-chip-disabled').exist?
|
8
|
+
end
|
9
|
+
|
10
|
+
def label
|
11
|
+
nokogiri.xpath('//text()')[0]
|
12
|
+
end
|
13
|
+
|
14
|
+
def remove
|
15
|
+
element(class: 'mat-chip-remove').click
|
16
|
+
end
|
17
|
+
|
18
|
+
def removable?
|
19
|
+
element(class: 'mat-chip-remove').exist?
|
20
|
+
end
|
21
|
+
|
22
|
+
def selected?
|
23
|
+
aria_selected == 'true'
|
24
|
+
end
|
25
|
+
|
26
|
+
def selectable?
|
27
|
+
element(class: 'mat-chip-select').exist?
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require_relative 'angular_material_component'
|
2
|
+
require 'pry'
|
3
|
+
class MatChipList < AngularMaterialComponent
|
4
|
+
select_by tag_name: 'mat-chip-list'
|
5
|
+
|
6
|
+
def add(value)
|
7
|
+
mat_input(ngcontent => true).set(value + "\n")
|
8
|
+
end
|
9
|
+
|
10
|
+
def clear_input(value)
|
11
|
+
mat_input(ngcontent => true).clear
|
12
|
+
end
|
13
|
+
|
14
|
+
def remove(value)
|
15
|
+
mat_input(ngcontent => true).mat_icon.click
|
16
|
+
end
|
17
|
+
|
18
|
+
def set_input(value)
|
19
|
+
mat_input(ngcontent => true).set(value)
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class MatSelect < MaterialAngularIO::Component
|
2
|
+
select_by tag_name: 'mat-select'
|
3
|
+
|
4
|
+
def options(*args)
|
5
|
+
mat_options(parse_args(args).merge!(css: "[#{ngcontent}='']"))
|
6
|
+
end
|
7
|
+
|
8
|
+
def option(*args)
|
9
|
+
mat_option(parse_args(args).merge!(css: "[#{ngcontent}='']"))
|
10
|
+
end
|
11
|
+
|
12
|
+
def select(*values)
|
13
|
+
values.each { |val| option(text: process_value(val)) }.click
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class MatSelectContent < MaterialAngularIO::Component
|
2
|
+
select_by class: 'mat-select-content'
|
3
|
+
|
4
|
+
# def select(**values)
|
5
|
+
# values.each do |v|
|
6
|
+
# if tmp = options.find { |opt| opt.text =~ /#{v}/i}
|
7
|
+
# tmp.click
|
8
|
+
# else
|
9
|
+
# raise ArgumentError, ""
|
10
|
+
# end
|
11
|
+
# end
|
12
|
+
# end
|
13
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require_relative 'site'
|
2
|
+
|
3
|
+
# Auto-generates some of material.angular.io pages
|
4
|
+
%w(autocomplete checkbox datepicker form-field input radio
|
5
|
+
select slider slide-toggle menu sidenav toolbar card divider
|
6
|
+
expansion grid-list list stepper tabs tree button button-toggle
|
7
|
+
badge chips icon progress-spinner progress-bar bottom-sheet
|
8
|
+
dialog snackbar tooltip paginator sort table
|
9
|
+
).each do |component|
|
10
|
+
%w(overview api examples).each do |category|
|
11
|
+
klass = Class.new(MaterialAngularIO::Page) do
|
12
|
+
set_url "/components/#{component}/#{category}"
|
13
|
+
end
|
14
|
+
|
15
|
+
Object.const_set(
|
16
|
+
"#{component.gsub('-', '_')}_#{category}_page".camelcase,
|
17
|
+
klass
|
18
|
+
)
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# https://github.com/watir/watir/pull/737
|
2
|
+
# TODO: Already merged to master, can remove after next release.
|
3
|
+
module Watir
|
4
|
+
module Locators
|
5
|
+
class Element
|
6
|
+
class SelectorBuilder
|
7
|
+
class XPath
|
8
|
+
# @todo Get rid of building
|
9
|
+
def lhs_for(_building, key)
|
10
|
+
case key
|
11
|
+
when :text, 'text'
|
12
|
+
'normalize-space()'
|
13
|
+
when String
|
14
|
+
"@#{key}"
|
15
|
+
when :href
|
16
|
+
# TODO: change this behaviour?
|
17
|
+
'normalize-space(@href)'
|
18
|
+
when :type
|
19
|
+
# type attributes can be upper case - downcase them
|
20
|
+
# https://github.com/watir/watir/issues/72
|
21
|
+
XpathSupport.downcase('@type')
|
22
|
+
else
|
23
|
+
"@#{key.to_s.tr("_", "-")}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# https://github.com/watir/watir/blob/0943e7e6a88f58ad6bea0e33a9853e6f5676f31c/lib/watir/element_collection.rb
|
33
|
+
# TODO: Already merged to master, can remove after next release.
|
34
|
+
module Watir
|
35
|
+
class ElementCollection
|
36
|
+
def to_a
|
37
|
+
hash = {}
|
38
|
+
@to_a ||=
|
39
|
+
elements.map.with_index do |e, idx|
|
40
|
+
element = element_class.new(@query_scope, @selector.merge(element: e, index: idx))
|
41
|
+
if [Watir::HTMLElement, Watir::Input].include? element.class
|
42
|
+
tag_name = element.tag_name.to_sym
|
43
|
+
hash[tag_name] ||= 0
|
44
|
+
hash[tag_name] += 1
|
45
|
+
Watir.element_class_for(tag_name).new(@query_scope, @selector.merge(element: e,
|
46
|
+
tag_name: tag_name,
|
47
|
+
index: hash[tag_name] - 1))
|
48
|
+
else
|
49
|
+
element
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# Require support files.
|
2
|
+
%w(watir_mods site pages utils).each do |f|
|
3
|
+
require "insite/examples/material_angular_io/#{f}"
|
4
|
+
end
|
5
|
+
|
6
|
+
# Require hand-crafted artisanal components.
|
7
|
+
%w(example_viewer mat_chip_list mat_chip mat_icon mat_form_field
|
8
|
+
mat_input mat_option mat_select_content mat_select no_selector).each do |c|
|
9
|
+
require "insite/examples/material_angular_io/components/#{c}"
|
10
|
+
end
|
11
|
+
|
12
|
+
# Most of the pages for material.angular.io are auto-generated because
|
13
|
+
# their URLs match a pattern. But some custom page definitions are included
|
14
|
+
# below. This first overwrites the auto-generated page definition for
|
15
|
+
# the /chips/overview page, adding some additional stuff that is used
|
16
|
+
# in some of the automated tests.
|
17
|
+
class ChipsOverviewPage < MaterialAngularIO::Page
|
18
|
+
set_url "/components/chips/overview"
|
19
|
+
|
20
|
+
no_selector :test_no_selector
|
21
|
+
no_selector :test_no_selector_with_args, tag_name: :div, index: 3
|
22
|
+
|
23
|
+
elements :mat_examples_collection, tag_name: 'example-viewer'
|
24
|
+
elements :modified_mat_examples_collection, tag_name: 'example-viewer' do
|
25
|
+
def test_method
|
26
|
+
'output'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
elements :examples_collection, tag_name: 'example-viewer'
|
31
|
+
elements :modified_examples_collection, tag_name: 'example-viewer' do
|
32
|
+
def test_method
|
33
|
+
'output'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
mat_chip :chip, tag_name: 'mat-chip'
|
38
|
+
element :modified_chip, tag_name: 'mat-chip' do
|
39
|
+
def test_method
|
40
|
+
text
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
element :chip_element, tag_name: 'mat-chip'
|
45
|
+
element :modified_chip_element, tag_name: 'mat-chip' do
|
46
|
+
def test_method
|
47
|
+
text
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
mat_chip_list :basic_chips, index: 0
|
52
|
+
mat_chip_list :chips_with_input, index: 1
|
53
|
+
mat_chip_list :chips_autocomplete, index: 2
|
54
|
+
end
|
data/lib/insite/insite.rb
CHANGED
@@ -10,19 +10,44 @@ module Insite
|
|
10
10
|
attr_reader :base_url, :unique_methods, :browser_type
|
11
11
|
attr_accessor :pages, :browser, :arguments, :most_recent_page
|
12
12
|
|
13
|
+
def self.class_to_tag(klass)
|
14
|
+
if klass.respond_to?(:collection) && klass.collection?
|
15
|
+
Watir.tag_to_class.key(klass) ||
|
16
|
+
Watir.tag_to_class.key(CLASS_MAP.key(klass)) ||
|
17
|
+
Watir.tag_to_class.key(CLASS_MAP.key(klass.collection_member_class))
|
18
|
+
else
|
19
|
+
Watir.tag_to_class.key(klass) ||
|
20
|
+
Watir.tag_to_class.key(CLASS_MAP.key(klass))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.tag_to_class(tag)
|
25
|
+
CLASS_MAP[Watir.tag_to_class[tag]] || Insite::HTMLElement
|
26
|
+
end
|
27
|
+
|
13
28
|
# Automatically sets up a Page class when Insite is included. Probably overkill
|
14
29
|
# but it protects against the case where two different sites are used at the
|
15
30
|
# same time: Each site will use its own page objects only.
|
16
31
|
def self.included(base)
|
17
32
|
mod = Module.new
|
18
|
-
base.const_set('
|
33
|
+
base.const_set('ComponentMethods', mod)
|
19
34
|
|
20
35
|
klass = Class.new(DefinedPage)
|
21
36
|
base.const_set('Page', klass)
|
22
|
-
base::send(:extend,
|
37
|
+
base::send(:extend, ComponentMethods)
|
23
38
|
|
24
39
|
klass = Class.new(UndefinedPage)
|
25
40
|
base.const_set('UndefinedPage', klass)
|
41
|
+
|
42
|
+
class << base
|
43
|
+
attr_reader :custom_tags
|
44
|
+
@custom_tags = []
|
45
|
+
end
|
46
|
+
|
47
|
+
base.define_singleton_method(:set_custom_tags) do |*tags|
|
48
|
+
@custom_tags ||= []
|
49
|
+
tags.sort.each { |t| @custom_tags << t.to_s.downcase.dasherize }
|
50
|
+
end
|
26
51
|
end
|
27
52
|
|
28
53
|
# Returns true if there's an open browser (that's also responding.) False if not.
|
@@ -36,7 +61,7 @@ module Insite
|
|
36
61
|
|
37
62
|
# Closes the site object's browser/driver.
|
38
63
|
def close
|
39
|
-
@browser.close
|
64
|
+
@browser.close if browser?
|
40
65
|
end
|
41
66
|
|
42
67
|
def describe
|
@@ -74,6 +99,23 @@ EOF
|
|
74
99
|
browser?
|
75
100
|
end
|
76
101
|
|
102
|
+
def generate_tag_classes
|
103
|
+
tags = []
|
104
|
+
cli = Highline.new
|
105
|
+
|
106
|
+
loop do
|
107
|
+
tags = (tags + find_non_standard_tags).uniq.sort
|
108
|
+
cli.choose do |menu|
|
109
|
+
menu.prompt "Found #{tags.length} non-standard tags. Choose one of the following options:"
|
110
|
+
menu.choice(:list_tags) { puts tags.join(",\n") + "\n" }
|
111
|
+
menu.choice(:continue) {}
|
112
|
+
menu.choice(:write_to_console) do
|
113
|
+
end
|
114
|
+
menu.choice(:exist_without_writing) { break }
|
115
|
+
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
77
119
|
# Creates a site object, which will have accessor methods for all pages that
|
78
120
|
# you have defined for the site. This object takes a hash argument. There is
|
79
121
|
# only one required value (the base_url for the site.) Example:
|
@@ -94,12 +136,25 @@ EOF
|
|
94
136
|
# site.bar
|
95
137
|
# => 1
|
96
138
|
# TODO: Sort args.
|
97
|
-
def initialize(base_url,
|
139
|
+
def initialize(base_url = nil, hsh={})
|
98
140
|
@arguments = hsh.with_indifferent_access
|
99
141
|
@base_url = base_url
|
100
142
|
@browser_type = (@arguments[:browser] ? @arguments[:browser].to_sym : nil)
|
101
143
|
@pages = self.class::DefinedPage.descendants.reject { |p| p.page_template? }
|
102
144
|
|
145
|
+
# Build generic components for custom tags, which are defined
|
146
|
+
# using Site.custom_tags.
|
147
|
+
if self.class.custom_tags
|
148
|
+
self.class.custom_tags.each do |tag|
|
149
|
+
# TODO: Ditch string interpolation.
|
150
|
+
self.class.class_eval %Q{
|
151
|
+
class #{tag.underscore.camelize} < Component
|
152
|
+
select_by tag_name: "#{tag}"
|
153
|
+
end
|
154
|
+
}
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
103
158
|
# Set up accessor methods for each page and page checking methods..
|
104
159
|
@pages.each do |current_page|
|
105
160
|
unless current_page.page_template?
|
@@ -144,6 +199,23 @@ EOF
|
|
144
199
|
"@most_recent_page=#{@most_recent_page}>"
|
145
200
|
end
|
146
201
|
|
202
|
+
def find_non_standard_tags
|
203
|
+
@browser.elements(xpath: non_standard_tag_xpath).map do |e|
|
204
|
+
e.html.match(/<(\S+?(?=[\s,>]))/)[1]
|
205
|
+
end.uniq.sort
|
206
|
+
end
|
207
|
+
|
208
|
+
def html_tags
|
209
|
+
%i(html title head body) + Insite::METHOD_MAP.values.flatten.each do |mth|
|
210
|
+
elem = @browser.send(mth)
|
211
|
+
elem.respond_to?(:selector) ? elem.selector.values.first.to_s : nil
|
212
|
+
end.sort
|
213
|
+
end
|
214
|
+
|
215
|
+
def non_standard_tag_xpath
|
216
|
+
"//*[#{html_tags.map { |sym| "not(local-name(.) = '#{sym}')" }.join(" and ") }]"
|
217
|
+
end
|
218
|
+
|
147
219
|
# In cases where Insite doesn't recognize a method call it will try to do the following:
|
148
220
|
# - Delegate the method call to the most recently accessed page, which is stored in
|
149
221
|
# Insite#most_recent_page.
|
@@ -153,19 +225,23 @@ EOF
|
|
153
225
|
#
|
154
226
|
# If delegation doesn't work then a NoMethodError will be raised with some details about
|
155
227
|
# what was attempted.
|
156
|
-
def method_missing(
|
228
|
+
def method_missing(mth, *args, &block)
|
157
229
|
original_page = @most_recent_page
|
158
|
-
if original_page.respond_to?(
|
159
|
-
original_page.public_send(
|
230
|
+
if original_page.respond_to?(mth)
|
231
|
+
original_page.public_send(mth, *args, &block)
|
160
232
|
else
|
161
233
|
new_page = page
|
162
|
-
if new_page.respond_to?(
|
163
|
-
page.public_send(
|
234
|
+
if new_page.respond_to?(mth)
|
235
|
+
page.public_send(mth, *args, &block)
|
236
|
+
elsif !new_page.defined?
|
237
|
+
raise NoMethodError, "Could not apply #{mth}. The current page could not be " \
|
238
|
+
"recognized. Current URL #{@browser.url}"
|
164
239
|
else
|
240
|
+
# TODO: Make it clearer where the method got called.
|
165
241
|
raise(
|
166
242
|
NoMethodError,
|
167
|
-
"Unable to apply
|
168
|
-
"currently displayed page doesn't support it either.\n" \
|
243
|
+
"Unable to apply method call :#{mth}. The site object doesn't support it. " \
|
244
|
+
"The currently displayed page (#{new_page}) doesn't support it either.\n" \
|
169
245
|
"Page:\t\t#{new_page.class}\n" \
|
170
246
|
"Current URL:\t#{@browser.url}\n\n",
|
171
247
|
caller
|
@@ -276,6 +352,15 @@ EOF
|
|
276
352
|
end
|
277
353
|
end
|
278
354
|
|
355
|
+
def respond_to_missing?(mth, include_priv = false)
|
356
|
+
# TODO: Page context changes.
|
357
|
+
@most_recent_page.respond_to?(mth, include_priv) || super
|
358
|
+
end
|
359
|
+
|
360
|
+
def target
|
361
|
+
@browser
|
362
|
+
end
|
363
|
+
|
279
364
|
def text
|
280
365
|
@browser.text
|
281
366
|
end
|