Browse files

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.
  • Loading branch information...
1 parent 281ac64 commit 52cf3f25501e1cdecf3bc8a70cd6b20fe1e2d875 @sstephenson committed Aug 4, 2007
Showing with 387 additions and 78 deletions.
  1. +8 −0 CHANGELOG
  2. +1 −1 Rakefile
  3. +109 −37 src/ajax.js
  4. +34 −1 test/lib/jstest.rb
  5. +3 −0 test/lib/unittest.js
  6. +219 −37 test/unit/ajax.html
  7. +1 −0 test/unit/fixtures/data.json
  8. 0 test/unit/fixtures/empty.html
  9. +12 −2 test/unit/unit_tests.html
View
8 CHANGELOG
@@ -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
2 Rakefile
@@ -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
146 src/ajax.js
@@ -56,15 +56,17 @@ 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 || {});
this.options.method = this.options.method.toLowerCase();
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
35 test/lib/jstest.rb
@@ -134,6 +134,34 @@ def to_s
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 @@ def initialize(name=:test)
@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 @@ def browser(browser)
IEBrowser.new
when :konqueror
KonquerorBrowser.new
+ when :opera
+ OperaBrowser.new
else
browser
end
View
3 test/lib/unittest.js
@@ -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
256 test/unit/ajax.html
@@ -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
1 test/unit/fixtures/data.json
@@ -0,0 +1 @@
+{test: 123}
View
0 test/unit/fixtures/empty.html
No changes.
View
14 test/unit/unit_tests.html
@@ -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");

0 comments on commit 52cf3f2

Please sign in to comment.