poltergeist 1.5.1 → 1.6.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.
- checksums.yaml +5 -13
- data/LICENSE +1 -1
- data/README.md +15 -44
- data/lib/capybara/poltergeist/browser.rb +58 -10
- data/lib/capybara/poltergeist/client.rb +2 -2
- data/lib/capybara/poltergeist/client/agent.coffee +46 -10
- data/lib/capybara/poltergeist/client/browser.coffee +160 -117
- data/lib/capybara/poltergeist/client/compiled/agent.js +60 -10
- data/lib/capybara/poltergeist/client/compiled/browser.js +208 -137
- data/lib/capybara/poltergeist/client/compiled/main.js +40 -2
- data/lib/capybara/poltergeist/client/compiled/node.js +7 -2
- data/lib/capybara/poltergeist/client/compiled/web_page.js +172 -111
- data/lib/capybara/poltergeist/client/main.coffee +10 -1
- data/lib/capybara/poltergeist/client/node.coffee +8 -4
- data/lib/capybara/poltergeist/client/web_page.coffee +139 -94
- data/lib/capybara/poltergeist/driver.rb +36 -2
- data/lib/capybara/poltergeist/errors.rb +8 -2
- data/lib/capybara/poltergeist/inspector.rb +1 -1
- data/lib/capybara/poltergeist/network_traffic/response.rb +1 -1
- data/lib/capybara/poltergeist/node.rb +12 -0
- data/lib/capybara/poltergeist/version.rb +1 -1
- metadata +44 -31
checksums.yaml
CHANGED
@@ -1,15 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
5
|
-
data.tar.gz: !binary |-
|
6
|
-
ZTFjYzAwNGI3YTM3ZGEwN2M0ZTI3MzAxMzNiYTI4MmY2YWQ2NjhiOA==
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 804da55f4a9b382633c1ea2388078a0a464dcf20
|
4
|
+
data.tar.gz: 50f49ce4999e47f266a048ada3a4edb453aef88d
|
7
5
|
SHA512:
|
8
|
-
metadata.gz:
|
9
|
-
|
10
|
-
MWE4ZGVlYTA1ZmI1NWJkMjc3Y2RiZTE1YzEwNjQ0MzhjZGRhOTQ2NGQ5Zjgx
|
11
|
-
N2IxZTc0YWFmNTY5MTE5NDA0YjNjMzAyODE5ZjNhMGM5ZDkzZjk=
|
12
|
-
data.tar.gz: !binary |-
|
13
|
-
MjI0NTg2YzZhMjg0NzE4NzI2MDhhMjQyODQ2MzY4NDI3MWQxNWY0NGQ3YWUx
|
14
|
-
ZGIzNDJjZDQ1ZTJkMzVlN2FjYmY2NjUxMTBkZTI5NTE1NGU2M2E2ZjgwMmVj
|
15
|
-
MWQ0MGQzMzRjZTJjMzA1ZjMxMWM1Mzc0ZjAyNWViOWZlZmY0N2E=
|
6
|
+
metadata.gz: 6129b9fb4c4cd02030a40f4d1afe79a4fb8dbb4244c0bb93b00bfd6587dccb93ece045c089c6731e2538c8abd2ea73ceb18c7aa9fe34a883112a5682493caabe
|
7
|
+
data.tar.gz: 9abd3b5839e1342411eb869c83917fe1a45341278ec663d870274d9f5ca501f897b1b49505367d5f3601cd80eb0e456999b1cfec49f2c96428477f81e7eae8f7
|
data/LICENSE
CHANGED
data/README.md
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
# Poltergeist - A PhantomJS driver for Capybara #
|
2
2
|
|
3
|
-
[](http://travis-ci.org/teampoltergeist/poltergeist)
|
4
4
|
|
5
5
|
Poltergeist is a driver for [Capybara](https://github.com/jnicklas/capybara). It allows you to
|
6
6
|
run your Capybara tests on a headless [WebKit](http://webkit.org) browser,
|
7
7
|
provided by [PhantomJS](http://www.phantomjs.org/).
|
8
8
|
|
9
|
-
**If you're viewing this at https://github.com/
|
9
|
+
**If you're viewing this at https://github.com/teampoltergeist/poltergeist,
|
10
10
|
you're reading the documentation for the master branch.
|
11
11
|
[View documentation for the latest release
|
12
|
-
(1.
|
12
|
+
(1.6.0).](https://github.com/teampoltergeist/poltergeist/tree/v1.6.0)**
|
13
13
|
|
14
14
|
## Getting help ##
|
15
15
|
|
@@ -17,7 +17,7 @@ Questions should be posted [on Stack
|
|
17
17
|
Overflow, using the 'poltergeist' tag](http://stackoverflow.com/questions/tagged/poltergeist).
|
18
18
|
|
19
19
|
Bug reports should be posted [on
|
20
|
-
GitHub](https://github.com/
|
20
|
+
GitHub](https://github.com/teampoltergeist/poltergeist/issues) (and be sure
|
21
21
|
to read the bug reporting guidance below).
|
22
22
|
|
23
23
|
## Installation ##
|
@@ -43,17 +43,17 @@ dependencies* (you don't need Qt, or a running X server, etc.)
|
|
43
43
|
|
44
44
|
* *Homebrew*: `brew install phantomjs`
|
45
45
|
* *MacPorts*: `sudo port install phantomjs`
|
46
|
-
* *Manual install*: [Download this](
|
46
|
+
* *Manual install*: [Download this](https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-1.9.7-macosx.zip)
|
47
47
|
|
48
48
|
### Linux ###
|
49
49
|
|
50
|
-
* Download the [32 bit](https://
|
51
|
-
or [64 bit](https://
|
50
|
+
* Download the [32 bit](https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-1.9.7-linux-i686.tar.bz2)
|
51
|
+
or [64 bit](https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-1.9.7-linux-x86_64.tar.bz2)
|
52
52
|
binary.
|
53
53
|
* Extract the tarball and copy `bin/phantomjs` into your `PATH`
|
54
54
|
|
55
55
|
### Windows ###
|
56
|
-
* Download the [precompiled binary](
|
56
|
+
* Download the [precompiled binary](https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-1.9.7-windows.zip)
|
57
57
|
for Windows
|
58
58
|
|
59
59
|
### Manual compilation ###
|
@@ -61,7 +61,7 @@ for Windows
|
|
61
61
|
Do this as a last resort if the binaries don't work for you. It will
|
62
62
|
take quite a long time as it has to build WebKit.
|
63
63
|
|
64
|
-
* Download [the source tarball](
|
64
|
+
* Download [the source tarball](https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-1.9.7-source.zip)
|
65
65
|
* Extract and cd in
|
66
66
|
* `./build.sh`
|
67
67
|
|
@@ -97,7 +97,6 @@ and the following optional features:
|
|
97
97
|
|
98
98
|
* `page.evaluate_script` and `page.execute_script`
|
99
99
|
* `page.within_frame`
|
100
|
-
* `page.within_window`
|
101
100
|
* `page.status_code`
|
102
101
|
* `page.response_headers`
|
103
102
|
* `page.save_screenshot`
|
@@ -105,6 +104,7 @@ and the following optional features:
|
|
105
104
|
* `page.driver.scroll_to(left, top)`
|
106
105
|
* `page.driver.basic_authorize(user, password)`
|
107
106
|
* `element.native.send_keys(*keys)`
|
107
|
+
* window API
|
108
108
|
* cookie handling
|
109
109
|
* drag-and-drop
|
110
110
|
|
@@ -131,11 +131,6 @@ If you need for some reasons base64 encoded screenshot you can simply call
|
|
131
131
|
same as for `save_screenshot` except the first argument which is format (:png by
|
132
132
|
default, acceptable :png, :gif, :jpeg).
|
133
133
|
|
134
|
-
### Resizing the window ###
|
135
|
-
|
136
|
-
Sometimes the window size is important to how things are rendered. Poltergeist sets the window
|
137
|
-
size to 1024x768 by default, but you can set this yourself with `page.driver.resize(width, height)`.
|
138
|
-
|
139
134
|
### Clicking precise coordinates ###
|
140
135
|
|
141
136
|
Sometimes its desirable to click a very specific area of the screen. You can accomplish this with
|
@@ -216,31 +211,7 @@ The following methods are used to inspect and manipulate cookies:
|
|
216
211
|
`:secure`, `:httponly`, `:expires`. `:expires` should be a `Time`
|
217
212
|
object.
|
218
213
|
* `page.driver.remove_cookie(name)` - remove a cookie
|
219
|
-
|
220
|
-
### Window switching ###
|
221
|
-
|
222
|
-
The following methods can be used to execute commands inside different windows:
|
223
|
-
|
224
|
-
* `page.driver.window_handles` - an array containing the names of all
|
225
|
-
the open windows.
|
226
|
-
|
227
|
-
* `page.within_window(name) { # actions }` - executes
|
228
|
-
the passed block in the context of the named window.
|
229
|
-
|
230
|
-
Example:
|
231
|
-
|
232
|
-
``` ruby
|
233
|
-
find_link("Login with Facebook").trigger("click")
|
234
|
-
|
235
|
-
sleep(0.1)
|
236
|
-
|
237
|
-
fb_popup = page.driver.window_handles.last
|
238
|
-
page.within_window fb_popup do
|
239
|
-
fill_in "email", :with => "facebook_email@email.tst"
|
240
|
-
fill_in "pass", :with => "my_pass"
|
241
|
-
click_button "Log In"
|
242
|
-
end
|
243
|
-
```
|
214
|
+
* `page.driver.clear_cookies` - clear all cookies
|
244
215
|
|
245
216
|
### Sending keys ###
|
246
217
|
|
@@ -288,12 +259,12 @@ end
|
|
288
259
|
* `:js_errors` (Boolean) - When false, Javascript errors do not get re-raised in Ruby.
|
289
260
|
* `:window_size` (Array) - The dimensions of the browser window in which to test, expressed
|
290
261
|
as a 2-element array, e.g. [1024, 768]. Default: [1024, 768]
|
291
|
-
* `:phantomjs_options` (Array) - Additional [command line options](
|
262
|
+
* `:phantomjs_options` (Array) - Additional [command line options](http://phantomjs.org/api/command-line.html)
|
292
263
|
to be passed to PhantomJS, e.g. `['--load-images=no', '--ignore-ssl-errors=yes']`
|
293
264
|
* `:extensions` (Array) - An array of JS files to be preloaded into
|
294
265
|
the phantomjs browser. Useful for faking unsupported APIs.
|
295
266
|
* `:port` (Fixnum) - The port which should be used to communicate
|
296
|
-
with the PhantomJS process.
|
267
|
+
with the PhantomJS process. Defaults to a random open port.
|
297
268
|
|
298
269
|
## Troubleshooting ##
|
299
270
|
|
@@ -309,7 +280,7 @@ occur sporadically and are not easily reproduced.
|
|
309
280
|
|
310
281
|
If your crash happens every time, you should read the [PhantomJS crash
|
311
282
|
reporting
|
312
|
-
guide](
|
283
|
+
guide](http://phantomjs.org/crash-reporting.html) and file
|
313
284
|
a bug against PhantomJS. Feel free to also file a bug against
|
314
285
|
Poltergeist in case there are workarounds that can be implemented within
|
315
286
|
Poltergeist. Also, if lots of Poltergeist users are experiencing the
|
@@ -412,7 +383,7 @@ the [changelog](CHANGELOG.md).
|
|
412
383
|
|
413
384
|
## License ##
|
414
385
|
|
415
|
-
Copyright (c) 2011 Jonathan Leighton
|
386
|
+
Copyright (c) 2011-2014 Jonathan Leighton
|
416
387
|
|
417
388
|
Permission is hereby granted, free of charge, to any person obtaining
|
418
389
|
a copy of this software and associated documentation files (the
|
@@ -5,9 +5,11 @@ require 'time'
|
|
5
5
|
module Capybara::Poltergeist
|
6
6
|
class Browser
|
7
7
|
ERROR_MAPPINGS = {
|
8
|
-
|
9
|
-
|
10
|
-
|
8
|
+
'Poltergeist.JavascriptError' => JavascriptError,
|
9
|
+
'Poltergeist.FrameNotFound' => FrameNotFound,
|
10
|
+
'Poltergeist.InvalidSelector' => InvalidSelector,
|
11
|
+
'Poltergeist.StatusFailError' => StatusFailError,
|
12
|
+
'Poltergeist.NoSuchWindowError' => NoSuchWindowError
|
11
13
|
}
|
12
14
|
|
13
15
|
attr_reader :server, :client, :logger
|
@@ -49,6 +51,10 @@ module Capybara::Poltergeist
|
|
49
51
|
command 'title'
|
50
52
|
end
|
51
53
|
|
54
|
+
def parents(page_id, id)
|
55
|
+
command 'parents', page_id, id
|
56
|
+
end
|
57
|
+
|
52
58
|
def find(method, selector)
|
53
59
|
result = command('find', method, selector)
|
54
60
|
result['ids'].map { |id| [result['page_id'], id] }
|
@@ -70,6 +76,10 @@ module Capybara::Poltergeist
|
|
70
76
|
command 'delete_text', page_id, id
|
71
77
|
end
|
72
78
|
|
79
|
+
def attributes(page_id, id)
|
80
|
+
command 'attributes', page_id, id
|
81
|
+
end
|
82
|
+
|
73
83
|
def attribute(page_id, id, name)
|
74
84
|
command 'attribute', page_id, id, name.to_s
|
75
85
|
end
|
@@ -122,21 +132,45 @@ module Capybara::Poltergeist
|
|
122
132
|
command 'pop_frame'
|
123
133
|
end
|
124
134
|
|
135
|
+
def window_handle
|
136
|
+
command 'window_handle'
|
137
|
+
end
|
138
|
+
|
125
139
|
def window_handles
|
126
|
-
command '
|
140
|
+
command 'window_handles'
|
141
|
+
end
|
142
|
+
|
143
|
+
def switch_to_window(handle)
|
144
|
+
command 'switch_to_window', handle
|
145
|
+
end
|
146
|
+
|
147
|
+
def open_new_window
|
148
|
+
command 'open_new_window'
|
149
|
+
end
|
150
|
+
|
151
|
+
def close_window(handle)
|
152
|
+
command 'close_window', handle
|
127
153
|
end
|
128
154
|
|
129
155
|
def within_window(name, &block)
|
130
|
-
|
156
|
+
original = window_handle
|
157
|
+
handle = command 'window_handle', name
|
158
|
+
handle = name if handle.nil? && window_handles.include?(name)
|
159
|
+
raise NoSuchWindowError unless handle
|
160
|
+
switch_to_window(handle)
|
131
161
|
yield
|
132
162
|
ensure
|
133
|
-
|
163
|
+
switch_to_window(original)
|
134
164
|
end
|
135
165
|
|
136
166
|
def click(page_id, id)
|
137
167
|
command 'click', page_id, id
|
138
168
|
end
|
139
169
|
|
170
|
+
def right_click(page_id, id)
|
171
|
+
command 'right_click', page_id, id
|
172
|
+
end
|
173
|
+
|
140
174
|
def double_click(page_id, id)
|
141
175
|
command 'double_click', page_id, id
|
142
176
|
end
|
@@ -175,6 +209,10 @@ module Capybara::Poltergeist
|
|
175
209
|
command 'render_base64', format.to_s, !!options[:full], options[:selector]
|
176
210
|
end
|
177
211
|
|
212
|
+
def set_zoom_factor(zoom_factor)
|
213
|
+
command 'set_zoom_factor', zoom_factor
|
214
|
+
end
|
215
|
+
|
178
216
|
def set_paper_size(size)
|
179
217
|
command 'set_paper_size', size
|
180
218
|
end
|
@@ -240,6 +278,10 @@ module Capybara::Poltergeist
|
|
240
278
|
command 'remove_cookie', name
|
241
279
|
end
|
242
280
|
|
281
|
+
def clear_cookies
|
282
|
+
command 'clear_cookies'
|
283
|
+
end
|
284
|
+
|
243
285
|
def cookies_enabled=(flag)
|
244
286
|
command 'cookies_enabled', !!flag
|
245
287
|
end
|
@@ -258,17 +300,23 @@ module Capybara::Poltergeist
|
|
258
300
|
end
|
259
301
|
end
|
260
302
|
|
303
|
+
def url_blacklist=(blacklist)
|
304
|
+
command 'set_url_blacklist', *blacklist
|
305
|
+
end
|
306
|
+
|
261
307
|
def debug=(val)
|
262
308
|
@debug = val
|
263
309
|
command 'set_debug', !!val
|
264
310
|
end
|
265
311
|
|
266
312
|
def command(name, *args)
|
267
|
-
message = { 'name' => name, 'args' => args }
|
268
|
-
log message
|
313
|
+
message = JSON.dump({ 'name' => name, 'args' => args })
|
314
|
+
log message
|
315
|
+
|
316
|
+
response = server.send(message)
|
317
|
+
log response
|
269
318
|
|
270
|
-
json = JSON.load(
|
271
|
-
log json.inspect
|
319
|
+
json = JSON.load(response)
|
272
320
|
|
273
321
|
if json['error']
|
274
322
|
klass = ERROR_MAPPINGS[json['error']['name']] || BrowserError
|
@@ -5,7 +5,7 @@ require 'cliver'
|
|
5
5
|
module Capybara::Poltergeist
|
6
6
|
class Client
|
7
7
|
PHANTOMJS_SCRIPT = File.expand_path('../client/compiled/main.js', __FILE__)
|
8
|
-
PHANTOMJS_VERSION = ['
|
8
|
+
PHANTOMJS_VERSION = ['>= 1.8.1', '< 3.0']
|
9
9
|
PHANTOMJS_NAME = 'phantomjs'
|
10
10
|
|
11
11
|
KILL_TIMEOUT = 2 # seconds
|
@@ -99,13 +99,13 @@ module Capybara::Poltergeist
|
|
99
99
|
# it works with JRuby but I've experienced strange mistakes on Rubinius.
|
100
100
|
def redirect_stdout
|
101
101
|
prev = STDOUT.dup
|
102
|
-
prev.autoclose = false
|
103
102
|
$stdout = @write_io
|
104
103
|
STDOUT.reopen(@write_io)
|
105
104
|
yield
|
106
105
|
ensure
|
107
106
|
STDOUT.reopen(prev)
|
108
107
|
$stdout = STDOUT
|
108
|
+
prev.close
|
109
109
|
end
|
110
110
|
|
111
111
|
def kill_phantomjs
|
@@ -25,7 +25,7 @@ class PoltergeistAgent
|
|
25
25
|
throw error
|
26
26
|
|
27
27
|
currentUrl: ->
|
28
|
-
encodeURI(window.location.href)
|
28
|
+
encodeURI(decodeURI(window.location.href))
|
29
29
|
|
30
30
|
find: (method, selector, within = document) ->
|
31
31
|
try
|
@@ -48,8 +48,8 @@ class PoltergeistAgent
|
|
48
48
|
@elements.length - 1
|
49
49
|
|
50
50
|
documentSize: ->
|
51
|
-
height: document.documentElement.scrollHeight,
|
52
|
-
width: document.documentElement.scrollWidth
|
51
|
+
height: document.documentElement.scrollHeight || document.documentElement.clientHeight,
|
52
|
+
width: document.documentElement.scrollWidth || document.documentElement.clientWidth
|
53
53
|
|
54
54
|
get: (id) ->
|
55
55
|
@nodes[id] or= new PoltergeistAgent.Node(this, @elements[id])
|
@@ -65,6 +65,9 @@ class PoltergeistAgent
|
|
65
65
|
afterUpload: (id) ->
|
66
66
|
this.get(id).removeAttribute('_poltergeist_selected')
|
67
67
|
|
68
|
+
clearLocalStorage: ->
|
69
|
+
localStorage.clear()
|
70
|
+
|
68
71
|
class PoltergeistAgent.ObsoleteNode
|
69
72
|
toString: -> "PoltergeistAgent.ObsoleteNode"
|
70
73
|
|
@@ -75,7 +78,8 @@ class PoltergeistAgent.Node
|
|
75
78
|
@EVENTS = {
|
76
79
|
FOCUS: ['blur', 'focus', 'focusin', 'focusout'],
|
77
80
|
MOUSE: ['click', 'dblclick', 'mousedown', 'mouseenter', 'mouseleave', 'mousemove',
|
78
|
-
'mouseover', 'mouseout', 'mouseup']
|
81
|
+
'mouseover', 'mouseout', 'mouseup', 'contextmenu'],
|
82
|
+
FORM: ['submit']
|
79
83
|
}
|
80
84
|
|
81
85
|
constructor: (@agent, @element) ->
|
@@ -83,6 +87,14 @@ class PoltergeistAgent.Node
|
|
83
87
|
parentId: ->
|
84
88
|
@agent.register(@element.parentNode)
|
85
89
|
|
90
|
+
parentIds: ->
|
91
|
+
ids = []
|
92
|
+
parent = @element.parentNode
|
93
|
+
while parent != document
|
94
|
+
ids.push @agent.register(parent)
|
95
|
+
parent = parent.parentNode
|
96
|
+
ids
|
97
|
+
|
86
98
|
find: (method, selector) ->
|
87
99
|
@agent.find(method, selector, @element)
|
88
100
|
|
@@ -136,17 +148,25 @@ class PoltergeistAgent.Node
|
|
136
148
|
@element.textContent
|
137
149
|
|
138
150
|
visibleText: ->
|
139
|
-
if
|
140
|
-
@element.
|
141
|
-
|
142
|
-
|
151
|
+
if this.isVisible()
|
152
|
+
if @element.nodeName == "TEXTAREA"
|
153
|
+
@element.textContent
|
154
|
+
else
|
155
|
+
@element.innerText
|
143
156
|
|
144
157
|
deleteText: ->
|
145
158
|
range = document.createRange()
|
146
159
|
range.selectNodeContents(@element)
|
160
|
+
window.getSelection().removeAllRanges()
|
147
161
|
window.getSelection().addRange(range)
|
148
162
|
window.getSelection().deleteFromDocument()
|
149
163
|
|
164
|
+
getAttributes: ->
|
165
|
+
attrs = {}
|
166
|
+
for attr, i in @element.attributes
|
167
|
+
attrs[attr.name] = attr.value.replace("\n","\\n");
|
168
|
+
attrs
|
169
|
+
|
150
170
|
getAttribute: (name) ->
|
151
171
|
if name == 'checked' || name == 'selected'
|
152
172
|
@element[name]
|
@@ -219,6 +239,16 @@ class PoltergeistAgent.Node
|
|
219
239
|
isDisabled: ->
|
220
240
|
@element.disabled || @element.tagName == 'OPTION' && @element.parentNode.disabled
|
221
241
|
|
242
|
+
containsSelection: ->
|
243
|
+
selectedNode = document.getSelection().focusNode
|
244
|
+
|
245
|
+
return false if !selectedNode
|
246
|
+
|
247
|
+
if selectedNode.nodeType == 3
|
248
|
+
selectedNode = selectedNode.parentNode
|
249
|
+
|
250
|
+
@element.contains(selectedNode)
|
251
|
+
|
222
252
|
frameOffset: ->
|
223
253
|
win = window
|
224
254
|
offset = { top: 0, left: 0 }
|
@@ -257,13 +287,19 @@ class PoltergeistAgent.Node
|
|
257
287
|
false, false, false, false, 0, null
|
258
288
|
)
|
259
289
|
else if Node.EVENTS.FOCUS.indexOf(name) != -1
|
260
|
-
event =
|
261
|
-
|
290
|
+
event = this.obtainEvent(name)
|
291
|
+
else if Node.EVENTS.FORM.indexOf(name) != -1
|
292
|
+
event = this.obtainEvent(name)
|
262
293
|
else
|
263
294
|
throw "Unknown event"
|
264
295
|
|
265
296
|
@element.dispatchEvent(event)
|
266
297
|
|
298
|
+
obtainEvent: (name) ->
|
299
|
+
event = document.createEvent('HTMLEvents')
|
300
|
+
event.initEvent(name, true, true)
|
301
|
+
event
|
302
|
+
|
267
303
|
mouseEventTest: (x, y) ->
|
268
304
|
frameOffset = this.frameOffset()
|
269
305
|
|