poltergeist 1.5.1 → 1.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
-
[![Build Status](https://secure.travis-ci.org/
|
3
|
+
[![Build Status](https://secure.travis-ci.org/teampoltergeist/poltergeist.png)](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
|
|