dill 0.4.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/dill.rb +28 -0
- data/lib/dill/auto_table.rb +73 -0
- data/lib/dill/base_table.rb +9 -0
- data/lib/dill/conversions.rb +29 -0
- data/lib/dill/cucumber.rb +3 -0
- data/lib/dill/document.rb +14 -0
- data/lib/dill/dsl.rb +34 -0
- data/lib/dill/field_group.rb +331 -0
- data/lib/dill/form.rb +15 -0
- data/lib/dill/instance_conversions.rb +19 -0
- data/lib/dill/list.rb +47 -0
- data/lib/dill/node_text.rb +9 -0
- data/lib/dill/table.rb +66 -0
- data/lib/dill/text_table.rb +107 -0
- data/lib/dill/text_table/cell_text.rb +7 -0
- data/lib/dill/text_table/mapping.rb +40 -0
- data/lib/dill/text_table/transformations.rb +13 -0
- data/lib/dill/text_table/void_mapping.rb +8 -0
- data/lib/dill/version.rb +3 -0
- data/lib/dill/widget.rb +212 -0
- data/lib/dill/widget_container.rb +23 -0
- data/lib/dill/widget_name.rb +56 -0
- metadata +165 -0
data/lib/dill.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'chronic'
|
2
|
+
require 'nokogiri'
|
3
|
+
require 'capybara'
|
4
|
+
|
5
|
+
require 'dill/widget_container'
|
6
|
+
require 'dill/conversions'
|
7
|
+
require 'dill/instance_conversions'
|
8
|
+
require 'dill/node_text'
|
9
|
+
require 'dill/widget_name'
|
10
|
+
require 'dill/widget'
|
11
|
+
require 'dill/list'
|
12
|
+
require 'dill/base_table'
|
13
|
+
require 'dill/auto_table'
|
14
|
+
require 'dill/table'
|
15
|
+
require 'dill/field_group'
|
16
|
+
require 'dill/form'
|
17
|
+
require 'dill/document'
|
18
|
+
require 'dill/text_table'
|
19
|
+
require 'dill/text_table/mapping'
|
20
|
+
require 'dill/text_table/void_mapping'
|
21
|
+
require 'dill/text_table/transformations'
|
22
|
+
require 'dill/text_table/cell_text'
|
23
|
+
require 'dill/dsl'
|
24
|
+
|
25
|
+
module Dill
|
26
|
+
# An exception that signals that something is missing.
|
27
|
+
class Missing < StandardError; end
|
28
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Dill
|
2
|
+
class AutoTable < BaseTable
|
3
|
+
|
4
|
+
# don't include footer in to_table, because footer column configuration is very
|
5
|
+
# often different from the headers & values.
|
6
|
+
def footers
|
7
|
+
@footers ||= root.all(footer_selector).map { |n| node_text(n) }
|
8
|
+
end
|
9
|
+
|
10
|
+
protected
|
11
|
+
|
12
|
+
def ensure_table_loaded
|
13
|
+
root.find(data_row_selector)
|
14
|
+
rescue Capybara::Ambiguous
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def data_cell_selector
|
20
|
+
'td'
|
21
|
+
end
|
22
|
+
|
23
|
+
def data_row(node)
|
24
|
+
Row.new(root: node, cell_selector: data_cell_selector)
|
25
|
+
end
|
26
|
+
|
27
|
+
def data_row_selector
|
28
|
+
'tbody tr'
|
29
|
+
end
|
30
|
+
|
31
|
+
def data_rows
|
32
|
+
@data_rows ||= root.all(data_row_selector).map { |n| data_row(n) }
|
33
|
+
end
|
34
|
+
|
35
|
+
def header_selector
|
36
|
+
'thead th'
|
37
|
+
end
|
38
|
+
|
39
|
+
def headers
|
40
|
+
@headers ||= root.all(header_selector).map { |n| node_text(n).downcase }
|
41
|
+
end
|
42
|
+
|
43
|
+
def footer_selector
|
44
|
+
'tfoot td'
|
45
|
+
end
|
46
|
+
|
47
|
+
def values
|
48
|
+
@values ||= data_rows.map(&:values)
|
49
|
+
end
|
50
|
+
|
51
|
+
class Row < Widget
|
52
|
+
def initialize(settings)
|
53
|
+
s = settings.dup
|
54
|
+
|
55
|
+
self.cell_selector = s.delete(:cell_selector)
|
56
|
+
|
57
|
+
super s
|
58
|
+
end
|
59
|
+
|
60
|
+
def values
|
61
|
+
root.all(cell_selector).map { |n| node_text(n) }
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
attr_accessor :cell_selector
|
67
|
+
|
68
|
+
def node_text(node)
|
69
|
+
NodeText.new(node)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Dill
|
2
|
+
module Conversions
|
3
|
+
def Boolean(val)
|
4
|
+
case val
|
5
|
+
when 'yes', 'true', true
|
6
|
+
true
|
7
|
+
when 'no', 'false', false, nil, ''
|
8
|
+
false
|
9
|
+
else
|
10
|
+
raise ArgumentError, "can't convert #{val.inspect} to boolean"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def List(valstr, &block)
|
15
|
+
vs = valstr.strip.split(/\s*,\s*/)
|
16
|
+
|
17
|
+
block ? vs.map(&block) : vs
|
18
|
+
end
|
19
|
+
|
20
|
+
def Timeish(val)
|
21
|
+
raise ArgumentError, "can't convert nil to Timeish" if val.nil?
|
22
|
+
|
23
|
+
return val if Date === val || Time === val || DateTime === val
|
24
|
+
|
25
|
+
Chronic.parse(val) or
|
26
|
+
raise ArgumentError, "can't parse #{val.inspect} to Timeish"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/dill/dsl.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
module Dill
|
2
|
+
module DSL
|
3
|
+
attr_writer :widget_lookup_scope
|
4
|
+
|
5
|
+
# @return [Document] the current document with the class of the
|
6
|
+
# current object set as the widget lookup scope.
|
7
|
+
def document
|
8
|
+
Document.new(widget_lookup_scope: widget_lookup_scope)
|
9
|
+
end
|
10
|
+
|
11
|
+
# @return [Boolean] Whether one or more widgets exist in the current
|
12
|
+
# document.
|
13
|
+
def has_widget?(name)
|
14
|
+
document.has_widget?(name)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns a widget instance for the given name.
|
18
|
+
#
|
19
|
+
# @param name [String, Symbol]
|
20
|
+
def widget(name, options = {})
|
21
|
+
document.widget(name, options)
|
22
|
+
end
|
23
|
+
|
24
|
+
def widget_lookup_scope
|
25
|
+
@widget_lookup_scope ||= default_widget_lookup_scope
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def default_widget_lookup_scope
|
31
|
+
Module === self ? self : self.class
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,331 @@
|
|
1
|
+
module Dill
|
2
|
+
# A group of form fields.
|
3
|
+
#
|
4
|
+
# @todo Explain how to use locators when defining fields, including what
|
5
|
+
# happens when locators are omitted.
|
6
|
+
class FieldGroup < Widget
|
7
|
+
root 'fieldset'
|
8
|
+
|
9
|
+
def self.default_locator(type = nil, &block)
|
10
|
+
alias_method :name_to_locator, type if type
|
11
|
+
|
12
|
+
define_method :name_to_locator, &block if block
|
13
|
+
end
|
14
|
+
|
15
|
+
# The names of all the fields that belong to this field group.
|
16
|
+
#
|
17
|
+
# Field names are automatically added to this group as long as you use
|
18
|
+
# the field definition macros.
|
19
|
+
#
|
20
|
+
# @return [Set] the field names.
|
21
|
+
#
|
22
|
+
# @see field
|
23
|
+
def self.field_names
|
24
|
+
@field_names ||= Set.new
|
25
|
+
end
|
26
|
+
|
27
|
+
# @!group Field definition macros
|
28
|
+
|
29
|
+
# Creates a new checkbox accessor.
|
30
|
+
#
|
31
|
+
# Adds the following methods to the widget:
|
32
|
+
#
|
33
|
+
# <name>:: Gets the current checkbox state, as a boolean. Returns +true+
|
34
|
+
# if the corresponding check box is checked, +false+ otherwise.
|
35
|
+
# <name>=:: Sets the current checkbox state. Pass +true+ to check the
|
36
|
+
# checkbox, +false+ otherwise.
|
37
|
+
#
|
38
|
+
# @example
|
39
|
+
# # Given the following HTML:
|
40
|
+
# #
|
41
|
+
# # <form>
|
42
|
+
# # <p>
|
43
|
+
# # <label for="checked-box">
|
44
|
+
# # <input type="checkbox" value="1" id="checked-box" checked>
|
45
|
+
# # </p>
|
46
|
+
# # <p>
|
47
|
+
# # <label for="unchecked-box">
|
48
|
+
# # <input type="checkbox" value="1" id="unchecked-box">
|
49
|
+
# # </p>
|
50
|
+
# # </form>
|
51
|
+
# class MyFieldGroup < Dill::FieldGroup
|
52
|
+
# root 'form'
|
53
|
+
#
|
54
|
+
# check_box :checked_box, 'checked-box'
|
55
|
+
# check_box :unchecked_box, 'unchecked-box'
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# form = widget(:my_field_group)
|
59
|
+
#
|
60
|
+
# form.checked_box #=> true
|
61
|
+
# form.unchecked_box #=> false
|
62
|
+
#
|
63
|
+
# form.unchecked_box = true
|
64
|
+
# form.unchecked_box #=> true
|
65
|
+
#
|
66
|
+
# @param name the name of the checkbox accessor.
|
67
|
+
# @param locator the locator for the checkbox. If +nil+ the locator will
|
68
|
+
# be derived from +name+.
|
69
|
+
#
|
70
|
+
# @todo Handle checkbox access when the field is disabled (raise an
|
71
|
+
# exception?)
|
72
|
+
def self.check_box(name, locator = nil)
|
73
|
+
field name, locator, CheckBox
|
74
|
+
end
|
75
|
+
|
76
|
+
# Defines a new field.
|
77
|
+
#
|
78
|
+
# @param name the name of the field accessor.
|
79
|
+
# @param locator the field locator.
|
80
|
+
# @param type the field class name.
|
81
|
+
#
|
82
|
+
# @api private
|
83
|
+
def self.field(name, locator, type)
|
84
|
+
raise TypeError, "can't convert `#{name}' to Symbol" \
|
85
|
+
unless name.respond_to?(:to_sym)
|
86
|
+
|
87
|
+
field_names << name.to_sym
|
88
|
+
|
89
|
+
label = name.to_s.gsub(/_/, ' ').capitalize
|
90
|
+
locator ||= label
|
91
|
+
|
92
|
+
widget name, locator, type do
|
93
|
+
define_method :label do
|
94
|
+
label
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
define_method "#{name}=" do |val|
|
99
|
+
widget(name).set val
|
100
|
+
end
|
101
|
+
|
102
|
+
define_method name do
|
103
|
+
widget(name).get
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Creates a new select accessor.
|
108
|
+
#
|
109
|
+
# Adds the following methods to the widget:
|
110
|
+
#
|
111
|
+
# <name>:: Gets the current selected option. Returns the label of the
|
112
|
+
# selected option, or +nil+, if no option is selected.
|
113
|
+
# <name>=:: Selects an option on the current select. Pass the label of
|
114
|
+
# the option you want to select.
|
115
|
+
#
|
116
|
+
# @example
|
117
|
+
# # Given the following HTML:
|
118
|
+
# #
|
119
|
+
# # <form>
|
120
|
+
# # <p>
|
121
|
+
# # <label for="selected">
|
122
|
+
# # <select id="selected">
|
123
|
+
# # <option selected>Selected option</option>
|
124
|
+
# # <option>Another option</option>
|
125
|
+
# # </select>
|
126
|
+
# # </p>
|
127
|
+
# # <p>
|
128
|
+
# # <label for="deselected">
|
129
|
+
# # <select id="deselected">
|
130
|
+
# # <option>Deselected option</option>
|
131
|
+
# # <option>Another option</option>
|
132
|
+
# # </select>
|
133
|
+
# # </p>
|
134
|
+
# # </form>
|
135
|
+
# class MyFieldGroup < Dill::FieldGroup
|
136
|
+
# root 'form'
|
137
|
+
#
|
138
|
+
# select :selected, 'selected'
|
139
|
+
# select :deselected, 'deselected'
|
140
|
+
# end
|
141
|
+
#
|
142
|
+
# form = widget(:my_field_group)
|
143
|
+
#
|
144
|
+
# form.selected #=> "Selected option"
|
145
|
+
# form.deselected #=> nil
|
146
|
+
#
|
147
|
+
# form.deselected = "Deselected option"
|
148
|
+
# form.unchecked_box #=> "Deselected option"
|
149
|
+
#
|
150
|
+
# @param name the name of the select accessor.
|
151
|
+
# @param locator the locator for the select. If +nil+ the locator will
|
152
|
+
# be derived from +name+.
|
153
|
+
#
|
154
|
+
# @todo Handle select access when the field is disabled (raise an
|
155
|
+
# exception?)
|
156
|
+
# @todo Raise an exception when an option doesn't exist.
|
157
|
+
# @todo Allow passing the option value to set an option.
|
158
|
+
# @todo Ensure an option with no text returns the empty string.
|
159
|
+
# @todo What to do when +nil+ is passed to the writer?
|
160
|
+
def self.select(name, locator = nil)
|
161
|
+
field name, locator, Select
|
162
|
+
end
|
163
|
+
|
164
|
+
# Creates a new text field accessor.
|
165
|
+
#
|
166
|
+
# Adds the following methods to the widget:
|
167
|
+
#
|
168
|
+
# <name>:: Returns the current text field value, or +nil+ if no value
|
169
|
+
# has been set.
|
170
|
+
# <name>=:: Sets the current text field value.
|
171
|
+
#
|
172
|
+
# @example
|
173
|
+
# # Given the following HTML:
|
174
|
+
# #
|
175
|
+
# # <form>
|
176
|
+
# # <p>
|
177
|
+
# # <label for="text-field">
|
178
|
+
# # <input type="text" value="Content" id="text-field">
|
179
|
+
# # </p>
|
180
|
+
# # <p>
|
181
|
+
# # <label for="empty-field">
|
182
|
+
# # <input type="text" id="empty-field">
|
183
|
+
# # </p>
|
184
|
+
# # </form>
|
185
|
+
# class MyFieldGroup < Dill::FieldGroup
|
186
|
+
# root 'form'
|
187
|
+
#
|
188
|
+
# text_field :filled_field, 'text-field'
|
189
|
+
# text_field :empty_field, 'empty-field'
|
190
|
+
# end
|
191
|
+
#
|
192
|
+
# form = widget(:my_field_group)
|
193
|
+
#
|
194
|
+
# form.filled_field #=> "Content"
|
195
|
+
# form.empty_field #=> nil
|
196
|
+
#
|
197
|
+
# form.empty_field = "Not anymore"
|
198
|
+
# form.empty_field #=> "Not anymore"
|
199
|
+
#
|
200
|
+
# @param name the name of the text field accessor.
|
201
|
+
# @param locator the locator for the text field. If +nil+ the locator
|
202
|
+
# will be derived from +name+.
|
203
|
+
#
|
204
|
+
# @todo Handle text field access when the field is disabled (raise an
|
205
|
+
# exception?)
|
206
|
+
def self.text_field(name, locator = nil)
|
207
|
+
field name, locator, TextField
|
208
|
+
end
|
209
|
+
|
210
|
+
# @!endgroup
|
211
|
+
|
212
|
+
# @return This field group's field widgets.
|
213
|
+
def fields
|
214
|
+
self.class.field_names.map { |name| widget(name) }
|
215
|
+
end
|
216
|
+
|
217
|
+
# Sets the given form attributes.
|
218
|
+
#
|
219
|
+
# @param attributes [Hash] the attributes and values we want to set.
|
220
|
+
#
|
221
|
+
# @return the current widget.
|
222
|
+
def set(attributes)
|
223
|
+
attributes.each do |k, v|
|
224
|
+
send "#{k}=", v
|
225
|
+
end
|
226
|
+
|
227
|
+
self
|
228
|
+
end
|
229
|
+
|
230
|
+
# Converts the current field group into a table suitable for diff'ing
|
231
|
+
# with Cucumber::Ast::Table.
|
232
|
+
#
|
233
|
+
# Field labels are determined by the widget name.
|
234
|
+
#
|
235
|
+
# Field values correspond to the return value of each field's +to_s+.
|
236
|
+
#
|
237
|
+
# @return [Array<Array>] the table.
|
238
|
+
def to_table
|
239
|
+
headers = fields.map { |field| field.label.downcase }
|
240
|
+
body = fields.map { |field| field.to_s.downcase }
|
241
|
+
|
242
|
+
[headers, body]
|
243
|
+
end
|
244
|
+
|
245
|
+
# A form field.
|
246
|
+
class Field < Widget
|
247
|
+
def self.find_in(parent, options)
|
248
|
+
new({root: parent.find_field(selector)}.merge(options))
|
249
|
+
end
|
250
|
+
|
251
|
+
def self.present_in?(parent)
|
252
|
+
parent.has_field?(selector)
|
253
|
+
end
|
254
|
+
|
255
|
+
# @return This field's value.
|
256
|
+
#
|
257
|
+
# Override this to get the actual value.
|
258
|
+
def get
|
259
|
+
raise NotImplementedError
|
260
|
+
end
|
261
|
+
|
262
|
+
# Sets the field value.
|
263
|
+
#
|
264
|
+
# Override this to set the value.
|
265
|
+
def set(value)
|
266
|
+
raise NotImplementedError
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
# A check box.
|
271
|
+
class CheckBox < Field
|
272
|
+
# @!method set(value)
|
273
|
+
# Checks or unchecks the current checkbox.
|
274
|
+
#
|
275
|
+
# @param value [Boolean] +true+ to check the checkbox, +false+
|
276
|
+
# otherwise.
|
277
|
+
def_delegator :root, :set
|
278
|
+
|
279
|
+
# @return [Boolean] +true+ if the checkbox is checked, +false+
|
280
|
+
# otherwise.
|
281
|
+
def get
|
282
|
+
!! root.checked?
|
283
|
+
end
|
284
|
+
|
285
|
+
# @return +"yes"+ if the checkbox is checked, +"no"+ otherwise.
|
286
|
+
def to_s
|
287
|
+
get ? 'yes' : 'no'
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
# A select.
|
292
|
+
class Select < Field
|
293
|
+
# @return [String] The text of the selected option.
|
294
|
+
def get
|
295
|
+
option = root.find('[selected]') rescue nil
|
296
|
+
|
297
|
+
option && option.text
|
298
|
+
end
|
299
|
+
|
300
|
+
# Selects the given option.
|
301
|
+
#
|
302
|
+
# @param option [String] The text of the option to select.
|
303
|
+
def set(option)
|
304
|
+
root.find('option', text: option).select_option
|
305
|
+
end
|
306
|
+
|
307
|
+
# @!method to_s
|
308
|
+
# @return the text of the selected option, or the empty string if
|
309
|
+
# no option is selected.
|
310
|
+
def_delegator :get, :to_s
|
311
|
+
end
|
312
|
+
|
313
|
+
# A text field.
|
314
|
+
class TextField < Field
|
315
|
+
# @!method get
|
316
|
+
# @return The text field value.
|
317
|
+
def_delegator :root, :value, :get
|
318
|
+
|
319
|
+
# @!method set(value)
|
320
|
+
# Sets the text field value.
|
321
|
+
#
|
322
|
+
# @param value [String] the value to set.
|
323
|
+
def_delegator :root, :set
|
324
|
+
|
325
|
+
# @!method to_s
|
326
|
+
# @return the text field value, or the empty string if the field is
|
327
|
+
# empty.
|
328
|
+
def_delegator :get, :to_s
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|
data/lib/dill/form.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
module Dill
|
2
|
+
class Form < FieldGroup
|
3
|
+
action :submit, '[type = submit]'
|
4
|
+
|
5
|
+
# Submit form with +attributes+.
|
6
|
+
#
|
7
|
+
# @param attributes [Hash] the form fields and their values
|
8
|
+
#
|
9
|
+
# @return the current widget
|
10
|
+
def submit_with(attributes)
|
11
|
+
set attributes
|
12
|
+
submit
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Dill
|
2
|
+
module InstanceConversions
|
3
|
+
def self.included(base)
|
4
|
+
base.send :include, Dill::Conversions
|
5
|
+
end
|
6
|
+
|
7
|
+
def to_boolean
|
8
|
+
Boolean(self)
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_a
|
12
|
+
List(self)
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_time
|
16
|
+
Timeish(self)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/dill/list.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
module Dill
|
2
|
+
class List < Widget
|
3
|
+
DEFAULT_TYPE = Widget
|
4
|
+
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
def_delegators :items, :size, :include?, :each, :empty?, :first, :last
|
8
|
+
|
9
|
+
def self.item(selector, type = DEFAULT_TYPE, &item_for)
|
10
|
+
define_method :item_selector do
|
11
|
+
@item_selector ||= selector
|
12
|
+
end
|
13
|
+
|
14
|
+
if block_given?
|
15
|
+
define_method :item_for, &item_for
|
16
|
+
else
|
17
|
+
define_method :item_factory do
|
18
|
+
type
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_table
|
24
|
+
items.map { |e| Array(e) }
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
attr_writer :item_selector
|
30
|
+
|
31
|
+
def item_factory
|
32
|
+
DEFAULT_TYPE
|
33
|
+
end
|
34
|
+
|
35
|
+
def item_for(node)
|
36
|
+
item_factory.new(root: node)
|
37
|
+
end
|
38
|
+
|
39
|
+
def item_selector
|
40
|
+
'li'
|
41
|
+
end
|
42
|
+
|
43
|
+
def items
|
44
|
+
root.all(item_selector).map { |node| item_for(node) }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/dill/table.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
module Dill
|
2
|
+
class Table < BaseTable
|
3
|
+
class ColumnDefinition
|
4
|
+
attr_reader :header
|
5
|
+
|
6
|
+
def initialize(selector, header, transform)
|
7
|
+
self.selector = selector
|
8
|
+
self.header = header
|
9
|
+
self.transform = transform
|
10
|
+
end
|
11
|
+
|
12
|
+
def ensure_loaded(container)
|
13
|
+
container.find(selector)
|
14
|
+
rescue Capybara::Ambiguous
|
15
|
+
end
|
16
|
+
|
17
|
+
def values(container)
|
18
|
+
container.all(selector).map { |n| transform.(node_text(n)).to_s }
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_accessor :selector
|
24
|
+
attr_writer :header, :transform
|
25
|
+
|
26
|
+
def node_text(node)
|
27
|
+
NodeText.new(node)
|
28
|
+
end
|
29
|
+
|
30
|
+
def transform
|
31
|
+
@transform ||= ->(v) { v }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class << self
|
36
|
+
attr_accessor :column_selector, :header_selector
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.column(selector, header = nil, &transform)
|
40
|
+
column_definitions << ColumnDefinition.new(selector, header, transform)
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.column_definitions
|
44
|
+
@column_definitions ||= []
|
45
|
+
end
|
46
|
+
|
47
|
+
protected
|
48
|
+
|
49
|
+
def ensure_table_loaded
|
50
|
+
column_definitions.first.ensure_loaded(self)
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def_delegators 'self.class', :column_selector, :column_definitions,
|
56
|
+
:header_selector
|
57
|
+
|
58
|
+
def headers
|
59
|
+
@headers ||= column_definitions.map(&:header)
|
60
|
+
end
|
61
|
+
|
62
|
+
def values
|
63
|
+
@values ||= column_definitions.map { |d| d.values(root) }.transpose
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module Dill
|
2
|
+
class TextTable
|
3
|
+
extend Forwardable
|
4
|
+
|
5
|
+
include Enumerable
|
6
|
+
include Conversions
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def Array(table)
|
10
|
+
new(table).to_a
|
11
|
+
end
|
12
|
+
|
13
|
+
def Hash(table)
|
14
|
+
new(table).to_h
|
15
|
+
end
|
16
|
+
|
17
|
+
def map(name, options = {}, &block)
|
18
|
+
case name
|
19
|
+
when :*
|
20
|
+
set_default_mapping options, &block
|
21
|
+
else
|
22
|
+
set_mapping name, options, &block
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def mappings
|
27
|
+
@mappings ||= Hash.
|
28
|
+
new { |h, k| h[k] = Mapping.new }.
|
29
|
+
merge(with_parent_mappings)
|
30
|
+
end
|
31
|
+
|
32
|
+
def skip(name)
|
33
|
+
case name
|
34
|
+
when :*
|
35
|
+
set_default_mapping VoidMapping
|
36
|
+
else
|
37
|
+
raise ArgumentError, "can't convert #{name.inspect} to name"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def set_default_mapping(options, &block)
|
44
|
+
case options
|
45
|
+
when Hash
|
46
|
+
@mappings = Hash.
|
47
|
+
new { |h, k|
|
48
|
+
h[k] = Mapping.new(key_transformer: options[:to],
|
49
|
+
value_transformer: block) }.
|
50
|
+
merge(mappings)
|
51
|
+
when Class
|
52
|
+
@mappings = Hash.new { |h, k| h[k] = options.new }.merge(mappings)
|
53
|
+
else
|
54
|
+
raise ArgumentError, "can't convert #{options.inspect} to mapping"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def set_mapping(name, options, &block)
|
59
|
+
mappings[name] = Mapping.
|
60
|
+
new(key: options[:to], value_transformer: block)
|
61
|
+
end
|
62
|
+
|
63
|
+
def with_parent_mappings
|
64
|
+
if superclass.respond_to?(:mappings)
|
65
|
+
superclass.send(:mappings).dup
|
66
|
+
else
|
67
|
+
{}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def_delegators 'self.class', :mappings
|
73
|
+
|
74
|
+
def initialize(table)
|
75
|
+
self.table = table
|
76
|
+
end
|
77
|
+
|
78
|
+
def each(&block)
|
79
|
+
rows.each(&block)
|
80
|
+
end
|
81
|
+
|
82
|
+
def rows
|
83
|
+
@rows ||= table.hashes.map { |h| new_row(h) }
|
84
|
+
end
|
85
|
+
|
86
|
+
def single_row
|
87
|
+
@single_row ||= new_row(table.rows_hash)
|
88
|
+
end
|
89
|
+
|
90
|
+
alias_method :to_a, :rows
|
91
|
+
alias_method :to_h, :single_row
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
attr_accessor :table
|
96
|
+
|
97
|
+
def new_row(hash)
|
98
|
+
hash.each_with_object({}) { |(k, v), h|
|
99
|
+
mapping_for(k).set(self, h, k, CellText.new(v))
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
def mapping_for(header)
|
104
|
+
mappings[header]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Dill
|
2
|
+
class TextTable
|
3
|
+
class Mapping
|
4
|
+
def initialize(settings = {})
|
5
|
+
self.key = settings[:key]
|
6
|
+
self.value_transformer = transformer(settings[:value_transformer], :pass)
|
7
|
+
self.key_transformer = transformer(settings[:key_transformer], :keyword)
|
8
|
+
end
|
9
|
+
|
10
|
+
def set(instance, row, key, value)
|
11
|
+
row[transform_key(instance, key)] = transform_value(instance, value)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
attr_accessor :key, :value_transformer, :key_transformer
|
17
|
+
|
18
|
+
def transform_key(_, k)
|
19
|
+
key || key_transformer.(k)
|
20
|
+
end
|
21
|
+
|
22
|
+
def transform_value(instance, value)
|
23
|
+
instance.instance_exec(value, &value_transformer)
|
24
|
+
end
|
25
|
+
|
26
|
+
def transformer(set, fallback)
|
27
|
+
case set
|
28
|
+
when Symbol
|
29
|
+
Transformations.send(set)
|
30
|
+
when Proc
|
31
|
+
set
|
32
|
+
when nil
|
33
|
+
Transformations.send(fallback)
|
34
|
+
else
|
35
|
+
raise ArgumentError, "can't convert #{set.inspect} to transformer"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/dill/version.rb
ADDED
data/lib/dill/widget.rb
ADDED
@@ -0,0 +1,212 @@
|
|
1
|
+
module Dill
|
2
|
+
class Widget
|
3
|
+
extend Forwardable
|
4
|
+
|
5
|
+
include WidgetContainer
|
6
|
+
|
7
|
+
# @!group Widget macros
|
8
|
+
|
9
|
+
# Defines a new action.
|
10
|
+
#
|
11
|
+
# This is a shortcut to help defining a widget and a method that clicks
|
12
|
+
# on that widget. You can then send a widget instance the message given
|
13
|
+
# by +name+.
|
14
|
+
#
|
15
|
+
# @example
|
16
|
+
# # Consider the widget will encapsulate the following HTML
|
17
|
+
# #
|
18
|
+
# # <div id="profile">
|
19
|
+
# # <a href="/profiles/1/edit" rel="edit">Edit</a>
|
20
|
+
# # </div>
|
21
|
+
# class PirateProfile < Dill::Widget
|
22
|
+
# root "#profile"
|
23
|
+
#
|
24
|
+
# # Declare the action
|
25
|
+
# action :edit, '[rel = edit]'
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# # Click the link
|
29
|
+
# widget(:pirate_profile).edit
|
30
|
+
#
|
31
|
+
# @param name the name of the action
|
32
|
+
# @param selector the selector for the widget that will be clicked
|
33
|
+
def self.action(name, selector)
|
34
|
+
widget name, selector
|
35
|
+
|
36
|
+
define_method name do
|
37
|
+
widget(name).click
|
38
|
+
|
39
|
+
self
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Declares a new sub-widget.
|
44
|
+
#
|
45
|
+
# Sub-widgets are accessible inside the container widget using the
|
46
|
+
# +widget+ message.
|
47
|
+
#
|
48
|
+
# @param name the name of the sub-widget
|
49
|
+
# @param selector the sub-widget selector
|
50
|
+
# @param parent [Class] the parent class of the new sub-widget
|
51
|
+
#
|
52
|
+
# @yield A block allowing you to further customize the widget behavior.
|
53
|
+
#
|
54
|
+
# @see #widget
|
55
|
+
def self.widget(name, selector, parent = Widget, &block)
|
56
|
+
type = Class.new(parent) {
|
57
|
+
root selector
|
58
|
+
}
|
59
|
+
|
60
|
+
type.class_eval(&block) if block_given?
|
61
|
+
|
62
|
+
const_set(Dill::WidgetName.new(name).to_sym, type)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Creates a delegator for one sub-widget message.
|
66
|
+
#
|
67
|
+
# Since widgets are accessed through {WidgetContainer#widget}, we can't
|
68
|
+
# use {Forwardable} to delegate messages to widgets.
|
69
|
+
#
|
70
|
+
# @param name the name of the receiver sub-widget
|
71
|
+
# @param widget_message the name of the message to be sent to the sub-widget
|
72
|
+
# @param method_name the name of the delegator. If +nil+ the method will
|
73
|
+
# have the same name as the message it will send.
|
74
|
+
def self.widget_delegator(name, widget_message, method_name = nil)
|
75
|
+
method_name = method_name || widget_message
|
76
|
+
|
77
|
+
class_eval <<-RUBY
|
78
|
+
def #{method_name}(*args)
|
79
|
+
if args.size == 1
|
80
|
+
widget(:#{name}).#{widget_message} args.first
|
81
|
+
else
|
82
|
+
widget(:#{name}).#{widget_message} *args
|
83
|
+
end
|
84
|
+
end
|
85
|
+
RUBY
|
86
|
+
end
|
87
|
+
|
88
|
+
# @!endgroup
|
89
|
+
|
90
|
+
# Determines if an instance of this widget class exists in
|
91
|
+
# +parent_node+.
|
92
|
+
#
|
93
|
+
# @param parent_node [Capybara::Node] the node we want to search in
|
94
|
+
#
|
95
|
+
# @return +true+ if a widget instance is found, +false+ otherwise.
|
96
|
+
def self.present_in?(parent_node)
|
97
|
+
parent_node.has_selector?(selector)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Finds a single instance of the current widget in +node+.
|
101
|
+
#
|
102
|
+
# @param node the node we want to search in
|
103
|
+
#
|
104
|
+
# @return a new instance of the current widget class.
|
105
|
+
#
|
106
|
+
# @raise [Capybara::ElementNotFoundError] if the widget can't be found
|
107
|
+
def self.find_in(node, options = {})
|
108
|
+
new(options.merge(root: node.find(selector)))
|
109
|
+
end
|
110
|
+
|
111
|
+
# Sets this widget's default selector.
|
112
|
+
#
|
113
|
+
# @param selector [String] a CSS or XPath query
|
114
|
+
def self.root(selector)
|
115
|
+
@selector = selector
|
116
|
+
end
|
117
|
+
|
118
|
+
# @return The selector specified with +root+.
|
119
|
+
def self.selector
|
120
|
+
@selector
|
121
|
+
end
|
122
|
+
|
123
|
+
# @return The root node of the current widget
|
124
|
+
attr_reader :root
|
125
|
+
|
126
|
+
def_delegators :root, :click
|
127
|
+
|
128
|
+
def initialize(settings = {})
|
129
|
+
self.root = settings.fetch(:root)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Determines if the widget underlying an action exists.
|
133
|
+
#
|
134
|
+
# @param name the name of the action
|
135
|
+
#
|
136
|
+
# @raise Missing if an action with +name+ can't be found.
|
137
|
+
#
|
138
|
+
# @return [Boolean] +true+ if the action widget is found, +false+
|
139
|
+
# otherwise.
|
140
|
+
def has_action?(name)
|
141
|
+
raise Missing, "couldn't find `#{name}' action" unless respond_to?(name)
|
142
|
+
|
143
|
+
has_widget?(name)
|
144
|
+
end
|
145
|
+
|
146
|
+
def inspect
|
147
|
+
xml = Nokogiri::HTML(page.body).at(root.path).to_xml
|
148
|
+
|
149
|
+
"<!-- #{self.class.name}: -->\n" <<
|
150
|
+
Nokogiri::XML(xml, &:noblanks).to_xhtml
|
151
|
+
end
|
152
|
+
|
153
|
+
class Reload < Capybara::ElementNotFound; end
|
154
|
+
|
155
|
+
# Reloads the widget, waiting for its contents to change (by default),
|
156
|
+
# or until +wait_time+ expires.
|
157
|
+
#
|
158
|
+
# Call this method to make sure a widget has enough time to update
|
159
|
+
# itself.
|
160
|
+
#
|
161
|
+
# You can pass a block to this method to control what it means for the
|
162
|
+
# widget to be reloaded.
|
163
|
+
#
|
164
|
+
# *Note: does not account for multiple changes to the widget yet.*
|
165
|
+
#
|
166
|
+
# @param wait_time [Numeric] how long we should wait for changes, in
|
167
|
+
# seconds.
|
168
|
+
#
|
169
|
+
# @yield A block that determines what it means for a widget to be
|
170
|
+
# reloaded.
|
171
|
+
# @yieldreturn [Boolean] +true+ if the widget is considered to be
|
172
|
+
# reloaded, +false+ otherwise.
|
173
|
+
#
|
174
|
+
# @return the current widget
|
175
|
+
def reload(wait_time = Capybara.default_wait_time, &test)
|
176
|
+
unless test
|
177
|
+
old_inspect = inspect
|
178
|
+
test = ->{ old_inspect != inspect }
|
179
|
+
end
|
180
|
+
|
181
|
+
root.synchronize(wait_time) do
|
182
|
+
raise Reload unless test.()
|
183
|
+
end
|
184
|
+
|
185
|
+
self
|
186
|
+
rescue Reload
|
187
|
+
# raised on timeout
|
188
|
+
|
189
|
+
self
|
190
|
+
end
|
191
|
+
|
192
|
+
def to_s
|
193
|
+
node_text(root)
|
194
|
+
end
|
195
|
+
|
196
|
+
alias_method :w, :widget
|
197
|
+
|
198
|
+
protected
|
199
|
+
|
200
|
+
def node_text(node)
|
201
|
+
NodeText.new(node)
|
202
|
+
end
|
203
|
+
|
204
|
+
private
|
205
|
+
|
206
|
+
attr_writer :root
|
207
|
+
|
208
|
+
def page
|
209
|
+
Capybara.current_session
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Dill
|
2
|
+
module WidgetContainer
|
3
|
+
def has_widget?(name)
|
4
|
+
widget_class(name).present_in?(root)
|
5
|
+
end
|
6
|
+
|
7
|
+
def widget(name, options = {})
|
8
|
+
widget_class(name).find_in(root, options)
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
attr_writer :widget_lookup_scope
|
14
|
+
|
15
|
+
def widget_class(name)
|
16
|
+
WidgetName.new(name).to_class(widget_lookup_scope)
|
17
|
+
end
|
18
|
+
|
19
|
+
def widget_lookup_scope
|
20
|
+
@widget_lookup_scope || self.class
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Dill
|
2
|
+
# The name of a widget in a format-independent representation.
|
3
|
+
class WidgetName < String
|
4
|
+
CAMEL_CASE_FORMAT = /\A([A-Z][a-z]*)+\Z/
|
5
|
+
SNAKE_CASE_FORMAT = /\A\w+\Z/
|
6
|
+
|
7
|
+
# Constructs the widget name.
|
8
|
+
#
|
9
|
+
# @param name [String, Symbol] the name of the widget in primitive form
|
10
|
+
def initialize(name)
|
11
|
+
@name = name
|
12
|
+
@canonical = canonical(@name)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns the class for this widget name, in the given scope.
|
16
|
+
def to_class(scope = Object)
|
17
|
+
const = scope.const_get(@canonical)
|
18
|
+
|
19
|
+
raise TypeError, "`#{@canonical}' is not a widget in this scope" \
|
20
|
+
unless const < Widget
|
21
|
+
|
22
|
+
const
|
23
|
+
rescue NameError
|
24
|
+
raise Missing, "couldn't find `#{@name}' widget in this scope"
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_sym
|
28
|
+
@canonical.to_sym
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def canonical(name)
|
34
|
+
str = name.to_s
|
35
|
+
|
36
|
+
case str
|
37
|
+
when SNAKE_CASE_FORMAT
|
38
|
+
camel_case_from_snake_case(str)
|
39
|
+
when CAMEL_CASE_FORMAT
|
40
|
+
str
|
41
|
+
else
|
42
|
+
raise ArgumentError, "can't convert `#{str.inspect}' to canonical form"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def camel_case_from_snake_case(name)
|
47
|
+
capitalize_word = ->(word) { word[0].upcase + word[1..-1] }
|
48
|
+
word_separator = '_'
|
49
|
+
|
50
|
+
name
|
51
|
+
.split(word_separator)
|
52
|
+
.map(&capitalize_word)
|
53
|
+
.join
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
metadata
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dill
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.4.0
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- David Leal
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-07-30 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
version_requirements: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ! '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
none: false
|
21
|
+
name: cucumber
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
requirement: !ruby/object:Gem::Requirement
|
25
|
+
requirements:
|
26
|
+
- - ! '>='
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
version: '0'
|
29
|
+
none: false
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
version_requirements: !ruby/object:Gem::Requirement
|
32
|
+
requirements:
|
33
|
+
- - ! '>='
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '0'
|
36
|
+
none: false
|
37
|
+
name: chronic
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
requirement: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ! '>='
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '0'
|
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
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ~>
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: 3.0.0
|
68
|
+
none: false
|
69
|
+
name: rspec-given
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ~>
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: 3.0.0
|
77
|
+
none: false
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ! '>='
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
none: false
|
85
|
+
name: sinatra
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
requirement: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - ! '>='
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
none: false
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
version_requirements: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - ~>
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: 1.0.0
|
100
|
+
none: false
|
101
|
+
name: capybara-webkit
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
requirement: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - ~>
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: 1.0.0
|
109
|
+
none: false
|
110
|
+
description: See https://github.com/mojotech/dill/README.md
|
111
|
+
email:
|
112
|
+
- dleal@mojotech.com
|
113
|
+
executables: []
|
114
|
+
extensions: []
|
115
|
+
extra_rdoc_files: []
|
116
|
+
files:
|
117
|
+
- lib/dill.rb
|
118
|
+
- lib/dill/auto_table.rb
|
119
|
+
- lib/dill/base_table.rb
|
120
|
+
- lib/dill/conversions.rb
|
121
|
+
- lib/dill/cucumber.rb
|
122
|
+
- lib/dill/document.rb
|
123
|
+
- lib/dill/dsl.rb
|
124
|
+
- lib/dill/field_group.rb
|
125
|
+
- lib/dill/form.rb
|
126
|
+
- lib/dill/instance_conversions.rb
|
127
|
+
- lib/dill/list.rb
|
128
|
+
- lib/dill/node_text.rb
|
129
|
+
- lib/dill/table.rb
|
130
|
+
- lib/dill/text_table.rb
|
131
|
+
- lib/dill/text_table/cell_text.rb
|
132
|
+
- lib/dill/text_table/mapping.rb
|
133
|
+
- lib/dill/text_table/transformations.rb
|
134
|
+
- lib/dill/text_table/void_mapping.rb
|
135
|
+
- lib/dill/version.rb
|
136
|
+
- lib/dill/widget.rb
|
137
|
+
- lib/dill/widget_container.rb
|
138
|
+
- lib/dill/widget_name.rb
|
139
|
+
homepage: https://github.com/mojotech/dill
|
140
|
+
licenses:
|
141
|
+
- MIT
|
142
|
+
post_install_message:
|
143
|
+
rdoc_options: []
|
144
|
+
require_paths:
|
145
|
+
- lib
|
146
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
147
|
+
requirements:
|
148
|
+
- - ! '>='
|
149
|
+
- !ruby/object:Gem::Version
|
150
|
+
version: '0'
|
151
|
+
none: false
|
152
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
153
|
+
requirements:
|
154
|
+
- - ! '>='
|
155
|
+
- !ruby/object:Gem::Version
|
156
|
+
version: '0'
|
157
|
+
none: false
|
158
|
+
requirements: []
|
159
|
+
rubyforge_project: ! '[none]'
|
160
|
+
rubygems_version: 1.8.23
|
161
|
+
signing_key:
|
162
|
+
specification_version: 3
|
163
|
+
summary: A set of helpers to ease integration testing
|
164
|
+
test_files: []
|
165
|
+
has_rdoc:
|