terminus 0.3.0 → 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/README.rdoc +25 -19
- data/bin/terminus +1 -1
- data/lib/capybara/driver/terminus.rb +28 -17
- data/lib/terminus.rb +29 -15
- data/lib/terminus/application.rb +5 -11
- data/lib/terminus/browser.rb +97 -44
- data/lib/terminus/client.rb +43 -0
- data/lib/terminus/client/browser.rb +30 -0
- data/lib/terminus/client/phantom.js +6 -0
- data/lib/terminus/client/phantomjs.rb +20 -0
- data/lib/terminus/connector.rb +9 -0
- data/lib/terminus/connector/server.rb +142 -0
- data/lib/terminus/connector/socket_handler.rb +72 -0
- data/lib/terminus/controller.rb +22 -5
- data/lib/terminus/host.rb +7 -2
- data/lib/terminus/node.rb +11 -5
- data/lib/terminus/proxy.rb +35 -1
- data/lib/terminus/proxy/driver_body.rb +26 -6
- data/lib/terminus/proxy/external.rb +9 -0
- data/lib/terminus/proxy/rewrite.rb +4 -2
- data/lib/terminus/public/compiled/terminus-min.js +3 -0
- data/lib/terminus/public/compiled/terminus.js +5270 -0
- data/lib/terminus/public/loader.js +1 -1
- data/lib/terminus/public/pathology.js +3174 -0
- data/lib/terminus/public/syn/browsers.js +2 -2
- data/lib/terminus/public/syn/drag/drag.js +3 -4
- data/lib/terminus/public/syn/key.js +130 -111
- data/lib/terminus/public/syn/mouse.js +2 -2
- data/lib/terminus/public/syn/synthetic.js +45 -34
- data/lib/terminus/public/terminus.js +183 -70
- data/lib/terminus/server.rb +6 -0
- data/lib/terminus/timeouts.rb +4 -2
- data/lib/terminus/views/bootstrap.erb +12 -27
- data/lib/terminus/views/index.erb +1 -1
- data/spec/reports/android.txt +875 -0
- data/spec/reports/chrome.txt +137 -8
- data/spec/reports/firefox.txt +137 -9
- data/spec/reports/opera.txt +142 -13
- data/spec/reports/phantomjs.txt +871 -0
- data/spec/reports/safari.txt +137 -8
- data/spec/spec_helper.rb +19 -17
- data/spec/terminus_driver_spec.rb +8 -6
- data/spec/terminus_session_spec.rb +4 -4
- metadata +209 -117
data/README.rdoc
CHANGED
@@ -1,35 +1,41 @@
|
|
1
1
|
= Terminus
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
automated:
|
9
|
-
|
3
|
+
{Terminus}[http://terminus.jcoglan.com] is a {Capybara}[https://github.com/jnicklas/capybara]
|
4
|
+
driver for real browsers. It lets you control your application in any browser on
|
5
|
+
any device (including {PhantomJS}[http://phantomjs.org/]), without needing
|
6
|
+
browser plugins. This allows several types of testing to be automated:
|
7
|
+
|
10
8
|
* Cross-browser testing
|
9
|
+
* Headless testing
|
11
10
|
* Multi-browser interaction e.g. messaging apps
|
12
|
-
* Testing on remote machines, phones, iPads etc
|
11
|
+
* Testing on remote machines, phones, iPads etc
|
12
|
+
|
13
|
+
|
14
|
+
== Running the example
|
15
|
+
|
16
|
+
Install the dependencies and boot the Terminus server, then open
|
17
|
+
{http://localhost:70004/}[http://localhost:70004/] in your browser.
|
13
18
|
|
19
|
+
bundle install
|
20
|
+
bundle exec bin/terminus
|
14
21
|
|
15
|
-
|
22
|
+
With your browser open, start an IRB session and begin controlling the app:
|
16
23
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
when running the Capybara specs.
|
24
|
+
$ irb -r ./example/app
|
25
|
+
>> extend Capybara::DSL
|
26
|
+
>> visit '/'
|
27
|
+
>> click_link 'Sign up!'
|
28
|
+
>> fill_in 'Username', :with => 'jcoglan'
|
29
|
+
>> fill_in 'Password', :with => 'hello'
|
30
|
+
>> choose 'Web scale'
|
31
|
+
>> click_button 'Go!'
|
26
32
|
|
27
33
|
|
28
34
|
== License
|
29
35
|
|
30
36
|
(The MIT License)
|
31
37
|
|
32
|
-
Copyright (c) 2010-
|
38
|
+
Copyright (c) 2010-2012 James Coglan
|
33
39
|
|
34
40
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
35
41
|
this software and associated documentation files (the 'Software'), to deal in
|
data/bin/terminus
CHANGED
@@ -1,24 +1,35 @@
|
|
1
1
|
class Capybara::Driver::Terminus < Capybara::Driver::Base
|
2
2
|
attr_reader :options
|
3
3
|
|
4
|
+
NULL_APP = lambda do |env|
|
5
|
+
[200, {'Content-Type' => 'text/html'}, ['']]
|
6
|
+
end
|
7
|
+
|
4
8
|
def initialize(app = nil, options = {})
|
5
|
-
|
6
|
-
|
7
|
-
@app = Terminus::Proxy[app]
|
9
|
+
@app = Terminus::Proxy[app || NULL_APP]
|
8
10
|
@options = options
|
9
11
|
@rack_server = Capybara::Server.new(@app)
|
10
12
|
|
11
13
|
@rack_server.boot
|
14
|
+
sleep(0.1) until Terminus.server_running?(@rack_server)
|
12
15
|
end
|
13
16
|
|
14
17
|
def find(xpath)
|
15
18
|
browser.find(xpath, self)
|
16
19
|
end
|
17
20
|
|
21
|
+
def invalid_element_errors
|
22
|
+
[::Terminus::ObsoleteElementError]
|
23
|
+
end
|
24
|
+
|
18
25
|
def visit(path)
|
19
26
|
browser.visit(@rack_server.url(path))
|
20
27
|
end
|
21
28
|
|
29
|
+
def wait?
|
30
|
+
true
|
31
|
+
end
|
32
|
+
|
22
33
|
extend Forwardable
|
23
34
|
def_delegators :browser, :body,
|
24
35
|
:current_url,
|
@@ -35,18 +46,7 @@ class Capybara::Driver::Terminus < Capybara::Driver::Base
|
|
35
46
|
yield
|
36
47
|
Terminus.browser = current_browser
|
37
48
|
end
|
38
|
-
|
39
|
-
def within_frame(name)
|
40
|
-
frame_src = browser.frame_src(name)
|
41
|
-
frame = browser.frames.find do |frame|
|
42
|
-
frame.current_url == frame_src or
|
43
|
-
frame.current_path == frame_src
|
44
|
-
end
|
45
|
-
current_browser = browser
|
46
|
-
Terminus.browser = frame
|
47
|
-
yield
|
48
|
-
Terminus.browser = current_browser
|
49
|
-
end
|
49
|
+
alias :within_frame :within_window
|
50
50
|
|
51
51
|
private
|
52
52
|
|
@@ -56,11 +56,22 @@ private
|
|
56
56
|
end
|
57
57
|
end
|
58
58
|
|
59
|
-
# Capybara 0.3.9 looks in the Capybara::Driver namespace for
|
60
|
-
#
|
59
|
+
# Capybara 0.3.9 looks in the Capybara::Driver namespace for appropriate
|
60
|
+
# drivers. 0.4 uses this registration API instead.
|
61
61
|
if Capybara.respond_to?(:register_driver)
|
62
62
|
Capybara.register_driver :terminus do |app|
|
63
63
|
Capybara::Driver::Terminus.new(app)
|
64
64
|
end
|
65
65
|
end
|
66
66
|
|
67
|
+
# We use WEBrick to boot the test app, because if we use Thin (the default) the
|
68
|
+
# slow response used to test Ajax resynchronization blocks the event loop. This
|
69
|
+
# stops Terminus receiving messages and causes false positives: the client is
|
70
|
+
# not really waiting for Ajax to complete, it's just having its messages blocked
|
71
|
+
# because EventMachine is frozen.
|
72
|
+
|
73
|
+
Capybara.server do |app, port|
|
74
|
+
handler = Rack::Handler.get('webrick')
|
75
|
+
handler.run(app, :Port => port, :AccessLog => [], :Logger => WEBrick::Log::new(nil, 0))
|
76
|
+
end
|
77
|
+
|
data/lib/terminus.rb
CHANGED
@@ -1,17 +1,20 @@
|
|
1
|
+
require 'erb'
|
1
2
|
require 'forwardable'
|
3
|
+
require 'http/parser'
|
4
|
+
require 'net/http'
|
5
|
+
require 'rbconfig'
|
6
|
+
require 'socket'
|
7
|
+
require 'stringio'
|
2
8
|
require 'uri'
|
3
|
-
|
4
|
-
require '
|
9
|
+
|
10
|
+
require 'capybara'
|
11
|
+
require 'childprocess'
|
12
|
+
require 'cookiejar'
|
5
13
|
require 'eventmachine'
|
6
14
|
require 'faye'
|
7
|
-
require 'sinatra'
|
8
|
-
require 'packr'
|
9
|
-
require 'capybara'
|
10
15
|
require 'rack-proxy'
|
11
16
|
require 'useragent'
|
12
17
|
|
13
|
-
Thin::Logging.silent = true
|
14
|
-
|
15
18
|
module Terminus
|
16
19
|
FAYE_MOUNT = '/messaging'
|
17
20
|
DEFAULT_HOST = 'localhost'
|
@@ -19,9 +22,11 @@ module Terminus
|
|
19
22
|
LOCALHOST = /^(localhost|0\.0\.0\.0|127\.0\.0\.1)$/
|
20
23
|
RETRY_LIMIT = 3
|
21
24
|
|
22
|
-
ROOT = File.expand_path(
|
25
|
+
ROOT = File.expand_path('..', __FILE__)
|
23
26
|
autoload :Application, ROOT + '/terminus/application'
|
24
27
|
autoload :Browser, ROOT + '/terminus/browser'
|
28
|
+
autoload :Client, ROOT + '/terminus/client'
|
29
|
+
autoload :Connector, ROOT + '/terminus/connector'
|
25
30
|
autoload :Controller, ROOT + '/terminus/controller'
|
26
31
|
autoload :Host, ROOT + '/terminus/host'
|
27
32
|
autoload :Node, ROOT + '/terminus/node'
|
@@ -31,24 +36,23 @@ module Terminus
|
|
31
36
|
|
32
37
|
require ROOT + '/capybara/driver/terminus'
|
33
38
|
|
39
|
+
class ObsoleteElementError < StandardError
|
40
|
+
end
|
41
|
+
|
34
42
|
class << self
|
35
|
-
attr_accessor :debug
|
43
|
+
attr_accessor :debug, :sockets
|
36
44
|
|
37
45
|
def create(options = {})
|
38
46
|
Server.new(options)
|
39
47
|
end
|
40
48
|
|
41
|
-
def driver_script(host = DEFAULT_HOST)
|
42
|
-
Application.driver_script(host)
|
43
|
-
end
|
44
|
-
|
45
49
|
def endpoint(host = DEFAULT_HOST)
|
46
50
|
"http://#{host}:#{port}#{FAYE_MOUNT}"
|
47
51
|
end
|
48
52
|
|
49
53
|
def ensure_reactor_running
|
50
54
|
Thread.new { EM.run unless EM.reactor_running? }
|
51
|
-
|
55
|
+
Thread.pass until EM.reactor_running?
|
52
56
|
end
|
53
57
|
|
54
58
|
def port
|
@@ -59,14 +63,24 @@ module Terminus
|
|
59
63
|
@port = port.to_i
|
60
64
|
end
|
61
65
|
|
66
|
+
def start_browser(options = {})
|
67
|
+
Client::Browser.start(options)
|
68
|
+
end
|
69
|
+
|
70
|
+
def start_phantomjs(options = {})
|
71
|
+
Client::PhantomJS.start(options)
|
72
|
+
end
|
73
|
+
|
62
74
|
extend Forwardable
|
63
75
|
def_delegators :controller, :browser,
|
64
76
|
:browsers,
|
65
77
|
:browser=,
|
78
|
+
:cookies,
|
66
79
|
:ensure_browsers,
|
67
80
|
:return_to_dock,
|
68
81
|
:rewrite_local,
|
69
|
-
:rewrite_remote
|
82
|
+
:rewrite_remote,
|
83
|
+
:server_running?
|
70
84
|
|
71
85
|
private
|
72
86
|
|
data/lib/terminus/application.rb
CHANGED
@@ -1,24 +1,18 @@
|
|
1
|
+
require 'packr'
|
2
|
+
require 'sinatra'
|
3
|
+
|
1
4
|
module Terminus
|
2
5
|
class Application < Sinatra::Base
|
3
6
|
|
4
|
-
ROOT = File.expand_path(
|
7
|
+
ROOT = File.expand_path('../..', __FILE__)
|
5
8
|
|
6
9
|
set :static, true
|
7
|
-
set :
|
8
|
-
set :views, ROOT + '/terminus/views'
|
9
|
-
|
10
|
-
def self.driver_script(host)
|
11
|
-
%Q{<script type="text/javascript" src="http://#{host}:#{Terminus.port}/bootstrap.js"></script>}
|
12
|
-
end
|
10
|
+
set :root, ROOT + '/terminus'
|
13
11
|
|
14
12
|
helpers do
|
15
13
|
def bootstrap
|
16
14
|
Packr.pack(erb(:bootstrap), :shrink_vars => true)
|
17
15
|
end
|
18
|
-
|
19
|
-
def host
|
20
|
-
"http://#{ env['HTTP_HOST'] }"
|
21
|
-
end
|
22
16
|
end
|
23
17
|
|
24
18
|
get '/' do
|
data/lib/terminus/browser.rb
CHANGED
@@ -2,14 +2,15 @@ module Terminus
|
|
2
2
|
class Browser
|
3
3
|
|
4
4
|
include Timeouts
|
5
|
+
attr_reader :connector
|
6
|
+
attr_writer :sockets
|
5
7
|
|
6
8
|
extend Forwardable
|
7
|
-
def_delegator :@user_agent, :browser, :name
|
8
9
|
def_delegators :@user_agent, :os, :version
|
9
10
|
|
10
|
-
def initialize(controller)
|
11
|
+
def initialize(controller, id)
|
11
12
|
@controller = controller
|
12
|
-
@attributes = {}
|
13
|
+
@attributes = {'id' => id}
|
13
14
|
@docked = false
|
14
15
|
@frames = Set.new
|
15
16
|
@namespace = Faye::Namespace.new
|
@@ -25,20 +26,32 @@ module Terminus
|
|
25
26
|
return false unless @user_agent
|
26
27
|
|
27
28
|
params.all? do |name, value|
|
28
|
-
property = __send__(name)
|
29
|
-
|
30
|
-
when Regexp then property =~ value
|
31
|
-
when String then property == value
|
32
|
-
end
|
29
|
+
property = __send__(name)
|
30
|
+
value === property
|
33
31
|
end
|
34
32
|
end
|
35
33
|
|
36
34
|
def ask(command, retries = RETRY_LIMIT)
|
37
|
-
id
|
38
|
-
|
39
|
-
|
35
|
+
debug(:ask, id, command)
|
36
|
+
value = if @connector
|
37
|
+
message = Yajl::Encoder.encode('commandId' => '_', 'command' => command)
|
38
|
+
response = @connector.request(message)
|
39
|
+
if response.nil?
|
40
|
+
retries == false ? false : ask(command)
|
41
|
+
else
|
42
|
+
result_hash = Yajl::Parser.parse(response)
|
43
|
+
result_hash['value']
|
44
|
+
end
|
45
|
+
else
|
46
|
+
command_id = tell(command)
|
47
|
+
result_hash = wait_with_timeout(:result) { result(command_id) }
|
48
|
+
result_hash[:value]
|
49
|
+
end
|
50
|
+
debug(:val, id, command, value)
|
51
|
+
raise ObsoleteElementError if value.nil?
|
52
|
+
value
|
40
53
|
rescue Timeouts::TimeoutError => e
|
41
|
-
raise e if retries
|
54
|
+
raise e if retries == 1
|
42
55
|
ask(command, retries - 1)
|
43
56
|
end
|
44
57
|
|
@@ -47,12 +60,18 @@ module Terminus
|
|
47
60
|
end
|
48
61
|
|
49
62
|
def current_path
|
50
|
-
|
51
|
-
URI.parse(url).path
|
63
|
+
URI.parse(current_url).path
|
52
64
|
end
|
53
65
|
|
54
66
|
def current_url
|
55
|
-
@attributes['url']
|
67
|
+
url = @attributes['url']
|
68
|
+
return '' unless url
|
69
|
+
return url unless @connector
|
70
|
+
rewrite_local(ask([:current_url]))
|
71
|
+
end
|
72
|
+
|
73
|
+
def debug(*args)
|
74
|
+
p args if Terminus.debug
|
56
75
|
end
|
57
76
|
|
58
77
|
def docked?
|
@@ -64,7 +83,7 @@ module Terminus
|
|
64
83
|
end
|
65
84
|
|
66
85
|
def execute_script(expression)
|
67
|
-
tell([:execute, expression])
|
86
|
+
@connector ? ask([:execute, expression]) : tell([:execute, expression])
|
68
87
|
nil
|
69
88
|
end
|
70
89
|
|
@@ -80,25 +99,32 @@ module Terminus
|
|
80
99
|
@frames.to_a
|
81
100
|
end
|
82
101
|
|
83
|
-
def frame_src(name)
|
84
|
-
ask([:frame_src, name])
|
85
|
-
end
|
86
|
-
|
87
102
|
def id
|
88
103
|
@attributes['id']
|
89
104
|
end
|
90
105
|
|
106
|
+
def infinite_redirect?
|
107
|
+
return @infinite_redirect unless @connector
|
108
|
+
evaluate_script('!!window.TERMINUS_INFINITE_REDIRECT')
|
109
|
+
end
|
110
|
+
|
111
|
+
def name
|
112
|
+
return 'PhantomJS' if @user_agent.to_str =~ /\bPhantomJS\b/
|
113
|
+
@user_agent.browser
|
114
|
+
end
|
115
|
+
|
91
116
|
def page_id
|
92
117
|
@attributes['page']
|
93
118
|
end
|
94
119
|
|
95
120
|
def ping!(message)
|
96
|
-
|
121
|
+
debug(:ping, id)
|
122
|
+
debug(:recv, message)
|
123
|
+
|
97
124
|
remove_timeout(:dead)
|
98
125
|
add_timeout(:dead, Timeouts::TIMEOUT) { drop_dead! }
|
99
126
|
|
100
|
-
|
101
|
-
message['url'] = uri.to_s
|
127
|
+
message['url'] = rewrite_local(message['url'])
|
102
128
|
|
103
129
|
@attributes = @attributes.merge(message)
|
104
130
|
@user_agent = UserAgent.parse(message['ua'])
|
@@ -106,18 +132,20 @@ module Terminus
|
|
106
132
|
|
107
133
|
@infinite_redirect = message['infinite']
|
108
134
|
|
109
|
-
if
|
110
|
-
@parent = Terminus.browser(
|
135
|
+
if id =~ /\//
|
136
|
+
@parent = Terminus.browser(id.gsub(/\/[^\/]+$/, ''))
|
111
137
|
@parent.frame!(self) unless @parent == self
|
112
138
|
end
|
113
139
|
|
140
|
+
start_connector if message['sockets'] and sockets?
|
141
|
+
|
114
142
|
@ping = true
|
115
143
|
end
|
116
144
|
|
117
145
|
def reset!
|
118
146
|
if url = @attributes['url']
|
119
147
|
uri = URI.parse(url)
|
120
|
-
visit("http://#{uri.host}:#{uri.port}")
|
148
|
+
visit("http://#{uri.host}:#{uri.port}/")
|
121
149
|
end
|
122
150
|
ask([:clear_cookies])
|
123
151
|
@attributes.delete('url')
|
@@ -128,17 +156,22 @@ module Terminus
|
|
128
156
|
end
|
129
157
|
|
130
158
|
def result!(message)
|
131
|
-
|
132
|
-
@results[message['commandId']] = message['result']
|
159
|
+
debug(:result, id, message['commandId'], message['result'])
|
160
|
+
@results[message['commandId']] = {:value => message['result']}
|
133
161
|
end
|
134
162
|
|
135
163
|
def result(id)
|
136
164
|
return nil unless @results.has_key?(id)
|
137
|
-
|
165
|
+
@results.delete(id)
|
138
166
|
end
|
139
167
|
|
140
168
|
def return_to_dock
|
141
|
-
|
169
|
+
return unless @dock_host
|
170
|
+
visit("http://#{@dock_host}:#{Terminus.port}/")
|
171
|
+
end
|
172
|
+
|
173
|
+
def sockets?
|
174
|
+
@sockets.nil? ? Terminus.sockets != false : @sockets
|
142
175
|
end
|
143
176
|
|
144
177
|
def source
|
@@ -150,20 +183,27 @@ module Terminus
|
|
150
183
|
end
|
151
184
|
|
152
185
|
def tell(command)
|
153
|
-
|
154
|
-
|
155
|
-
messenger.publish(
|
156
|
-
|
186
|
+
command_id = @namespace.generate
|
187
|
+
debug(:tell, id, command, command_id)
|
188
|
+
messenger.publish(command_channel, 'command' => command, 'commandId' => command_id)
|
189
|
+
command_id
|
157
190
|
end
|
158
191
|
|
159
192
|
def visit(url, retries = RETRY_LIMIT)
|
160
193
|
close_frames!
|
161
194
|
uri = @controller.rewrite_remote(url, @dock_host)
|
162
195
|
uri.host = @dock_host if uri.host =~ LOCALHOST
|
163
|
-
tell([:visit, uri.to_s])
|
164
|
-
wait_for_ping
|
165
196
|
|
166
|
-
if @
|
197
|
+
if @connector
|
198
|
+
ask([:visit, uri.to_s], false)
|
199
|
+
@connector.drain_socket
|
200
|
+
@attributes['url'] = rewrite_local(uri)
|
201
|
+
else
|
202
|
+
tell([:visit, uri.to_s])
|
203
|
+
wait_for_ping
|
204
|
+
end
|
205
|
+
|
206
|
+
if infinite_redirect?
|
167
207
|
@infinite_redirect = nil
|
168
208
|
raise Capybara::InfiniteRedirectError
|
169
209
|
end
|
@@ -188,16 +228,21 @@ module Terminus
|
|
188
228
|
def drop_dead!
|
189
229
|
remove_timeout(:dead)
|
190
230
|
close_frames!
|
231
|
+
@connector.close if @connector
|
191
232
|
@dead = true
|
192
233
|
@controller.drop_browser(self)
|
193
234
|
end
|
194
235
|
|
195
236
|
private
|
196
237
|
|
197
|
-
def
|
238
|
+
def command_channel
|
198
239
|
"/terminus/clients/#{id}"
|
199
240
|
end
|
200
241
|
|
242
|
+
def socket_channel
|
243
|
+
"/terminus/sockets/#{id}"
|
244
|
+
end
|
245
|
+
|
201
246
|
def close_frames!
|
202
247
|
@frames.each { |frame| frame.drop_dead! }
|
203
248
|
@frames = Set.new
|
@@ -205,12 +250,20 @@ module Terminus
|
|
205
250
|
|
206
251
|
def detect_dock_host
|
207
252
|
uri = URI.parse(@attributes['url'])
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
253
|
+
@docked = (uri.port == Terminus.port)
|
254
|
+
@dock_host = @attributes['host']
|
255
|
+
end
|
256
|
+
|
257
|
+
def rewrite_local(url)
|
258
|
+
@controller.rewrite_local(url.to_s, @dock_host).to_s
|
259
|
+
end
|
260
|
+
|
261
|
+
def start_connector
|
262
|
+
return if @connector or @dock_host.nil? or Terminus.browser != self
|
263
|
+
@connector = Connector::Server.new(self)
|
264
|
+
url = "ws://#{@dock_host}:#{@connector.port}/"
|
265
|
+
debug(:connect, id, url)
|
266
|
+
messenger.publish(socket_channel, 'url' => url)
|
214
267
|
end
|
215
268
|
|
216
269
|
def messenger
|