cucumber-salad 0.1.0 → 0.2.0
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.
- data/lib/cucumber/salad/conversions.rb +1 -1
- data/lib/cucumber/salad/instance_conversions.rb +21 -0
- data/lib/cucumber/salad/node_text.rb +11 -0
- data/lib/cucumber/salad/table/cell_text.rb +9 -0
- data/lib/cucumber/salad/table/mapping.rb +42 -0
- data/lib/cucumber/salad/table/transformations.rb +15 -0
- data/lib/cucumber/salad/table/void_mapping.rb +10 -0
- data/lib/cucumber/salad/table.rb +61 -25
- data/lib/cucumber/salad/version.rb +1 -1
- data/lib/cucumber/salad/widgets/action.rb +9 -0
- data/lib/cucumber/salad/widgets/{text.rb → atom.rb} +2 -2
- data/lib/cucumber/salad/widgets/auto_table.rb +66 -0
- data/lib/cucumber/salad/widgets/base_table.rb +13 -0
- data/lib/cucumber/salad/widgets/form.rb +22 -7
- data/lib/cucumber/salad/widgets/list.rb +3 -5
- data/lib/cucumber/salad/widgets/table.rb +43 -54
- data/lib/cucumber/salad/widgets/widget.rb +24 -15
- data/lib/cucumber/salad/widgets.rb +4 -1
- data/lib/cucumber/salad.rb +6 -0
- metadata +28 -3
@@ -0,0 +1,21 @@
|
|
1
|
+
module Cucumber
|
2
|
+
module Salad
|
3
|
+
module InstanceConversions
|
4
|
+
def self.included(base)
|
5
|
+
base.send :include, Cucumber::Salad::Conversions
|
6
|
+
end
|
7
|
+
|
8
|
+
def to_boolean
|
9
|
+
Boolean(self)
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_a
|
13
|
+
List(self)
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_time
|
17
|
+
Timeish(self)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Cucumber
|
2
|
+
module Salad
|
3
|
+
class Table
|
4
|
+
class Mapping
|
5
|
+
def initialize(settings = {})
|
6
|
+
self.key = settings[:key]
|
7
|
+
self.value_transformer = transformer(settings[:value_transformer], :pass)
|
8
|
+
self.key_transformer = transformer(settings[:key_transformer], :keyword)
|
9
|
+
end
|
10
|
+
|
11
|
+
def set(instance, row, key, value)
|
12
|
+
row[transform_key(instance, key)] = transform_value(instance, value)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
attr_accessor :key, :value_transformer, :key_transformer
|
18
|
+
|
19
|
+
def transform_key(_, k)
|
20
|
+
key || key_transformer.(k)
|
21
|
+
end
|
22
|
+
|
23
|
+
def transform_value(instance, value)
|
24
|
+
instance.instance_exec(value, &value_transformer)
|
25
|
+
end
|
26
|
+
|
27
|
+
def transformer(set, fallback)
|
28
|
+
case set
|
29
|
+
when Symbol
|
30
|
+
Transformations.send(set)
|
31
|
+
when Proc
|
32
|
+
set
|
33
|
+
when nil
|
34
|
+
Transformations.send(fallback)
|
35
|
+
else
|
36
|
+
raise ArgumentError, "can't convert #{set.inspect} to transformer"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/cucumber/salad/table.rb
CHANGED
@@ -7,39 +7,70 @@ module Cucumber
|
|
7
7
|
include Conversions
|
8
8
|
|
9
9
|
class << self
|
10
|
-
def
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
10
|
+
def Array(table)
|
11
|
+
new(table).to_a
|
12
|
+
end
|
13
|
+
|
14
|
+
def Hash(table)
|
15
|
+
new(table).to_h
|
15
16
|
end
|
16
17
|
|
17
18
|
def map(name, options = {}, &block)
|
18
|
-
|
19
|
-
|
19
|
+
case name
|
20
|
+
when :*
|
21
|
+
set_default_mapping options, &block
|
22
|
+
else
|
23
|
+
set_mapping name, options, &block
|
24
|
+
end
|
20
25
|
end
|
21
26
|
|
22
|
-
def
|
23
|
-
@
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
+
def mappings
|
28
|
+
@mappings ||= Hash.
|
29
|
+
new { |h, k| h[k] = Mapping.new }.
|
30
|
+
merge(with_parent_mappings)
|
31
|
+
end
|
32
|
+
|
33
|
+
def skip(name)
|
34
|
+
case name
|
35
|
+
when :*
|
36
|
+
set_default_mapping VoidMapping
|
37
|
+
else
|
38
|
+
raise ArgumentError, "can't convert #{name.inspect} to name"
|
39
|
+
end
|
27
40
|
end
|
28
41
|
|
29
42
|
private
|
30
43
|
|
31
|
-
def
|
32
|
-
|
44
|
+
def set_default_mapping(options, &block)
|
45
|
+
case options
|
46
|
+
when Hash
|
47
|
+
@mappings = Hash.
|
48
|
+
new { |h, k|
|
49
|
+
h[k] = Mapping.new(key_transformer: options[:to],
|
50
|
+
value_transformer: block) }.
|
51
|
+
merge(mappings)
|
52
|
+
when Class
|
53
|
+
@mappings = Hash.new { |h, k| h[k] = options.new }.merge(mappings)
|
54
|
+
else
|
55
|
+
raise ArgumentError, "can't convert #{options.inspect} to mapping"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def set_mapping(name, options, &block)
|
60
|
+
mappings[name] = Mapping.
|
61
|
+
new(key: options[:to], value_transformer: block)
|
62
|
+
end
|
33
63
|
|
34
|
-
|
35
|
-
|
64
|
+
def with_parent_mappings
|
65
|
+
if superclass.respond_to?(:mappings)
|
66
|
+
superclass.send(:mappings).dup
|
36
67
|
else
|
37
|
-
|
68
|
+
{}
|
38
69
|
end
|
39
70
|
end
|
40
71
|
end
|
41
72
|
|
42
|
-
def_delegators 'self.class', :
|
73
|
+
def_delegators 'self.class', :mappings
|
43
74
|
|
44
75
|
def initialize(table)
|
45
76
|
self.table = table
|
@@ -53,20 +84,25 @@ module Cucumber
|
|
53
84
|
@rows ||= table.hashes.map { |h| new_row(h) }
|
54
85
|
end
|
55
86
|
|
87
|
+
def single_row
|
88
|
+
@single_row ||= new_row(table.rows_hash)
|
89
|
+
end
|
90
|
+
|
91
|
+
alias_method :to_a, :rows
|
92
|
+
alias_method :to_h, :single_row
|
93
|
+
|
56
94
|
private
|
57
95
|
|
58
96
|
attr_accessor :table
|
59
97
|
|
60
|
-
def key_for(header)
|
61
|
-
header_mappings[header]
|
62
|
-
end
|
63
|
-
|
64
98
|
def new_row(hash)
|
65
|
-
hash.each_with_object({}) { |(k, v), h|
|
99
|
+
hash.each_with_object({}) { |(k, v), h|
|
100
|
+
mapping_for(k).set(self, h, k, CellText.new(v))
|
101
|
+
}
|
66
102
|
end
|
67
103
|
|
68
|
-
def
|
69
|
-
|
104
|
+
def mapping_for(header)
|
105
|
+
mappings[header]
|
70
106
|
end
|
71
107
|
end
|
72
108
|
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Cucumber
|
2
|
+
module Salad
|
3
|
+
module Widgets
|
4
|
+
class AutoTable < BaseTable
|
5
|
+
protected
|
6
|
+
|
7
|
+
def ensure_table_loaded
|
8
|
+
root.find(data_row_selector)
|
9
|
+
rescue Capybara::Ambiguous
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def data_cell_selector
|
15
|
+
'td'
|
16
|
+
end
|
17
|
+
|
18
|
+
def data_row(node)
|
19
|
+
Row.new(root: node, cell_selector: data_cell_selector)
|
20
|
+
end
|
21
|
+
|
22
|
+
def data_row_selector
|
23
|
+
'tbody tr'
|
24
|
+
end
|
25
|
+
|
26
|
+
def data_rows
|
27
|
+
@data_rows ||= root.all(data_row_selector).map { |n| data_row(n) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def header_selector
|
31
|
+
'thead th'
|
32
|
+
end
|
33
|
+
|
34
|
+
def headers
|
35
|
+
@headers ||= root.all(header_selector).map { |n| node_text(n).downcase }
|
36
|
+
end
|
37
|
+
|
38
|
+
def values
|
39
|
+
@values ||= data_rows.map(&:values)
|
40
|
+
end
|
41
|
+
|
42
|
+
class Row < Widget
|
43
|
+
def initialize(settings)
|
44
|
+
s = settings.dup
|
45
|
+
|
46
|
+
self.cell_selector = s.delete(:cell_selector)
|
47
|
+
|
48
|
+
super s
|
49
|
+
end
|
50
|
+
|
51
|
+
def values
|
52
|
+
root.all(cell_selector).map { |n| node_text(n) }
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
attr_accessor :cell_selector
|
58
|
+
|
59
|
+
def node_text(node)
|
60
|
+
NodeText.new(node)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -2,9 +2,15 @@ module Cucumber
|
|
2
2
|
module Salad
|
3
3
|
module Widgets
|
4
4
|
class Form < Widget
|
5
|
+
def self.default_locator(type = nil, &block)
|
6
|
+
alias_method :name_to_locator, type if type
|
7
|
+
|
8
|
+
define_method :name_to_locator, &block if block
|
9
|
+
end
|
10
|
+
|
5
11
|
def self.check_box(name, label = nil)
|
6
12
|
define_method "#{name}=" do |val|
|
7
|
-
l = label ||
|
13
|
+
l = label || name_to_locator(name)
|
8
14
|
|
9
15
|
if val
|
10
16
|
root.check l
|
@@ -19,7 +25,7 @@ module Cucumber
|
|
19
25
|
label, = args
|
20
26
|
|
21
27
|
define_method "#{name}=" do |val|
|
22
|
-
l = label ||
|
28
|
+
l = label || name_to_locator(name)
|
23
29
|
w = opts.fetch(:writer) { ->(v) { v } }
|
24
30
|
|
25
31
|
root.select w.(val).to_s, from: l
|
@@ -32,14 +38,19 @@ module Cucumber
|
|
32
38
|
|
33
39
|
def self.text(name, label = nil)
|
34
40
|
define_method "#{name}=" do |val|
|
35
|
-
l = label ||
|
41
|
+
l = label || name_to_locator(name)
|
36
42
|
|
37
43
|
root.fill_in l, with: val.to_s
|
38
44
|
end
|
39
45
|
end
|
40
46
|
|
41
|
-
def initialize(
|
42
|
-
|
47
|
+
def initialize(settings = {})
|
48
|
+
s = settings.dup
|
49
|
+
data = s.delete(:data) || {}
|
50
|
+
|
51
|
+
super s
|
52
|
+
|
53
|
+
fill_all data
|
43
54
|
|
44
55
|
if block_given?
|
45
56
|
yield self
|
@@ -57,16 +68,20 @@ module Cucumber
|
|
57
68
|
end
|
58
69
|
|
59
70
|
def submit
|
60
|
-
root.find('[
|
71
|
+
root.find('[type = "submit"]').click
|
61
72
|
|
62
73
|
self
|
63
74
|
end
|
64
75
|
|
65
76
|
private
|
66
77
|
|
67
|
-
def
|
78
|
+
def label(name)
|
68
79
|
name.to_s.humanize
|
69
80
|
end
|
81
|
+
|
82
|
+
def name_to_locator(name)
|
83
|
+
label(name)
|
84
|
+
end
|
70
85
|
end
|
71
86
|
end
|
72
87
|
end
|
@@ -2,13 +2,11 @@ module Cucumber
|
|
2
2
|
module Salad
|
3
3
|
module Widgets
|
4
4
|
class List < Widget
|
5
|
-
DEFAULT_TYPE =
|
6
|
-
|
7
|
-
extend Forwardable
|
5
|
+
DEFAULT_TYPE = Atom
|
8
6
|
|
9
7
|
include Enumerable
|
10
8
|
|
11
|
-
def_delegators :items, :size, :include?, :each, :empty
|
9
|
+
def_delegators :items, :size, :include?, :each, :empty?, :first
|
12
10
|
|
13
11
|
def self.item(selector, type = DEFAULT_TYPE, &item_for)
|
14
12
|
define_method :item_selector do
|
@@ -19,7 +17,7 @@ module Cucumber
|
|
19
17
|
define_method :item_for, &item_for
|
20
18
|
else
|
21
19
|
define_method :item_factory do
|
22
|
-
|
20
|
+
type
|
23
21
|
end
|
24
22
|
end
|
25
23
|
end
|
@@ -1,79 +1,68 @@
|
|
1
1
|
module Cucumber
|
2
2
|
module Salad
|
3
3
|
module Widgets
|
4
|
-
class Table <
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
4
|
+
class Table < BaseTable
|
5
|
+
class ColumnDefinition
|
6
|
+
attr_reader :header
|
7
|
+
|
8
|
+
def initialize(selector, header, transform)
|
9
|
+
self.selector = selector
|
10
|
+
self.header = header
|
11
|
+
self.transform = transform
|
12
|
+
end
|
12
13
|
|
13
|
-
|
14
|
+
def ensure_loaded(container)
|
15
|
+
container.find(selector)
|
16
|
+
rescue Capybara::Ambiguous
|
17
|
+
end
|
14
18
|
|
15
|
-
|
16
|
-
|
17
|
-
|
19
|
+
def values(container)
|
20
|
+
container.all(selector).map { |n| transform.(node_text(n)).to_s }
|
21
|
+
end
|
18
22
|
|
19
|
-
|
20
|
-
'thead th'
|
21
|
-
end
|
23
|
+
private
|
22
24
|
|
23
|
-
|
24
|
-
|
25
|
-
end
|
25
|
+
attr_accessor :selector
|
26
|
+
attr_writer :header, :transform
|
26
27
|
|
27
|
-
|
28
|
-
|
29
|
-
|
28
|
+
def node_text(node)
|
29
|
+
NodeText.new(node)
|
30
|
+
end
|
30
31
|
|
31
|
-
|
32
|
-
|
32
|
+
def transform
|
33
|
+
@transform ||= ->(v) { v }
|
34
|
+
end
|
33
35
|
end
|
34
36
|
|
35
|
-
|
36
|
-
|
37
|
+
class << self
|
38
|
+
attr_accessor :column_selector, :header_selector
|
37
39
|
end
|
38
40
|
|
39
|
-
def
|
40
|
-
|
41
|
+
def self.column(selector, header = nil, &transform)
|
42
|
+
column_definitions << ColumnDefinition.new(selector, header, transform)
|
41
43
|
end
|
42
44
|
|
43
|
-
def
|
44
|
-
|
45
|
+
def self.column_definitions
|
46
|
+
@column_definitions ||= []
|
45
47
|
end
|
46
48
|
|
47
|
-
|
48
|
-
def self.cell(name, selector, type = Text, &block)
|
49
|
-
widget name, selector, type, &block
|
49
|
+
protected
|
50
50
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
def self.cells
|
55
|
-
@cells ||= []
|
56
|
-
end
|
57
|
-
|
58
|
-
def to_a
|
59
|
-
declared_row || generated_row
|
60
|
-
end
|
61
|
-
|
62
|
-
protected
|
51
|
+
def ensure_table_loaded
|
52
|
+
column_definitions.first.ensure_loaded(self)
|
53
|
+
end
|
63
54
|
|
64
|
-
|
65
|
-
'td'
|
66
|
-
end
|
55
|
+
private
|
67
56
|
|
68
|
-
|
69
|
-
|
57
|
+
def_delegators 'self.class', :column_selector, :column_definitions,
|
58
|
+
:header_selector
|
70
59
|
|
71
|
-
|
72
|
-
|
60
|
+
def headers
|
61
|
+
@headers ||= column_definitions.map(&:header)
|
62
|
+
end
|
73
63
|
|
74
|
-
|
75
|
-
|
76
|
-
end
|
64
|
+
def values
|
65
|
+
@values ||= column_definitions.map { |d| d.values(root) }.transpose
|
77
66
|
end
|
78
67
|
end
|
79
68
|
end
|
@@ -2,8 +2,20 @@ module Cucumber
|
|
2
2
|
module Salad
|
3
3
|
module Widgets
|
4
4
|
class Widget
|
5
|
+
extend Forwardable
|
6
|
+
|
5
7
|
include Salad::Conversions
|
6
8
|
|
9
|
+
def self.action(name, selector, options = {})
|
10
|
+
wname = "#{name}_action"
|
11
|
+
|
12
|
+
widget wname, selector, type: options[:type] || Action
|
13
|
+
|
14
|
+
define_method name do
|
15
|
+
send(wname).click
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
7
19
|
def self.root(selector)
|
8
20
|
define_method :default_root_selector do
|
9
21
|
selector
|
@@ -12,14 +24,13 @@ module Cucumber
|
|
12
24
|
private :default_root_selector
|
13
25
|
end
|
14
26
|
|
15
|
-
def self.widget(name, selector,
|
16
|
-
|
17
|
-
|
18
|
-
else
|
19
|
-
->{ lookup(type).new(root: root.find(selector)) }
|
20
|
-
end
|
27
|
+
def self.widget(name, selector, options = {}, &block)
|
28
|
+
type = options.fetch(:type, Atom)
|
29
|
+
t = block_given? ? Class.new(type, &block) : type
|
21
30
|
|
22
|
-
define_method name
|
31
|
+
define_method name do
|
32
|
+
t.new(root: root.find(selector))
|
33
|
+
end
|
23
34
|
end
|
24
35
|
|
25
36
|
def initialize(settings = {})
|
@@ -33,6 +44,12 @@ module Cucumber
|
|
33
44
|
Nokogiri::XML(xml, &:noblanks).to_xhtml
|
34
45
|
end
|
35
46
|
|
47
|
+
protected
|
48
|
+
|
49
|
+
def node_text(node)
|
50
|
+
NodeText.new(node)
|
51
|
+
end
|
52
|
+
|
36
53
|
private
|
37
54
|
|
38
55
|
attr_writer :root_selector
|
@@ -42,14 +59,6 @@ module Cucumber
|
|
42
59
|
"#{self.class.name}: default root selector undefined"
|
43
60
|
end
|
44
61
|
|
45
|
-
def lookup(type)
|
46
|
-
if Class === type
|
47
|
-
type
|
48
|
-
else
|
49
|
-
self.class.const_get(type.to_s.classify)
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
62
|
def page
|
54
63
|
Capybara.current_session
|
55
64
|
end
|
@@ -1,3 +1,6 @@
|
|
1
1
|
require 'cucumber/salad/widgets/widget'
|
2
|
-
require 'cucumber/salad/widgets/
|
2
|
+
require 'cucumber/salad/widgets/atom'
|
3
3
|
require 'cucumber/salad/widgets/list'
|
4
|
+
require 'cucumber/salad/widgets/base_table'
|
5
|
+
require 'cucumber/salad/widgets/auto_table'
|
6
|
+
require 'cucumber/salad/widgets/table'
|
data/lib/cucumber/salad.rb
CHANGED
@@ -1,5 +1,11 @@
|
|
1
1
|
require 'chronic'
|
2
2
|
|
3
3
|
require 'cucumber/salad/conversions'
|
4
|
+
require 'cucumber/salad/instance_conversions'
|
5
|
+
require 'cucumber/salad/node_text'
|
4
6
|
require 'cucumber/salad/widgets'
|
5
7
|
require 'cucumber/salad/table'
|
8
|
+
require 'cucumber/salad/table/mapping'
|
9
|
+
require 'cucumber/salad/table/void_mapping'
|
10
|
+
require 'cucumber/salad/table/transformations'
|
11
|
+
require 'cucumber/salad/table/cell_text'
|
metadata
CHANGED
@@ -2,14 +2,14 @@
|
|
2
2
|
name: cucumber-salad
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 0.
|
5
|
+
version: 0.2.0
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- David Leal
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-05-20 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -43,6 +43,22 @@ dependencies:
|
|
43
43
|
- !ruby/object:Gem::Version
|
44
44
|
version: '0'
|
45
45
|
none: false
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
version_requirements: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ! '>='
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: '2.0'
|
52
|
+
none: false
|
53
|
+
name: capybara
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ! '>='
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '2.0'
|
61
|
+
none: false
|
46
62
|
- !ruby/object:Gem::Dependency
|
47
63
|
version_requirements: !ruby/object:Gem::Requirement
|
48
64
|
requirements:
|
@@ -68,13 +84,22 @@ extra_rdoc_files: []
|
|
68
84
|
files:
|
69
85
|
- lib/cucumber/salad.rb
|
70
86
|
- lib/cucumber/salad/conversions.rb
|
87
|
+
- lib/cucumber/salad/instance_conversions.rb
|
88
|
+
- lib/cucumber/salad/node_text.rb
|
71
89
|
- lib/cucumber/salad/table.rb
|
90
|
+
- lib/cucumber/salad/table/cell_text.rb
|
91
|
+
- lib/cucumber/salad/table/mapping.rb
|
92
|
+
- lib/cucumber/salad/table/transformations.rb
|
93
|
+
- lib/cucumber/salad/table/void_mapping.rb
|
72
94
|
- lib/cucumber/salad/version.rb
|
73
95
|
- lib/cucumber/salad/widgets.rb
|
96
|
+
- lib/cucumber/salad/widgets/action.rb
|
97
|
+
- lib/cucumber/salad/widgets/atom.rb
|
98
|
+
- lib/cucumber/salad/widgets/auto_table.rb
|
99
|
+
- lib/cucumber/salad/widgets/base_table.rb
|
74
100
|
- lib/cucumber/salad/widgets/form.rb
|
75
101
|
- lib/cucumber/salad/widgets/list.rb
|
76
102
|
- lib/cucumber/salad/widgets/table.rb
|
77
|
-
- lib/cucumber/salad/widgets/text.rb
|
78
103
|
- lib/cucumber/salad/widgets/widget.rb
|
79
104
|
homepage: https://github.com/mojotech/cucumber-salad
|
80
105
|
licenses: []
|