This repository has been archived by the owner on Dec 16, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 520
/
browser.coffee
787 lines (697 loc) · 27.4 KB
/
browser.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
jsdom = require("jsdom")
html = jsdom.dom.level3.html
vm = process.binding("evals")
require "./jsdom_patches"
require "./forms"
require "./xpath"
History = require("./history").History
EventLoop = require("./eventloop").EventLoop
require.paths.push "../../build/default"
WindowContext = require("../../build/default/windowcontext").WindowContext
# Use the browser to open up new windows and load documents.
#
# The browser maintains state for cookies and localStorage.
class Browser extends require("events").EventEmitter
constructor: (options) ->
cache = require("./cache").use(this)
cookies = require("./cookies").use(this)
storage = require("./storage").use(this)
interact = require("./interact").use(this)
xhr = require("./xhr").use(cache)
resources = require("./resources")
# Options
# -------
@OPTIONS = ["debug", "runScripts", "userAgent"]
# ### debug
#
# True to have Zombie report what it's doing.
@debug = false
# ### runScripts
#
# Run scripts included in or loaded from the page. Defaults to true.
@runScripts = true
# ### userAgent
#
# User agent string sent to server.
@userAgent = "Mozilla/5.0 Chrome/10.0.613.0 Safari/534.15 Zombie.js/#{exports.version}"
# ### withOptions(options, fn)
#
# Changes the browser options, and calls the function with a
# callback (reset). When you're done processing, call the reset
# function to bring options back to their previous values.
#
# See `visit` if you want to see this method in action.
@withOptions = (options, fn)->
if options
restore = {}
[restore[k], @[k]] = [@[k], v] for k,v of options
fn =>
@[k] = v for k,v of restore if restore
# Sets the browser options.
if options
for k,v of options
if @OPTIONS.indexOf(k) >= 0
@[k] = v
else
throw "I don't recognize the option #{k}"
# ### browser.fork() => Browser
#
# Return a new browser with a snapshot of this browser's state.
# Any changes to the forked browser's state do not affect this browser.
this.fork = ->
forked = new Browser()
forked.loadCookies this.saveCookies()
forked.loadStorage this.saveStorage()
forked.loadHistory this.saveHistory()
return forked
# Windows
# -------
window = null
# ### browser.open() => Window
#
# Open new browser window. Takes a single argument that determines
# which features are supported by this Window. At the moment all
# features are undocumented, use at your own peril.
this.open = (features = {})->
features.interactive ?= true
history = features.history || new History
newWindow = jsdom.createWindow(html)
# Add context for evaluating scripts.
newWindow._evalContext = new WindowContext(newWindow)
newWindow._evaluate = (code, filename)-> newWindow._evalContext.evaluate(code, filename)
# Switch to the newly created window if it's interactive.
# Examples of non-interactive windows are frames.
window = newWindow if features.interactive
newWindow.parent = newWindow
newWindow.__defineGetter__ "browser", => this
newWindow.__defineGetter__ "title", => @window?.document?.title
newWindow.__defineSetter__ "title", (title)=> @window?.document?.title = title
newWindow.navigator.userAgent = @userAgent
resources.extend newWindow
cookies.extend newWindow
storage.extend newWindow
newWindow._eventloop = new EventLoop(newWindow)
history.extend newWindow
interact.extend newWindow
xhr.extend newWindow
newWindow.screen = new Screen()
newWindow.JSON = JSON
# Default onerror handler.
newWindow.onerror = (event)=> @emit "error", event.error || new Error("Error loading script")
# TODO: Fix
newWindow.Image = ->
return newWindow
# Events
# ------
# ### browser.wait(callback?)
# ### browser.wait(terminator, callback)
#
# Process all events from the queue. This method returns immediately, events
# are processed in the background. When all events are exhausted, it calls
# the callback with `null, browser`; if any event fails, it calls the
# callback with the exception.
#
# With one argument, that argument is the callback. With two arguments, the
# first argument is a terminator and the last argument is the callback. The
# terminator is one of:
#
# * null -- process all events
# * number -- process that number of events
# * function -- called after each event, stop processing when function
# returns false
#
# You can call this method with no arguments and simply listen to the `done`
# and `error` events.
#
# Events include timeout, interval and XHR `onreadystatechange`. DOM events
# are handled synchronously.
this.wait = (terminate, callback)->
if !callback
[callback, terminate] = [terminate, null]
if callback
onerror = (error)=>
@removeListener "error", onerror
@removeListener "done", ondone
callback error
ondone = (error)=>
@removeListener "error", onerror
@removeListener "done", ondone
callback null, this
@on "error", onerror
@on "done", ondone
window._eventloop.wait window, terminate
return
# ### browser.fire(name, target, callback?)
#
# Fire a DOM event. You can use this to simulate a DOM event, e.g. clicking a
# link. These events will bubble up and can be cancelled. With a callback, this
# method will call `wait`.
#
# * name -- Even name (e.g `click`)
# * target -- Target element (e.g a link)
# * callback -- Wait for events to be processed, then call me (optional)
this.fire = (name, target, options, callback)->
[callback, options] = [options, null] if typeof(options) == 'function'
options ?= {}
klass = options.klass || if (name in mouseEventNames) then "MouseEvents" else "HTMLEvents"
bubbles = options.bubbles ? true
cancelable = options.cancelable ? true
event = window.document.createEvent(klass)
event.initEvent(name, bubbles, cancelable)
if options.attributes?
for key, value of options.attributes
event[key] = value
target.dispatchEvent event
@wait callback if callback
mouseEventNames = ['mousedown', 'mousemove', 'mouseup']
# ### browser.clock => Number
#
# The current system time according to the browser (see also
# `browser.clock`).
#
# You can change this to advance the system clock during tests. It will
# also advance when handling timeout/interval events.
@clock = Date.now()
# ### browser.now => Date
#
# The current system time according to the browser (see also
# `browser.clock`).
@__defineGetter__ "now", -> new Date(@clock)
# Accessors
# ---------
# ### browser.querySelector(selector) => Element
#
# Select a single element (first match) and return it.
#
# * selector -- CSS selector
#
# Returns an Element or null
this.querySelector = (selector)->
window.document?.querySelector(selector)
# ### browser.querySelectorAll(selector) => NodeList
#
# Select multiple elements and return a static node list.
#
# * selector -- CSS selector
#
# Returns a NodeList or null
this.querySelectorAll = (selector)-> window.document?.querySelectorAll(selector)
# ### browser.text(selector, context?) => String
#
# Returns the text contents of the selected elements.
#
# * selector -- CSS selector (if missing, entire document)
# * context -- Context element (if missing, uses document)
#
# Returns a string
this.text = (selector, context)->
return "" unless @document.documentElement
@css(selector, context).map((e)-> e.textContent).join("")
# ### browser.html(selector?, context?) => String
#
# Returns the HTML contents of the selected elements.
#
# * selector -- CSS selector (if missing, entire document)
# * context -- Context element (if missing, uses document)
#
# Returns a string
this.html = (selector, context)->
return "" unless @document.documentElement
@css(selector, context).map((e)-> e.outerHTML.trim()).join("")
# ### browser.css(selector, context?) => NodeList
#
# Evaluates the CSS selector against the document (or context node) and
# return a node list. Shortcut for `document.querySelectorAll`.
this.css = (selector, context)->
if selector then (context || @document).querySelectorAll(selector).toArray() else [@document]
# ### browser.xpath(expression, context?) => XPathResult
#
# Evaluates the XPath expression against the document (or context node) and
# return the XPath result. Shortcut for `document.evaluate`.
this.xpath = (expression, context)->
@document.evaluate(expression, context || @document)
# ### browser.window => Window
#
# Returns the main window.
@__defineGetter__ "window", -> window
# ### browser.document => Document
#
# Returns the main window's document. Only valid after opening a document
# (see `browser.open`).
@__defineGetter__ "document", -> window?.document
# ### browser.body => Element
#
# Returns the body Element of the current document.
@__defineGetter__ "body", -> window.document?.querySelector("body")
# ### browser.statusCode => Number
#
# Returns the status code of the request for loading the window.
@__defineGetter__ "statusCode", ->
@window.resources.first?.response.statusCode
# ### browser.redirected => Boolean
#
# Returns true if the request for loading the window followed a
# redirect.
@__defineGetter__ "redirected", ->
@window.resources.first?.response.redirected
# ### source => String
#
# Returns the unmodified source of the document loaded by the browser
@__defineGetter__ "source", => @window.resources.first?.response.body
# ### browser.cache => Cache
#
# Returns the browser's cache.
@__defineGetter__ "cache", -> cache
# Navigation
# ----------
# ### browser.visit(url, callback?)
# ### browser.visit(url, options, callback)
#
# Loads document from the specified URL, processes events and calls the
# callback. If the second argument are options, uses these options
# for the duration of the request and resets the options afterwards.
#
# If it fails to download, calls the callback with the error.
this.visit = (url, options, callback)->
if typeof options is "function"
[callback, options] = [options, null]
@withOptions options, (reset)=>
window.history._assign url
@wait (error, browser)->
reset()
if callback && error
callback error
else if callback
callback null, browser, browser.statusCode
return
# ### browser.location => Location
#
# Return the location of the current document (same as `window.location`).
@__defineGetter__ "location", -> window.location
# ### browser.location = url
#
# Changes document location, loads new document if necessary (same as
# setting `window.location`).
@__defineSetter__ "location", (url)-> window.location = url
# ### browser.link(selector) : Element
#
# Finds and returns a link by its text content or selector.
this.link = (selector)->
if link = @querySelector(selector)
return link if link.tagName == "A"
for link in @querySelectorAll("body a")
if link.textContent.trim() == selector
return link
return
# ### browser.clickLink(selector, callback)
#
# Clicks on a link. Clicking on a link can trigger other events, load new
# page, etc: use a callback to be notified of completion. Finds link by
# text content or selector.
#
# * selector -- CSS selector or link text
# * callback -- Called with two arguments: error and browser
this.clickLink = (selector, callback)->
if link = @link(selector)
@fire "click", link, =>
callback null, this, this.statusCode
else
callback new Error("No link matching '#{selector}'")
# ### browser.saveHistory() => String
#
# Save history to a text string. You can use this to load the data
# later on using `browser.loadHistory`.
this.saveHistory = -> window.history.save()
# ### browser.loadHistory(String)
#
# Load history from a text string (e.g. previously created using
# `browser.saveHistory`.
this.loadHistory = (serialized)-> window.history.load serialized
# Forms
# -----
# ### browser.field(selector) : Element
#
# Find and return an input field (`INPUT`, `TEXTAREA` or `SELECT`) based on
# a CSS selector, field name (its `name` attribute) or the text value of a
# label associated with that field (case sensitive, but ignores
# leading/trailing spaces).
this.field = (selector)->
# If the field has already been queried, return itself
if selector instanceof html.Element
return selector
# Try more specific selector first.
if field = @querySelector(selector)
return field if field.tagName == "INPUT" || field.tagName == "TEXTAREA" || field.tagName == "SELECT"
# Use field name (case sensitive).
if field = @querySelector(":input[name='#{selector}']")
return field
# Try finding field from label.
for label in @querySelectorAll("label")
if label.textContent.trim() == selector
# Label can either reference field or enclose it
if for_attr = label.getAttribute("for")
return @document.getElementById(for_attr)
else
return label.querySelector(":input")
return
# HTML5 field types that you can "fill in".
TEXT_TYPES = "email number password range search text url".split(" ")
# ### browser.fill(selector, value) => this
#
# Fill in a field: input field or text area.
#
# * selector -- CSS selector, field name or text of the field label
# * value -- Field value
#
# Returns this
this.fill = (selector, value)->
field = @field(selector)
if field && (field.tagName == "TEXTAREA" || (field.tagName == "INPUT")) # && TEXT_TYPES.indexOf(field.type) >= 0))
throw new Error("This INPUT field is disabled") if field.getAttribute("input")
throw new Error("This INPUT field is readonly") if field.getAttribute("readonly")
field.value = value
@fire "change", field
return this
throw new Error("No INPUT matching '#{selector}'")
setCheckbox = (selector, value)=>
field = @field(selector)
if field && field.tagName == "INPUT" && field.type == "checkbox"
throw new Error("This INPUT field is disabled") if field.getAttribute("input")
throw new Error("This INPUT field is readonly") if field.getAttribute("readonly")
if(field.checked ^ value)
@fire "click", field
return this
else
throw new Error("No checkbox INPUT matching '#{selector}'")
# ### browser.check(selector) => this
#
# Checks a checkbox.
#
# * selector -- CSS selector, field name or text of the field label
#
# Returns this
this.check = (selector)-> setCheckbox(selector, true)
# ### browser.uncheck(selector) => this
#
# Unchecks a checkbox.
#
# * selector -- CSS selector, field name or text of the field label
#
# Returns this
this.uncheck = (selector)-> setCheckbox(selector, false)
# ### browser.choose(selector) => this
#
# Selects a radio box option.
#
# * selector -- CSS selector, field value or text of the field label
#
# Returns this
this.choose = (selector)->
field = @field(selector)
if field.tagName == "INPUT" && field.type == "radio" && field.form
if(!field.checked)
radios = @querySelectorAll(":radio[name='#{field.getAttribute("name")}']", field.form)
for radio in radios
radio.checked = false unless radio.getAttribute("disabled") || radio.getAttribute("readonly")
field.checked = true
@fire "change", field
@fire "click", field
return this
throw new Error("No radio INPUT matching '#{selector}'")
findOption = (selector, value)=>
field = @field(selector)
if field && field.tagName == "SELECT"
throw new Error("This SELECT field is disabled") if field.getAttribute("disabled")
throw new Error("This SELECT field is readonly") if field.getAttribute("readonly")
for option in field.options
if option.value == value
return option
for option in field.options
if option.label == value
return option
throw new Error("No OPTION '#{value}'")
else
throw new Error("No SELECT matching '#{selector}'")
# ### browser.attach(selector, filename) => this
#
# Attaches a file to the specified input field. The second argument is the
# file name.
this.attach = (selector, filename)->
field = @field(selector)
if field && field.tagName == "INPUT" && field.type == "file"
field.value = filename
@fire "change", field
return this
else
throw new Error("No file INPUT matching '#{selector}'")
# ### browser.select(selector, value) => this
#
# Selects an option.
#
# * selector -- CSS selector, field name or text of the field label
# * value -- Value (or label) or option to select
#
# Returns this
this.select = (selector, value)->
option = findOption(selector, value)
@selectOption(option)
return this
# ### browser.selectOption(option) => this
#
# Selects an option.
#
# * option -- option to select
#
# Returns this
this.selectOption = (option)->
if(option && !option.selected)
select = @xpath("./ancestor::select", option).value[0]
@fire "beforeactivate", select
option.selected = true
@fire "beforedeactivate", select
@fire "change", select
return this
# ### browser.unselect(selector, value) => this
#
# Unselects an option.
#
# * selector -- CSS selector, field name or text of the field label
# * value -- Value (or label) or option to unselect
#
# Returns this
this.unselect = (selector, value)->
option = findOption(selector, value)
@unselectOption(option)
return this
# ### browser.unselectOption(option) => this
#
# Unselects an option.
#
# * option -- option to unselect
#
# Returns this
this.unselectOption = (option)->
if(option && option.selected)
select = @xpath("./ancestor::select", option).value[0]
throw new Error("Cannot unselect in single select") unless select.multiple
option.removeAttribute('selected')
@fire "change", select
return this
# ### browser.button(selector) : Element
#
# Finds a button using CSS selector, button name or button text (`BUTTON` or
# `INPUT` element).
#
# * selector -- CSS selector, button name or text of BUTTON element
this.button = (selector)->
if button = @querySelector(selector)
return button if button.tagName == "BUTTON" || button.tagName == "INPUT"
for button in @querySelectorAll("form button")
return button if button.textContent.trim() == selector
inputs = @querySelectorAll("form :submit, form :reset, form :button")
for input in inputs
return input if input.name == selector
for input in inputs
return input if input.value == selector
return
# ### browser.pressButton(selector, callback)
#
# Press a button (button element or input of type `submit`). Typically
# this will submit the form. Use the callback to wait for the from
# submission, page to load and all events run their course.
#
# * selector -- CSS selector, button name or text of BUTTON element
# * callback -- Called with two arguments: error and browser
this.pressButton = (selector, callback)->
if button = @button(selector)
if button.getAttribute("disabled")
callback new Error("This button is disabled")
else
@fire "click", button, =>
callback null, this, this.statusCode
else
callback new Error("No BUTTON '#{selector}'")
# Cookies and storage
# -------------------
# ### browser.cookies(domain, path) => Cookies
#
# Returns all the cookies for this domain/path. Path defaults to "/".
this.cookies = (domain, path)-> cookies.access(domain, path)
# ### browser.saveCookies() => String
#
# Save cookies to a text string. You can use this to load them back
# later on using `browser.loadCookies`.
this.saveCookies = -> cookies.save()
# ### browser.loadCookies(String)
#
# Load cookies from a text string (e.g. previously created using
# `browser.saveCookies`.
this.loadCookies = (serialized)-> cookies.load serialized
# ### brower.localStorage(host) => Storage
#
# Returns local Storage based on the document origin (hostname/port). This
# is the same storage area you can access from any document of that origin.
this.localStorage = (host)-> storage.local(host)
# ### brower.sessionStorage(host) => Storage
#
# Returns session Storage based on the document origin (hostname/port). This
# is the same storage area you can access from any document of that origin.
this.sessionStorage = (host)-> storage.session(host)
# ### browser.saveStorage() => String
#
# Save local/session storage to a text string. You can use this to
# load the data later on using `browser.loadStorage`.
this.saveStorage = -> storage.save()
# ### browser.loadStorage(String)
#
# Load local/session stroage from a text string (e.g. previously
# created using `browser.saveStorage`.
this.loadStorage = (serialized)-> storage.load serialized
# Scripts
# -------
# ### browser.evaluate(function) : Object
# ### browser.evaluate(code, filename) : Object
#
# Evaluates a JavaScript expression in the context of the current window
# and returns the result. When evaluating external script, also include
# filename.
#
# You can also use this to evaluate a function in the context of the
# window: for timers and asynchronous callbacks (e.g. XHR).
this.evaluate = (code, filename)->
this.window._evaluate code, filename
# Interaction
# -----------
# ### browser.onalert(fn)
#
# Called by `window.alert` with the message.
this.onalert = (fn)-> interact.onalert fn
# ### browser.onconfirm(question, response)
# ### browser.onconfirm(fn)
#
# The first form specifies a canned response to return when
# `window.confirm` is called with that question. The second form
# will call the function with the question and use the respone of
# the first function to return a value (true or false).
#
# The response to the question can be true or false, so all canned
# responses are converted to either value. If no response
# available, returns false.
this.onconfirm = (question, response)-> interact.onconfirm question, response
# ### browser.onprompt(message, response)
# ### browser.onprompt(fn)
#
# The first form specifies a canned response to return when
# `window.prompt` is called with that message. The second form will
# call the function with the message and default value and use the
# response of the first function to return a value or false.
#
# The response to a prompt can be any value (converted to a string),
# false to indicate the user cancelled the prompt (returning null),
# or nothing to have the prompt return the default value or an empty
# string.
this.onprompt = (message, response)-> interact.onprompt message, response
# ### browser.prompted(message) => boolean
#
# Returns true if user was prompted with that message
# (`window.alert`, `window.confirm` or `window.prompt`)
this.prompted = (message)-> interact.prompted(message)
# Debugging
# ---------
# ### browser.viewInBrowser(name?)
#
# Views the current document in a real Web browser. Uses the default
# system browser on OS X, BSD and Linux. Probably errors on Windows.
this.viewInBrowser = (browser)->
require("./bcat").bcat @html()
# ### browser.lastRequest => HTTPRequest
#
# Returns the last request sent by this browser. The object will have the
# properties url, method, headers, and body.
@__defineGetter__ "lastRequest", -> @window.resources.last?.request
# ### browser.lastResponse => HTTPResponse
#
# Returns the last response received by this browser. The object will have the
# properties url, status, headers and body. Long bodies may be truncated.
@__defineGetter__ "lastResponse", -> @window.resources.last?.response
# ### browser.lastError => Object
#
# Returns the last error received by this browser in lieu of response.
@__defineGetter__ "lastError", -> @window.resources.last?.error
# Zombie can spit out messages to help you figure out what's going
# on as your code executes.
#
# To spit a message to the console when running in debug mode, call
# this method with one or more values (same as `console.log`). You
# can also call it with a function that will be evaluated only when
# running in debug mode.
#
# For example:
# browser.log("Opening page:", url);
# browser.log(function() { return "Opening page: " + url });
this.log = ->
if @debug
values = ["Zombie:"]
if typeof arguments[0] == "function"
try
values.push arguments[0]()
catch ex
values.push ex
else
values.push arg for arg in arguments
console.log.apply null, values
# Dump information to the consolt: Zombie version, current URL,
# history, cookies, event loop, etc. Useful for debugging and
# submitting error reports.
this.dump = ->
indent = (lines)-> lines.map((l) -> " #{l}\n").join("")
console.log "Zombie: #{exports.version}\n"
console.log "URL: #{@window.location.href}"
console.log "History:\n#{indent window.history.dump()}"
console.log "Cookies:\n#{indent cookies.dump()}"
console.log "Storage:\n#{indent storage.dump()}"
console.log "Eventloop:\n#{indent window._eventloop.dump()}"
if @document
html = @document.outerHTML
html = html.slice(0, 497) + "..." if html.length > 497
console.log "Document:\n#{indent html.split("\n")}"
else
console.log "No document" unless @document
class Screen
constructor: ->
@width = 1280
@height = 800
@left = 0
@top = 0
@__defineGetter__ "availLeft", -> 0
@__defineGetter__ "availTop", -> 0
@__defineGetter__ "availWidth", -> @width
@__defineGetter__ "availHeight", -> @height
@__defineGetter__ "colorDepth", -> 24
@__defineGetter__ "pixelDepth", -> 24
# Always start with an open window.
@open()
exports.Browser = Browser
# ### zombie.version : String
exports.package = JSON.parse(require("fs").readFileSync(__dirname + "/../../package.json"))
exports.version = exports.package.version