prototype: Merge -r7016:HEAD from ../branches/ajax. Add Ajax.Response object which supports the following methods: responseJSON, headerJSON, getHeader, getAllHeaders and handles browser discrepancies in the other response methods. Add sanitizeJSON, evalJS and evalJSON to Ajax.Request. Closes #8122, #8006, #7295.

This commit is contained in:
Sam Stephenson 2007-08-04 04:40:22 +00:00
parent 281ac64876
commit 52cf3f2550
9 changed files with 388 additions and 79 deletions

View File

@ -1,5 +1,11 @@
*SVN*
* Add Ajax.Response object which supports the following methods: responseJSON, headerJSON, getHeader, getAllHeaders and handles browser discrepancies in the other response methods. Add sanitizeJSON, evalJS and evalJSON to Ajax.Request. Closes #8122, #8006, #7295. [Tobie Langel]
* Add an isRunningFromRake property to unit tests. [Tobie Langel]
* Add support for Opera browser in jstest.rb. [Tobie Langel]
* Inheritance branch merged to trunk; robust inheritance support for Class.create. Closes #5459. [Dean Edwards, Alex Arnell, Andrew Dupont, Mislav Mahronic]
- To access a method's superclass method, add "$super" as the first argument. (The naming is significant.) Works like Function#wrap.
- Class.create now takes two optional arguments. The first is an existing class to subclass; the second is an object literal defining the instance properties/methods. Either can be omitted. Backwards-compatible with old Class.create.
@ -9,6 +15,8 @@
* Add Function#argumentNames, which returns an ordered array of the function's named arguments. [sam]
* Prevent a crash in Safari 1.3 on String#stripScripts and String#extractScripts. Closes #8332. [grant, Tobie Langel]
* Add Prototype.Browser.MobileSafari which evaluates to true on the iPhone's browser. [sam]
* Optimize Selector#match and Element#match for simple selectors. Closes #9082. [Andrew Dupont]

View File

@ -49,7 +49,7 @@ JavaScriptTestTask.new(:test_units) do |t|
t.run(test_file) unless tests_to_run && !tests_to_run.include?(test_name)
end
%w( safari firefox ie konqueror ).each do |browser|
%w( safari firefox ie konqueror opera ).each do |browser|
t.browser(browser.to_sym) unless browsers_to_test && !browsers_to_test.include?(browser)
end
end

View File

@ -56,7 +56,9 @@ Ajax.Base.prototype = {
asynchronous: true,
contentType: 'application/x-www-form-urlencoded',
encoding: 'UTF-8',
parameters: ''
parameters: '',
evalJSON: true,
evalJS: true
}
Object.extend(this.options, options || {});
@ -64,7 +66,7 @@ Ajax.Base.prototype = {
if (typeof this.options.parameters == 'string')
this.options.parameters = this.options.parameters.toQueryParams();
}
}
};
Ajax.Request = Class.create();
Ajax.Request.Events =
@ -101,8 +103,9 @@ Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
}
try {
if (this.options.onCreate) this.options.onCreate(this.transport);
Ajax.Responders.dispatch('onCreate', this, this.transport);
var response = new Ajax.Response(this);
if (this.options.onCreate) this.options.onCreate(response);
Ajax.Responders.dispatch('onCreate', this, response);
this.transport.open(this.method.toUpperCase(), this.url,
this.options.asynchronous);
@ -167,33 +170,39 @@ Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
},
success: function() {
return !this.transport.status
|| (this.transport.status >= 200 && this.transport.status < 300);
var status = this.getStatus();
return !status || (status >= 200 && status < 300);
},
getStatus: function() {
try {
return this.transport.status || 0;
} catch (e) { return 0 }
},
respondToReadyState: function(readyState) {
var state = Ajax.Request.Events[readyState];
var transport = this.transport, json = this.evalJSON();
var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this);
if (state == 'Complete') {
try {
this._complete = true;
(this.options['on' + this.transport.status]
(this.options['on' + response.status]
|| this.options['on' + (this.success() ? 'Success' : 'Failure')]
|| Prototype.emptyFunction)(transport, json);
|| Prototype.emptyFunction)(response, response.headerJSON);
} catch (e) {
this.dispatchException(e);
}
var contentType = this.getHeader('Content-type');
if (contentType && contentType.strip().
match(/^(text|application)\/(x-)?(java|ecma)script(;.*)?$/i))
this.evalResponse();
var contentType = response.getHeader('Content-type');
if (this.options.evalJS == 'force'
|| (this.options.evalJS && contentType
&& contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i)))
this.evalResponse();
}
try {
(this.options['on' + state] || Prototype.emptyFunction)(transport, json);
Ajax.Responders.dispatch('on' + state, this, transport, json);
(this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON);
Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON);
} catch (e) {
this.dispatchException(e);
}
@ -210,13 +219,6 @@ Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
} catch (e) { return null }
},
evalJSON: function() {
try {
var json = this.getHeader('X-JSON');
return json ? json.evalJSON() : null;
} catch (e) { return null }
},
evalResponse: function() {
try {
return eval((this.transport.responseText || '').unfilterJSON());
@ -231,6 +233,76 @@ Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
}
});
Ajax.Response = Class.create();
Ajax.Response.prototype = {
initialize: function(request){
this.request = request;
var transport = this.transport = request.transport,
readyState = this.readyState = transport.readyState;
if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) {
this.status = this.getStatus();
this.statusText = this.getStatusText();
this.responseText = String.interpret(transport.responseText);
this.headerJSON = this.getHeaderJSON();
}
if(readyState == 4) {
var xml = transport.responseXML;
this.responseXML = xml === undefined ? null : xml;
this.responseJSON = this.getResponseJSON();
}
},
status: 0,
statusText: '',
getStatus: Ajax.Request.prototype.getStatus,
getStatusText: function() {
try {
return this.transport.statusText || '';
} catch (e) { return '' }
},
getHeader: Ajax.Request.prototype.getHeader,
getAllHeaders: function() {
try {
return this.getAllResponseHeaders();
} catch (e) { return null }
},
getResponseHeader: function(name) {
return this.transport.getResponseHeader(name);
},
getAllResponseHeaders: function() {
return this.transport.getAllResponseHeaders();
},
getHeaderJSON: function() {
var json = this.getHeader('X-JSON');
try {
return json ? json.evalJSON(this.request.options.sanitizeJSON) : null;
} catch (e) {
this.request.dispatchException(e);
}
},
getResponseJSON: function() {
var options = this.request.options;
try {
if (options.evalJSON == 'force' || (options.evalJSON &&
(this.getHeader('Content-type') || '').include('application/json')))
return this.transport.responseText.evalJSON(options.sanitizeJSON);
return null;
} catch (e) {
this.request.dispatchException(e);
}
}
};
Ajax.Updater = Class.create();
Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), {
@ -244,29 +316,29 @@ Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), {
this.setOptions(options);
var onComplete = this.options.onComplete || Prototype.emptyFunction;
this.options.onComplete = (function(transport, param) {
this.updateContent();
onComplete(transport, param);
this.options.onComplete = (function(response, param) {
this.updateContent(response.responseText);
onComplete(response, param);
}).bind(this);
this.request(url);
},
updateContent: function() {
var receiver = this.container[this.success() ? 'success' : 'failure'];
var response = this.transport.responseText, options = this.options;
updateContent: function(responseText) {
var receiver = this.container[this.success() ? 'success' : 'failure'],
options = this.options;
if (!options.evalScripts) response = response.stripScripts();
if (!options.evalScripts) responseText = responseText.stripScripts();
if (receiver = $(receiver)) {
if (options.insertion) {
if (typeof options.insertion == 'string') {
var insertion = {}; insertion[options.insertion] = response;
var insertion = {}; insertion[options.insertion] = responseText;
receiver.insert(insertion);
}
else options.insertion(receiver, response);
else options.insertion(receiver, responseText);
}
else receiver.update(response);
else receiver.update(responseText);
}
if (this.success()) {
@ -302,12 +374,12 @@ Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), {
(this.onComplete || Prototype.emptyFunction).apply(this, arguments);
},
updateComplete: function(request) {
updateComplete: function(responseText) {
if (this.options.decay) {
this.decay = (request.responseText == this.lastText ?
this.decay = (responseText == this.lastText ?
this.decay * this.options.decay : 1);
this.lastText = request.responseText;
this.lastText = responseText;
}
this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency);
},

View File

@ -134,6 +134,34 @@ class KonquerorBrowser < Browser
end
end
class OperaBrowser < Browser
def initialize(path='c:\Program Files\Opera\Opera.exe')
@path = path
end
def setup
if windows?
puts %{
MAJOR ANNOYANCE on Windows.
You have to shut down Opera manually after each test
for the script to proceed.
Any suggestions on fixing this is GREATLY appreciated!
Thank you for your understanding.
}
end
end
def visit(url)
applescript('tell application "Opera" to GetURL "' + url + '"') if macos?
system("#{@path} #{url}") if windows?
system("opera #{url}") if linux?
end
def to_s
"Opera"
end
end
# shut up, webrick :-)
class ::WEBrick::HTTPServer
def access_log(config, req, res)
@ -185,7 +213,10 @@ class JavaScriptTestTask < ::Rake::TaskLib
@server.mount_proc("/content-type") do |req, res|
res.body = req["content-type"]
end
@server.mount_proc("/response") do |req, res|
req.query.each {|k, v| res[k] = v unless k == 'responseBody'}
res.body = req.query["responseBody"]
end
yield self if block_given?
define
end
@ -238,6 +269,8 @@ class JavaScriptTestTask < ::Rake::TaskLib
IEBrowser.new
when :konqueror
KonquerorBrowser.new
when :opera
OperaBrowser.new
else
browser
end

View File

@ -290,6 +290,9 @@ Test.Unit.Assertions.prototype = {
if (this.errors > 0) return 'error';
return 'passed';
},
isRunningFromRake: (function() {
return window.location.port == 4711;
})(),
assert: function(expression) {
var message = arguments[1] || 'assert: got "' + Test.Unit.inspect(expression) + '"';
try { expression ? this.pass() :

View File

@ -28,14 +28,59 @@
<!-- Tests follow -->
<script type="text/javascript" language="javascript" charset="utf-8">
// <![CDATA[
var Fixtures = {
js: {
responseBody: '$("content").update("<H2>Hello world!</H2>");',
'Content-Type': ' text/javascript '
},
html: {
responseBody: "Pack my box with <em>five dozen</em> liquor jugs! " +
"Oh, how <strong>quickly</strong> daft jumping zebras vex..."
},
xml: {
responseBody: '<?xml version="1.0" encoding="UTF-8" ?><name attr="foo">bar</name>',
'Content-Type': 'application/xml'
},
json: {
responseBody: '{\n\r"test": 123}',
'Content-Type': 'application/json'
},
jsonWithoutContentType: {
responseBody: '{"test": 123}'
},
invalidJson: {
responseBody: '{});window.attacked = true;({}',
'Content-Type': 'application/json'
},
headerJson: {
'X-JSON': '{"test": 123}'
}
};
var extendDefault = function(options) {
return Object.extend({
asynchronous: false,
method: 'get',
onException: function(e) { throw e }
}, options);
};
var responderCounter = 0;
// lowercase comparison because of MSIE which presents HTML tags in uppercase
var sentence = ("Pack my box with <em>five dozen</em> liquor jugs! " +
"Oh, how <strong>quickly</strong> daft jumping zebras vex...").toLowerCase();
var message = 'You must be running your tests from rake to test this feature.';
new Test.Unit.Runner({
setup: function(){
$('content').update('');
$('content2').update('');
@ -53,7 +98,7 @@
new Ajax.Request("fixtures/hello.js", {
asynchronous: false,
method: 'GET',
onComplete: function(response) { eval(response.responseText) }
evalJS: 'force'
});
assertEqual(0, Ajax.activeRequestCount);
@ -67,9 +112,9 @@
new Ajax.Request("fixtures/hello.js", {
asynchronous: true,
method: 'get',
onComplete: function(response) { eval(response.responseText) }
evalJS: 'force'
});
wait(1000,function(){
wait(1000, function() {
var h2 = $("content").firstChild;
assertEqual("Hello world!", h2.innerHTML);
});
@ -80,7 +125,7 @@
new Ajax.Updater("content", "fixtures/content.html", { method:'get' });
wait(1000,function(){
wait(1000, function() {
assertEqual(sentence, $("content").innerHTML.strip().toLowerCase());
$('content').update('');
@ -91,7 +136,7 @@
new Ajax.Updater("", "fixtures/content.html", { method:'get', parameters:"pet=monkey" });
wait(1000,function(){
wait(1000, function() {
assertEqual(sentence, $("content").innerHTML.strip().toLowerCase());
assertEqual("", $("content2").innerHTML);
});
@ -101,16 +146,16 @@
testUpdaterWithInsertion: function() {with(this) {
$('content').update();
new Ajax.Updater("content", "fixtures/content.html", { method:'get', insertion: Insertion.Top });
wait(1000,function(){
wait(1000, function() {
assertEqual(sentence, $("content").innerHTML.strip().toLowerCase());
$('content').update();
new Ajax.Updater("content", "fixtures/content.html", { method:'get', insertion: 'bottom' });
wait(1000,function(){
wait(1000, function() {
assertEqual(sentence, $("content").innerHTML.strip().toLowerCase());
$('content').update();
new Ajax.Updater("content", "fixtures/content.html", { method:'get', insertion: 'after' });
wait(1000,function(){
wait(1000, function() {
assertEqual('five dozen', $("content").next().innerHTML.strip().toLowerCase());
});
});
@ -122,7 +167,7 @@
assertEqual(1, Ajax.Responders.responders.length);
var dummyResponder = {
onComplete: function(req){ /* dummy */ }
onComplete: function(req) { /* dummy */ }
};
Ajax.Responders.register(dummyResponder);
@ -148,52 +193,189 @@
assertEqual(1, responderCounter);
assertEqual(1, Ajax.activeRequestCount);
wait(1000,function(){
wait(1000,function() {
assertEqual(3, responderCounter);
assertEqual(0, Ajax.activeRequestCount);
});
}},
testEvalResponseShouldBeCalledBeforeOnComplete: function() {with(this) {
assertEqual("", $("content").innerHTML);
if (isRunningFromRake) {
assertEqual("", $("content").innerHTML);
assertEqual(0, Ajax.activeRequestCount);
new Ajax.Request("fixtures/hello.js", {
asynchronous: false,
method: 'GET',
onComplete: function(response) { assertNotEqual("", $("content").innerHTML) }
});
assertEqual(0, Ajax.activeRequestCount);
assertEqual(0, Ajax.activeRequestCount);
new Ajax.Request("fixtures/hello.js", extendDefault({
onComplete: function(response) { assertNotEqual("", $("content").innerHTML) }
}));
assertEqual(0, Ajax.activeRequestCount);
var h2 = $("content").firstChild;
assertEqual("Hello world!", h2.innerHTML);
var h2 = $("content").firstChild;
assertEqual("Hello world!", h2.innerHTML);
} else {
info(message);
}
}},
testContentTypeSetForSimulatedVerbs: function() {with(this) {
var isRunningFromRake = true;
new Ajax.Request('/content-type', {
method: 'put',
contentType: 'application/bogus',
asynchronous: false,
onFailure: function() {
isRunningFromRake = false;
},
onComplete: function(response) {
if (isRunningFromRake)
if (isRunningFromRake) {
new Ajax.Request('/content-type', extendDefault({
method: 'put',
contentType: 'application/bogus',
onComplete: function(response) {
assertEqual('application/bogus; charset=UTF-8', response.responseText);
}
});
}
}));
} else {
info(message);
}
}},
testOnCreateCallback: function() {with(this) {
new Ajax.Request("fixtures/content.html", {
asynchronous: false,
new Ajax.Request("fixtures/content.html", extendDefault({
onCreate: function(transport) { assertEqual(0, transport.readyState) },
onComplete: function(transport) { assertNotEqual(0, transport.readyState) }
});
}}
}));
}},
testEvalJS: function() {with(this) {
if (isRunningFromRake) {
$('content').update();
new Ajax.Request("/response", extendDefault({
parameters: Fixtures.js,
onComplete: function(transport) {
var h2 = $("content").firstChild;
assertEqual("Hello world!", h2.innerHTML);
}
}));
$('content').update();
new Ajax.Request("/response", extendDefault({
evalJS: false,
parameters: Fixtures.js,
onComplete: function(transport) {
assertEqual("", $("content").innerHTML);
}
}));
} else {
info(message);
}
$('content').update();
new Ajax.Request("fixtures/hello.js", extendDefault({
evalJS: 'force',
onComplete: function(transport) {
var h2 = $("content").firstChild;
assertEqual("Hello world!", h2.innerHTML);
}
}));
}},
testCallbacks: function() {with(this) {
var options = extendDefault({
onCreate: function(transport) { assertInstanceOf(Ajax.Response, transport) }
});
Ajax.Request.Events.each(function(state){
options['on' + state] = options.onCreate;
});
new Ajax.Request("fixtures/content.html", options);
}},
testResponseText: function() {with(this) {
new Ajax.Request("fixtures/empty.html", extendDefault({
onComplete: function(transport) { assertEqual('', transport.responseText) }
}));
new Ajax.Request("fixtures/content.html", extendDefault({
onComplete: function(transport) { assertEqual(sentence, transport.responseText.toLowerCase()) }
}));
}},
testResponseXML: function() {with(this) {
if (isRunningFromRake) {
new Ajax.Request("/response", extendDefault({
parameters: Fixtures.xml,
onComplete: function(transport) {
assertEqual('foo', transport.responseXML.getElementsByTagName('name')[0].getAttribute('attr'))
}
}));
} else {
info(message);
}
}},
testResponseJSON: function() {with(this) {
if (isRunningFromRake) {
new Ajax.Request("/response", extendDefault({
parameters: Fixtures.json,
onComplete: function(transport) { assertEqual(123, transport.responseJSON.test) }
}));
new Ajax.Request("/response", extendDefault({
evalJSON: false,
parameters: Fixtures.json,
onComplete: function(transport) { assertNull(transport.responseJSON) }
}));
new Ajax.Request("/response", extendDefault({
parameters: Fixtures.jsonWithoutContentType,
onComplete: function(transport) { assertNull(transport.responseJSON) }
}));
new Ajax.Request("/response", extendDefault({
sanitizeJSON: true,
parameters: Fixtures.invalidJson,
onException: function(request, error) {
assert(error.message.include('Badly formed JSON string'));
assertInstanceOf(Ajax.Request, request);
}
}));
} else {
info(message);
}
new Ajax.Request("fixtures/data.json", extendDefault({
evalJSON: 'force',
onComplete: function(transport) { assertEqual(123, transport.responseJSON.test) }
}));
}},
testHeaderJSON: function() {with(this) {
if (isRunningFromRake) {
new Ajax.Request("/response", extendDefault({
parameters: Fixtures.headerJson,
onComplete: function(transport, json) {
assertEqual(123, transport.headerJSON.test);
assertEqual(123, json.test);
}
}));
new Ajax.Request("/response", extendDefault({
onComplete: function(transport, json) {
assertNull(transport.headerJSON)
assertNull(json)
}
}));
} else {
info(message);
}
}},
testGetHeader: function() {with(this) {
if (isRunningFromRake) {
new Ajax.Request("/response", extendDefault({
parameters: { 'X-TEST': 'some value' },
onComplete: function(transport) {
assertEqual('some value', transport.getHeader('X-Test'));
assertNull(null, transport.getHeader('X-Inexistant'));
}
}));
} else {
info(message);
}
}}
}, 'testlog');
// ]]>
</script>

View File

@ -0,0 +1 @@
{test: 123}

View File

View File

@ -45,16 +45,26 @@
// <![CDATA[
var testObj = {
isNice: function(){
isNice: function() {
return true;
},
isBroken: function(){
isBroken: function() {
return false;
}
}
new Test.Unit.Runner({
testIsRunningFromRake: function() { with(this) {
if (window.location.toString().startsWith('http')) {
assert(isRunningFromRake);
info('These tests are runingn from rake.')
} else {
assert(!isRunningFromRake);
info('These tests are *not* running from rake.')
}
}},
testAssertEqual: function() { with(this) {
assertEqual(0, 0);
assertEqual(0, 0, "test");