1
0
Fork 0
mirror of https://github.com/teampoltergeist/poltergeist.git synced 2022-11-09 12:05:00 -05:00

Propagate Javascript errors on the page to Ruby. Fixes #27.

This commit is contained in:
Jon Leighton 2012-02-29 20:38:01 +00:00
parent 47a310dbdd
commit 0b9d286e3f
10 changed files with 181 additions and 71 deletions

View file

@ -196,6 +196,9 @@ makes debugging easier). Running `rake autocompile` will watch the
sessions, you might want to use this to reap the child phantomjs
process. [Issue #24]
* Errors produced by Javascript on the page will now generate an
exception within Ruby. [Issue #27]
#### Bug fixes ####
* Fix bug where we could end up interacting with an obsolete element. [Issue #30]

View file

@ -113,7 +113,11 @@ module Capybara::Poltergeist
log json.inspect
if json['error']
raise BrowserError.new(json['error'])
if json['error']['name'] == 'Poltergeist.JavascriptError'
raise JavascriptError.new(json['error'])
else
raise BrowserError.new(json['error'])
end
else
json['response']
end

View file

@ -13,37 +13,46 @@ class Poltergeist.Browser
@page.onLoadFinished = (status) =>
if @state == 'loading'
@owner.sendResponse(status)
this.sendResponse(status)
@state = 'default'
sendResponse: (response) ->
errors = @page.errors()
if errors.length > 0
@page.clearErrors()
@owner.sendError(new Poltergeist.JavascriptError(errors))
else
@owner.sendResponse(response)
visit: (url) ->
@state = 'loading'
@page.open(url)
current_url: ->
@owner.sendResponse @page.currentUrl()
this.sendResponse @page.currentUrl()
body: ->
@owner.sendResponse @page.content()
this.sendResponse @page.content()
source: ->
@owner.sendResponse @page.source()
this.sendResponse @page.source()
find: (selector, id) ->
@owner.sendResponse @page.find(selector, id)
this.sendResponse @page.find(selector, id)
text: (id) ->
@owner.sendResponse @page.get(id).text()
this.sendResponse @page.get(id).text()
attribute: (id, name) ->
@owner.sendResponse @page.get(id).getAttribute(name)
this.sendResponse @page.get(id).getAttribute(name)
value: (id) ->
@owner.sendResponse @page.get(id).value()
this.sendResponse @page.get(id).value()
set: (id, value) ->
@page.get(id).set(value)
@owner.sendResponse(true)
this.sendResponse(true)
# PhantomJS only allows us to reference the element by CSS selector, not XPath,
# so we have to add an attribute to the element to identify it, then remove it
@ -65,31 +74,31 @@ class Poltergeist.Browser
element.removeAttribute('_poltergeist_selected')
element.setAttribute('multiple', 'multiple') if multiple
@owner.sendResponse(true)
this.sendResponse(true)
select: (id, value) ->
@owner.sendResponse @page.get(id).select(value)
this.sendResponse @page.get(id).select(value)
tag_name: (id) ->
@owner.sendResponse @page.get(id).tagName()
this.sendResponse @page.get(id).tagName()
visible: (id) ->
@owner.sendResponse @page.get(id).isVisible()
this.sendResponse @page.get(id).isVisible()
evaluate: (script) ->
@owner.sendResponse JSON.parse(@page.evaluate("function() { return JSON.stringify(#{script}) }"))
this.sendResponse JSON.parse(@page.evaluate("function() { return JSON.stringify(#{script}) }"))
execute: (script) ->
@page.execute("function() { #{script} }")
@owner.sendResponse(true)
this.sendResponse(true)
push_frame: (id) ->
@page.pushFrame(id)
@owner.sendResponse(true)
this.sendResponse(true)
pop_frame: ->
@page.popFrame()
@owner.sendResponse(true)
this.sendResponse(true)
click: (id) ->
# If the click event triggers onLoadStarted, we will transition to the 'loading'
@ -104,22 +113,22 @@ class Poltergeist.Browser
=>
if @state == 'clicked'
@state = 'default'
@owner.sendResponse(true)
this.sendResponse(true)
,
10
)
drag: (id, other_id) ->
@page.get(id).dragTo(@page.get(other_id))
@owner.sendResponse(true)
this.sendResponse(true)
trigger: (id, event) ->
@page.get(id).trigger(event)
@owner.sendResponse(event)
this.sendResponse(event)
reset: ->
this.resetPage()
@owner.sendResponse(true)
this.sendResponse(true)
render: (path, full) ->
dimensions = @page.validatedDimensions()
@ -135,11 +144,11 @@ class Poltergeist.Browser
@page.setClipRect(left: 0, top: 0, width: viewport.width, height: viewport.height)
@page.render(path)
@owner.sendResponse(true)
this.sendResponse(true)
resize: (width, height) ->
@page.setViewportSize(width: width, height: height)
@owner.sendResponse(true)
this.sendResponse(true)
exit: ->
phantom.exit()

View file

@ -17,39 +17,49 @@ Poltergeist.Browser = (function() {
}, this);
return this.page.onLoadFinished = __bind(function(status) {
if (this.state === 'loading') {
this.owner.sendResponse(status);
this.sendResponse(status);
return this.state = 'default';
}
}, this);
};
Browser.prototype.sendResponse = function(response) {
var errors;
errors = this.page.errors();
if (errors.length > 0) {
this.page.clearErrors();
return this.owner.sendError(new Poltergeist.JavascriptError(errors));
} else {
return this.owner.sendResponse(response);
}
};
Browser.prototype.visit = function(url) {
this.state = 'loading';
return this.page.open(url);
};
Browser.prototype.current_url = function() {
return this.owner.sendResponse(this.page.currentUrl());
return this.sendResponse(this.page.currentUrl());
};
Browser.prototype.body = function() {
return this.owner.sendResponse(this.page.content());
return this.sendResponse(this.page.content());
};
Browser.prototype.source = function() {
return this.owner.sendResponse(this.page.source());
return this.sendResponse(this.page.source());
};
Browser.prototype.find = function(selector, id) {
return this.owner.sendResponse(this.page.find(selector, id));
return this.sendResponse(this.page.find(selector, id));
};
Browser.prototype.text = function(id) {
return this.owner.sendResponse(this.page.get(id).text());
return this.sendResponse(this.page.get(id).text());
};
Browser.prototype.attribute = function(id, name) {
return this.owner.sendResponse(this.page.get(id).getAttribute(name));
return this.sendResponse(this.page.get(id).getAttribute(name));
};
Browser.prototype.value = function(id) {
return this.owner.sendResponse(this.page.get(id).value());
return this.sendResponse(this.page.get(id).value());
};
Browser.prototype.set = function(id, value) {
this.page.get(id).set(value);
return this.owner.sendResponse(true);
return this.sendResponse(true);
};
Browser.prototype.select_file = function(id, value) {
var element, multiple;
@ -64,31 +74,31 @@ Poltergeist.Browser = (function() {
if (multiple) {
element.setAttribute('multiple', 'multiple');
}
return this.owner.sendResponse(true);
return this.sendResponse(true);
};
Browser.prototype.select = function(id, value) {
return this.owner.sendResponse(this.page.get(id).select(value));
return this.sendResponse(this.page.get(id).select(value));
};
Browser.prototype.tag_name = function(id) {
return this.owner.sendResponse(this.page.get(id).tagName());
return this.sendResponse(this.page.get(id).tagName());
};
Browser.prototype.visible = function(id) {
return this.owner.sendResponse(this.page.get(id).isVisible());
return this.sendResponse(this.page.get(id).isVisible());
};
Browser.prototype.evaluate = function(script) {
return this.owner.sendResponse(JSON.parse(this.page.evaluate("function() { return JSON.stringify(" + script + ") }")));
return this.sendResponse(JSON.parse(this.page.evaluate("function() { return JSON.stringify(" + script + ") }")));
};
Browser.prototype.execute = function(script) {
this.page.execute("function() { " + script + " }");
return this.owner.sendResponse(true);
return this.sendResponse(true);
};
Browser.prototype.push_frame = function(id) {
this.page.pushFrame(id);
return this.owner.sendResponse(true);
return this.sendResponse(true);
};
Browser.prototype.pop_frame = function() {
this.page.popFrame();
return this.owner.sendResponse(true);
return this.sendResponse(true);
};
Browser.prototype.click = function(id) {
this.state = 'clicked';
@ -96,21 +106,21 @@ Poltergeist.Browser = (function() {
return setTimeout(__bind(function() {
if (this.state === 'clicked') {
this.state = 'default';
return this.owner.sendResponse(true);
return this.sendResponse(true);
}
}, this), 10);
};
Browser.prototype.drag = function(id, other_id) {
this.page.get(id).dragTo(this.page.get(other_id));
return this.owner.sendResponse(true);
return this.sendResponse(true);
};
Browser.prototype.trigger = function(id, event) {
this.page.get(id).trigger(event);
return this.owner.sendResponse(event);
return this.sendResponse(event);
};
Browser.prototype.reset = function() {
this.resetPage();
return this.owner.sendResponse(true);
return this.sendResponse(true);
};
Browser.prototype.render = function(path, full) {
var dimensions, document, viewport;
@ -142,14 +152,14 @@ Poltergeist.Browser = (function() {
});
this.page.render(path);
}
return this.owner.sendResponse(true);
return this.sendResponse(true);
};
Browser.prototype.resize = function(width, height) {
this.page.setViewportSize({
width: width,
height: height
});
return this.owner.sendResponse(true);
return this.sendResponse(true);
};
Browser.prototype.exit = function() {
return phantom.exit();

View file

@ -8,12 +8,7 @@ Poltergeist = (function() {
try {
return this.browser[command.name].apply(this.browser, command.args);
} catch (error) {
return this.connection.send({
error: {
name: error.name && error.name() || 'Generic',
args: error.args && error.args() || [error.toString()]
}
});
return this.sendError(error);
}
};
Poltergeist.prototype.sendResponse = function(response) {
@ -21,6 +16,14 @@ Poltergeist = (function() {
response: response
});
};
Poltergeist.prototype.sendError = function(error) {
return this.connection.send({
error: {
name: error.name && error.name() || 'Generic',
args: error.args && error.args() || [error.toString()]
}
});
};
return Poltergeist;
})();
window.Poltergeist = Poltergeist;
@ -47,6 +50,18 @@ Poltergeist.ClickFailed = (function() {
};
return ClickFailed;
})();
Poltergeist.JavascriptError = (function() {
function JavascriptError(errors) {
this.errors = errors;
}
JavascriptError.prototype.name = function() {
return "Poltergeist.JavascriptError";
};
JavascriptError.prototype.args = function() {
return [this.errors];
};
return JavascriptError;
})();
phantom.injectJs('web_page.js');
phantom.injectJs('node.js');
phantom.injectJs('connection.js');

View file

@ -9,6 +9,7 @@ Poltergeist.WebPage = (function() {
this["native"] = require('webpage').create();
this.nodes = {};
this._source = "";
this._errors = [];
this.setViewportSize({
width: 1024,
height: 768
@ -66,8 +67,12 @@ Poltergeist.WebPage = (function() {
WebPage.prototype.onLoadFinishedNative = function() {
return this._source || (this._source = this["native"].content);
};
WebPage.prototype.onConsoleMessage = function(message) {
return console.log(message);
WebPage.prototype.onConsoleMessage = function(message, line, file) {
if (line === 0 && file === "undefined") {
return this._errors.push(message);
} else {
return console.log(message);
}
};
WebPage.prototype.content = function() {
return this["native"].content;
@ -75,6 +80,12 @@ Poltergeist.WebPage = (function() {
WebPage.prototype.source = function() {
return this._source;
};
WebPage.prototype.errors = function() {
return this._errors;
};
WebPage.prototype.clearErrors = function() {
return this._errors = [];
};
WebPage.prototype.viewportSize = function() {
return this["native"].viewportSize;
};

View file

@ -7,15 +7,18 @@ class Poltergeist
try
@browser[command.name].apply(@browser, command.args)
catch error
@connection.send(
error:
name: error.name && error.name() || 'Generic',
args: error.args && error.args() || [error.toString()]
)
this.sendError(error)
sendResponse: (response) ->
@connection.send(response: response)
sendError: (error) ->
@connection.send(
error:
name: error.name && error.name() || 'Generic',
args: error.args && error.args() || [error.toString()]
)
# This is necessary because the remote debugger will wrap the
# script in a function, causing the Poltergeist variable to
# become local.
@ -26,13 +29,15 @@ class Poltergeist.ObsoleteNode
args: -> []
class Poltergeist.ClickFailed
constructor: (selector, position) ->
@selector = selector
@position = position
constructor: (@selector, @position) ->
name: -> "Poltergeist.ClickFailed"
args: -> [@selector, @position]
class Poltergeist.JavascriptError
constructor: (@errors) ->
name: -> "Poltergeist.JavascriptError"
args: -> [@errors]
phantom.injectJs('web_page.js')
phantom.injectJs('node.js')
phantom.injectJs('connection.js')

View file

@ -1,13 +1,16 @@
class Poltergeist.WebPage
@CALLBACKS = ['onAlert', 'onConsoleMessage', 'onLoadFinished', 'onInitialized',
'onLoadStarted', 'onResourceRequested', 'onResourceReceived']
@DELEGATES = ['open', 'sendEvent', 'uploadFile', 'release', 'render']
@COMMANDS = ['currentUrl', 'find', 'nodeCall', 'pushFrame', 'popFrame', 'documentSize']
constructor: ->
@native = require('webpage').create()
@nodes = {}
@_source = ""
@_errors = []
this.setViewportSize(width: 1024, height: 768)
@ -43,8 +46,14 @@ class Poltergeist.WebPage
onLoadFinishedNative: ->
@_source or= @native.content
onConsoleMessage: (message) ->
console.log(message)
onConsoleMessage: (message, line, file) ->
if line == 0 && file == "undefined"
# file:line will always be "undefined:0" in current release of
# PhantomJS ;(
@_errors.push(message)
else
# here line == 1 && file == "". don't ask me why!
console.log(message)
content: ->
@native.content
@ -52,6 +61,12 @@ class Poltergeist.WebPage
source: ->
@_source
errors: ->
@_errors
clearErrors: ->
@_errors = []
viewportSize: ->
@native.viewportSize

View file

@ -3,13 +3,15 @@ module Capybara
class Error < StandardError
end
class BrowserError < Error
class ClientError < Error
attr_reader :response
def initialize(response)
@response = response
end
end
class BrowserError < ClientError
def name
response['name']
end
@ -23,12 +25,25 @@ module Capybara
end
end
class NodeError < Error
attr_reader :node, :response
class JavascriptError < ClientError
def javascript_messages
response['args'].first
end
def message
"One or more errors were raised in the Javascript code on the page: #{javascript_messages.inspect} " \
"Unfortunately, it is not currently possible to provide a stack trace, or even the line/file where " \
"the error occurred. (This is due to lack of support within QtWebKit.) Fixing this is a high " \
"priority, but we're not there yet."
end
end
class NodeError < ClientError
attr_reader :node
def initialize(node, response)
@node = node
@response = response
@node = node
super(response)
end
end

View file

@ -115,5 +115,28 @@ module Capybara::Poltergeist
raise "process is still alive"
end
end
context 'javascript errors' do
it 'propagates an error on the page to a ruby exception' do
expect { @driver.execute_script "omg" }.to raise_error(JavascriptError)
begin
@driver.execute_script "omg"
rescue JavascriptError => e
e.message.should include("omg")
e.message.should include("ReferenceError")
end
end
it "doesn't re-raise a Javascript error if it's rescued" do
begin
@driver.execute_script("omg")
rescue JavascriptError
end
# should not raise again
@driver.evaluate_script("1+1").should == 2
end
end
end
end