akephalos 0.0.1 → 0.0.2
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/.gitignore +2 -0
- data/TODO.txt +18 -0
- data/VERSION +1 -1
- data/akephalos.gemspec +7 -6
- data/bin/akephalos +4 -2
- data/lib/akephalos.rb +3 -0
- data/lib/akephalos/capybara.rb +20 -70
- data/lib/akephalos/client.rb +28 -0
- data/lib/akephalos/htmlunit.rb +0 -9
- data/lib/akephalos/node.rb +68 -0
- data/lib/akephalos/page.rb +34 -0
- data/lib/akephalos/remote_client.rb +21 -0
- data/lib/akephalos/server.rb +8 -26
- metadata +8 -7
- data/lib/akephalos/htmlunit/html_element.rb +0 -11
- data/lib/akephalos/htmlunit/html_page.rb +0 -28
- data/lib/akephalos/htmlunit/html_select.rb +0 -19
- data/lib/akephalos/htmlunit/web_client.rb +0 -16
data/.gitignore
CHANGED
data/TODO.txt
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
0.0.3
|
2
|
+
=======================================================================================
|
3
|
+
* DRb server should never shut down prematurely -- missing methods, 500 errors,
|
4
|
+
even java errors (NPE error from bad javascript).
|
5
|
+
|
6
|
+
0.0.2 - 10 May 2010
|
7
|
+
=======================================================================================
|
8
|
+
|
9
|
+
- Ensure users cannot accidently call non-existant methods on DRb objects
|
10
|
+
- Refactor akephalos classes to expose HTMLUnit behavior, instead of shipping
|
11
|
+
objects directly to remote process.
|
12
|
+
|
13
|
+
|
14
|
+
0.0.1 - 03 May 2010
|
15
|
+
=======================================================================================
|
16
|
+
- DRb server starts automatically when needed
|
17
|
+
- DRb objects are undumped, and references are kept on the server
|
18
|
+
- All capybara provided specs pass.
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.2
|
data/akephalos.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{akephalos}
|
8
|
-
s.version = "0.0.
|
8
|
+
s.version = "0.0.2"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Bernerd Schaefer"]
|
12
|
-
s.date = %q{2010-05-
|
12
|
+
s.date = %q{2010-05-10}
|
13
13
|
s.default_executable = %q{akephalos}
|
14
14
|
s.description = %q{}
|
15
15
|
s.email = %q{bj.schaefer@gmail.com}
|
@@ -17,17 +17,18 @@ Gem::Specification.new do |s|
|
|
17
17
|
s.files = [
|
18
18
|
".gitignore",
|
19
19
|
"Rakefile",
|
20
|
+
"TODO.txt",
|
20
21
|
"VERSION",
|
21
22
|
"akephalos.gemspec",
|
22
23
|
"bin/akephalos",
|
23
24
|
"lib/akephalos.rb",
|
24
25
|
"lib/akephalos/capybara.rb",
|
26
|
+
"lib/akephalos/client.rb",
|
25
27
|
"lib/akephalos/cucumber.rb",
|
26
28
|
"lib/akephalos/htmlunit.rb",
|
27
|
-
"lib/akephalos/
|
28
|
-
"lib/akephalos/
|
29
|
-
"lib/akephalos/
|
30
|
-
"lib/akephalos/htmlunit/web_client.rb",
|
29
|
+
"lib/akephalos/node.rb",
|
30
|
+
"lib/akephalos/page.rb",
|
31
|
+
"lib/akephalos/remote_client.rb",
|
31
32
|
"lib/akephalos/server.rb",
|
32
33
|
"spec/driver/akephalos_driver_spec.rb",
|
33
34
|
"spec/session/akephalos_session_spec.rb",
|
data/bin/akephalos
CHANGED
@@ -5,7 +5,9 @@ raise "Usage: akephalos socket_file" unless ARGV[0]
|
|
5
5
|
require 'pathname'
|
6
6
|
|
7
7
|
root = Pathname(__FILE__).expand_path.dirname.parent
|
8
|
+
lib = root + 'lib'
|
8
9
|
jruby = root + "src/jruby-complete-1.4.0.jar"
|
9
|
-
server =
|
10
|
+
server = 'akephalos/server'
|
10
11
|
|
11
|
-
|
12
|
+
command = %Q(java -Xmx2048M -jar #{jruby} -I#{lib} -r #{server} -e 'Akephalos::Server.start!(%s)')
|
13
|
+
exec command % ARGV[0].inspect
|
data/lib/akephalos.rb
CHANGED
data/lib/akephalos/capybara.rb
CHANGED
@@ -6,41 +6,36 @@ class Capybara::Driver::Akephalos < Capybara::Driver::Base
|
|
6
6
|
name = name.to_s
|
7
7
|
case name
|
8
8
|
when 'checked'
|
9
|
-
node.
|
9
|
+
node.checked?
|
10
10
|
else
|
11
11
|
node[name.to_s]
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
15
|
def text
|
16
|
-
node.
|
16
|
+
node.text
|
17
17
|
end
|
18
18
|
|
19
19
|
def value
|
20
20
|
if tag_name == "select" && self[:multiple]
|
21
|
-
|
22
|
-
results = node.getSelectedOptions
|
23
|
-
while (i ||= 0) < results.size
|
24
|
-
values << results[i].asText
|
25
|
-
i += 1
|
26
|
-
end
|
27
|
-
values
|
21
|
+
node.selected_options.map { |option| option.text }
|
28
22
|
elsif tag_name == "select"
|
29
|
-
node.
|
23
|
+
selected_option = node.selected_options.first
|
24
|
+
selected_option ? selected_option.text : nil
|
30
25
|
else
|
31
|
-
|
26
|
+
self[:value]
|
32
27
|
end
|
33
28
|
end
|
34
29
|
|
35
30
|
def set(value)
|
36
31
|
if tag_name == 'textarea'
|
37
|
-
node.
|
32
|
+
node.value = value.to_s
|
38
33
|
elsif tag_name == 'input' and type == 'radio'
|
39
|
-
|
34
|
+
click
|
40
35
|
elsif tag_name == 'input' and type == 'checkbox'
|
41
|
-
|
36
|
+
click
|
42
37
|
elsif tag_name == 'input'
|
43
|
-
node.
|
38
|
+
node.value = value.to_s
|
44
39
|
end
|
45
40
|
end
|
46
41
|
|
@@ -48,13 +43,7 @@ class Capybara::Driver::Akephalos < Capybara::Driver::Base
|
|
48
43
|
result = node.select_option(option)
|
49
44
|
|
50
45
|
if result == nil
|
51
|
-
options =
|
52
|
-
results = node.getOptions
|
53
|
-
while (i ||= 0) < results.size
|
54
|
-
options << results[i].asText
|
55
|
-
i += 1
|
56
|
-
end
|
57
|
-
options = options.join(", ")
|
46
|
+
options = node.options.map(&:text).join(", ")
|
58
47
|
raise Capybara::OptionNotFound, "No such option '#{option}' in this select box. Available options: #{options}"
|
59
48
|
else
|
60
49
|
result
|
@@ -69,13 +58,7 @@ class Capybara::Driver::Akephalos < Capybara::Driver::Base
|
|
69
58
|
result = node.unselect_option(option)
|
70
59
|
|
71
60
|
if result == nil
|
72
|
-
options =
|
73
|
-
results = node.getOptions
|
74
|
-
while (i ||= 0) < results.size
|
75
|
-
options << results[i].asText
|
76
|
-
i += 1
|
77
|
-
end
|
78
|
-
options = options.join(", ")
|
61
|
+
options = node.options.map(&:text).join(", ")
|
79
62
|
raise Capybara::OptionNotFound, "No such option '#{option}' in this select box. Available options: #{options}"
|
80
63
|
else
|
81
64
|
result
|
@@ -87,17 +70,17 @@ class Capybara::Driver::Akephalos < Capybara::Driver::Base
|
|
87
70
|
end
|
88
71
|
|
89
72
|
def tag_name
|
90
|
-
node.
|
73
|
+
node.tag_name
|
91
74
|
end
|
92
75
|
|
93
76
|
def visible?
|
94
|
-
node.
|
77
|
+
node.visible?
|
95
78
|
end
|
96
79
|
|
97
80
|
def drag_to(element)
|
98
|
-
|
99
|
-
element.
|
100
|
-
element.
|
81
|
+
trigger('mousedown')
|
82
|
+
element.trigger('mousemove')
|
83
|
+
element.trigger('mouseup')
|
101
84
|
end
|
102
85
|
|
103
86
|
def click
|
@@ -107,13 +90,7 @@ class Capybara::Driver::Akephalos < Capybara::Driver::Base
|
|
107
90
|
private
|
108
91
|
|
109
92
|
def all_unfiltered(selector)
|
110
|
-
|
111
|
-
results = node.getByXPath(selector)
|
112
|
-
while (i ||= 0) < results.size
|
113
|
-
nodes << Node.new(driver, results[i])
|
114
|
-
i += 1
|
115
|
-
end
|
116
|
-
nodes
|
93
|
+
node.find(selector).map { |node| Node.new(driver, node) }
|
117
94
|
end
|
118
95
|
|
119
96
|
def type
|
@@ -124,28 +101,7 @@ class Capybara::Driver::Akephalos < Capybara::Driver::Base
|
|
124
101
|
attr_reader :app, :rack_server
|
125
102
|
|
126
103
|
def self.driver
|
127
|
-
|
128
|
-
require '/usr/local/projects/personal/htmlunit-ruby/jruby'
|
129
|
-
@driver ||= WebClient.new
|
130
|
-
else
|
131
|
-
@driver ||= begin
|
132
|
-
socket_file = "/tmp/akephalos.#{Process.pid}.sock"
|
133
|
-
uri = "drbunix://#{socket_file}"
|
134
|
-
|
135
|
-
server = fork do
|
136
|
-
exec("#{Pathname(__FILE__).dirname.parent.parent + 'bin/akephalos'} #{socket_file}")
|
137
|
-
end
|
138
|
-
|
139
|
-
DRb.start_service
|
140
|
-
|
141
|
-
sleep 1 until File.exists?(socket_file)
|
142
|
-
|
143
|
-
client_class = DRbObject.new_with_uri(uri)
|
144
|
-
|
145
|
-
at_exit { Process.kill(:INT, server); File.unlink(socket_file) }
|
146
|
-
client_class
|
147
|
-
end
|
148
|
-
end
|
104
|
+
@driver ||= Akephalos::Client.new
|
149
105
|
end
|
150
106
|
|
151
107
|
def initialize(app)
|
@@ -171,13 +127,7 @@ class Capybara::Driver::Akephalos < Capybara::Driver::Base
|
|
171
127
|
end
|
172
128
|
|
173
129
|
def find(selector)
|
174
|
-
|
175
|
-
results = page.find(selector)
|
176
|
-
while (i ||= 0) < results.size
|
177
|
-
nodes << Node.new(self, results[i])
|
178
|
-
i += 1
|
179
|
-
end
|
180
|
-
nodes
|
130
|
+
page.find(selector).map { |node| Node.new(self, node) }
|
181
131
|
end
|
182
132
|
|
183
133
|
def evaluate_script(script)
|
@@ -0,0 +1,28 @@
|
|
1
|
+
if RUBY_PLATFORM != "java"
|
2
|
+
require 'akephalos/remote_client'
|
3
|
+
Akephalos::Client = Akephalos::RemoteClient
|
4
|
+
else
|
5
|
+
require 'akephalos/htmlunit'
|
6
|
+
require 'akephalos/page'
|
7
|
+
require 'akephalos/node'
|
8
|
+
|
9
|
+
module Akephalos
|
10
|
+
class Client
|
11
|
+
def initialize
|
12
|
+
@_client = WebClient.new
|
13
|
+
@_client.setCssErrorHandler(com.gargoylesoftware.htmlunit.SilentCssErrorHandler.new)
|
14
|
+
end
|
15
|
+
|
16
|
+
def visit(url)
|
17
|
+
@page = Page.new(@_client.getPage(url))
|
18
|
+
end
|
19
|
+
|
20
|
+
def page
|
21
|
+
if @page != (page = @_client.getCurrentWindow.getEnclosedPage)
|
22
|
+
@page = Page.new(page)
|
23
|
+
end
|
24
|
+
@page
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/akephalos/htmlunit.rb
CHANGED
@@ -21,17 +21,8 @@ require "xml-apis-1.3.04.jar"
|
|
21
21
|
logger = org.apache.commons.logging.LogFactory.getLog('com.gargoylesoftware.htmlunit')
|
22
22
|
logger.getLogger().setLevel(java.util.logging.Level::SEVERE)
|
23
23
|
|
24
|
-
java_import 'java.io.StringWriter'
|
25
|
-
java_import 'java.io.PrintWriter'
|
26
24
|
java_import "com.gargoylesoftware.htmlunit.WebClient"
|
27
|
-
java_import "com.gargoylesoftware.htmlunit.html.HtmlPage"
|
28
|
-
java_import "com.gargoylesoftware.htmlunit.html.HtmlSubmitInput"
|
29
25
|
|
30
26
|
com.gargoylesoftware.htmlunit.BrowserVersion.setDefault(
|
31
27
|
com.gargoylesoftware.htmlunit.BrowserVersion::FIREFOX_3
|
32
28
|
)
|
33
|
-
|
34
|
-
require Pathname(__FILE__).dirname + "htmlunit/html_element"
|
35
|
-
require Pathname(__FILE__).dirname + "htmlunit/html_page"
|
36
|
-
require Pathname(__FILE__).dirname + "htmlunit/html_select"
|
37
|
-
require Pathname(__FILE__).dirname + "htmlunit/web_client"
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Akephalos
|
2
|
+
class Node
|
3
|
+
def initialize(node)
|
4
|
+
@_node = node
|
5
|
+
end
|
6
|
+
|
7
|
+
def checked?
|
8
|
+
@_node.isChecked
|
9
|
+
end
|
10
|
+
|
11
|
+
def text
|
12
|
+
@_node.asText
|
13
|
+
end
|
14
|
+
|
15
|
+
def [](name)
|
16
|
+
@_node.hasAttribute(name.to_s) ? @_node.getAttribute(name.to_s) : nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def value=(value)
|
20
|
+
case tag_name
|
21
|
+
when "textarea"
|
22
|
+
@_node.setText(value)
|
23
|
+
when "input"
|
24
|
+
@_node.setValueAttribute(value)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def select_option(option)
|
29
|
+
opt = @_node.getOptions.detect { |o| o.asText == option }
|
30
|
+
|
31
|
+
opt && opt.setSelected(true)
|
32
|
+
end
|
33
|
+
|
34
|
+
def unselect_option(option)
|
35
|
+
opt = @_node.getOptions.detect { |o| o.asText == option }
|
36
|
+
|
37
|
+
opt && opt.setSelected(false)
|
38
|
+
end
|
39
|
+
|
40
|
+
def options
|
41
|
+
@_node.getOptions.map { |node| Node.new(node) }
|
42
|
+
end
|
43
|
+
|
44
|
+
def selected_options
|
45
|
+
@_node.getSelectedOptions.map { |node| Node.new(node) }
|
46
|
+
end
|
47
|
+
|
48
|
+
def fire_event(name)
|
49
|
+
@_node.fireEvent(name)
|
50
|
+
end
|
51
|
+
|
52
|
+
def tag_name
|
53
|
+
@_node.getNodeName
|
54
|
+
end
|
55
|
+
|
56
|
+
def visible?
|
57
|
+
@_node.isDisplayed
|
58
|
+
end
|
59
|
+
|
60
|
+
def click
|
61
|
+
@_node.click
|
62
|
+
end
|
63
|
+
|
64
|
+
def find(selector)
|
65
|
+
@_node.getByXPath(selector).map { |node| Node.new(node) }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Akephalos
|
2
|
+
class Page
|
3
|
+
def initialize(page)
|
4
|
+
@nodes = []
|
5
|
+
@_page = page
|
6
|
+
end
|
7
|
+
|
8
|
+
def find(selector)
|
9
|
+
nodes = @_page.getByXPath(selector).map { |node| Node.new(node) }
|
10
|
+
@nodes << nodes
|
11
|
+
nodes
|
12
|
+
end
|
13
|
+
|
14
|
+
def modified_source
|
15
|
+
@_page.asXml
|
16
|
+
end
|
17
|
+
|
18
|
+
def source
|
19
|
+
@_page.getWebResponse.getContentAsString
|
20
|
+
end
|
21
|
+
|
22
|
+
def current_url
|
23
|
+
@_page.getWebResponse.getRequestSettings.getUrl.toString
|
24
|
+
end
|
25
|
+
|
26
|
+
def execute_script(script)
|
27
|
+
@_page.executeJavaScript(script).getJavaScriptResult
|
28
|
+
end
|
29
|
+
|
30
|
+
def ==(other)
|
31
|
+
@_page == other
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Akephalos
|
2
|
+
class RemoteClient
|
3
|
+
@socket_file = "/tmp/akephalos.#{Process.pid}.sock"
|
4
|
+
|
5
|
+
def self.start!
|
6
|
+
remote_client = fork do
|
7
|
+
exec("#{Akephalos::BIN_DIR + 'akephalos'} #{@socket_file}")
|
8
|
+
end
|
9
|
+
|
10
|
+
sleep 1 until File.exists?(@socket_file)
|
11
|
+
|
12
|
+
at_exit { Process.kill(:INT, remote_client); File.unlink(@socket_file) }
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.new
|
16
|
+
start!
|
17
|
+
DRb.start_service
|
18
|
+
DRbObject.new_with_uri("drbunix://#{@socket_file}")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/akephalos/server.rb
CHANGED
@@ -1,6 +1,12 @@
|
|
1
1
|
require "pathname"
|
2
2
|
require "drb/drb"
|
3
|
-
require
|
3
|
+
require "akephalos/client"
|
4
|
+
|
5
|
+
class NameError::Message
|
6
|
+
def _dump
|
7
|
+
to_s
|
8
|
+
end
|
9
|
+
end
|
4
10
|
|
5
11
|
[
|
6
12
|
java.net.URL,
|
@@ -17,34 +23,10 @@ require Pathname(__FILE__).expand_path.dirname + "htmlunit"
|
|
17
23
|
com.gargoylesoftware.htmlunit.WebRequestSettings
|
18
24
|
].each { |klass| klass.send(:include, DRbUndumped) }
|
19
25
|
|
20
|
-
class WebClient
|
21
|
-
def page
|
22
|
-
@page = getCurrentWindow.getEnclosedPage
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
class HtmlPage
|
27
|
-
def find(selector)
|
28
|
-
@present_nodes = getByXPath(selector).to_a
|
29
|
-
(@nodes ||= []).push(*@present_nodes)
|
30
|
-
@present_nodes
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
class HtmlSubmitInput
|
35
|
-
def click
|
36
|
-
super
|
37
|
-
rescue => e
|
38
|
-
puts e
|
39
|
-
puts e.backtrace.join("\n")
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
26
|
module Akephalos
|
44
27
|
class Server
|
45
28
|
def self.start!(socket_file)
|
46
|
-
client =
|
47
|
-
client.setCssErrorHandler(com.gargoylesoftware.htmlunit.SilentCssErrorHandler.new)
|
29
|
+
client = Client.new
|
48
30
|
DRb.start_service("drbunix://#{socket_file}", client)
|
49
31
|
DRb.thread.join
|
50
32
|
end
|
metadata
CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
segments:
|
6
6
|
- 0
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
version: 0.0.
|
8
|
+
- 2
|
9
|
+
version: 0.0.2
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Bernerd Schaefer
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-05-
|
17
|
+
date: 2010-05-10 00:00:00 -05:00
|
18
18
|
default_executable: akephalos
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
@@ -68,17 +68,18 @@ extra_rdoc_files: []
|
|
68
68
|
files:
|
69
69
|
- .gitignore
|
70
70
|
- Rakefile
|
71
|
+
- TODO.txt
|
71
72
|
- VERSION
|
72
73
|
- akephalos.gemspec
|
73
74
|
- bin/akephalos
|
74
75
|
- lib/akephalos.rb
|
75
76
|
- lib/akephalos/capybara.rb
|
77
|
+
- lib/akephalos/client.rb
|
76
78
|
- lib/akephalos/cucumber.rb
|
77
79
|
- lib/akephalos/htmlunit.rb
|
78
|
-
- lib/akephalos/
|
79
|
-
- lib/akephalos/
|
80
|
-
- lib/akephalos/
|
81
|
-
- lib/akephalos/htmlunit/web_client.rb
|
80
|
+
- lib/akephalos/node.rb
|
81
|
+
- lib/akephalos/page.rb
|
82
|
+
- lib/akephalos/remote_client.rb
|
82
83
|
- lib/akephalos/server.rb
|
83
84
|
- spec/driver/akephalos_driver_spec.rb
|
84
85
|
- spec/session/akephalos_session_spec.rb
|
@@ -1,28 +0,0 @@
|
|
1
|
-
module Akephalos
|
2
|
-
module Htmlunit
|
3
|
-
module HtmlPage
|
4
|
-
|
5
|
-
def modified_source
|
6
|
-
asXml
|
7
|
-
end
|
8
|
-
|
9
|
-
def source
|
10
|
-
getWebResponse.getContentAsString
|
11
|
-
end
|
12
|
-
|
13
|
-
def current_url
|
14
|
-
getWebResponse.getRequestSettings.getUrl.toString
|
15
|
-
end
|
16
|
-
|
17
|
-
def find(selector)
|
18
|
-
getByXPath(selector)
|
19
|
-
end
|
20
|
-
|
21
|
-
def execute_script(script)
|
22
|
-
executeJavaScript(script).getJavaScriptResult
|
23
|
-
end
|
24
|
-
|
25
|
-
com.gargoylesoftware.htmlunit.html.HtmlPage.send(:include, self)
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
@@ -1,19 +0,0 @@
|
|
1
|
-
module Akephalos
|
2
|
-
module Htmlunit
|
3
|
-
module HtmlSelect
|
4
|
-
def select_option(option)
|
5
|
-
opt = getOptions.detect { |o| o.asText == option }
|
6
|
-
|
7
|
-
opt && opt.setSelected(true)
|
8
|
-
end
|
9
|
-
|
10
|
-
def unselect_option(option)
|
11
|
-
opt = getOptions.detect { |o| o.asText == option }
|
12
|
-
|
13
|
-
opt && opt.setSelected(false)
|
14
|
-
end
|
15
|
-
|
16
|
-
com.gargoylesoftware.htmlunit.html.HtmlSelect.send(:include, self)
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|