I'm trying to use SoundJS to load sounds dynamically.
If I register a sound that has been already registered with createjs.Sound.registerSound, I don't get the callback function to load (called by the "fileload" event listener).
createjs.Sound.addEventListener("fileload", createjs.proxy(this.loadHandler, this));
createjs.Sound.registerSound(url, "song");
According to a post here and SoundJS documentation, I can check if the sound has been already loaded with loadComplete(url).
But this:
createjs.Sound.loadComplete("audio/file.mp3")
...returns:
Uncaught TypeError: Cannot read property 'mp3' of null.
The problem seems to be that the following function is returning null, because activePlugin is null. Why that happens when audio is playing correctly (I'm testing this on Chrome on a Mac)? Shouldn't it be using defaults WebAudioPlugin or HTMLAudioPlugin?
from soundjs-0.5.2.combined.js:
s.getCapabilities = function () {
if (s.activePlugin == null) {
return null;
}
return s.activePlugin._capabilities;
};
UPDATE
Here's a temporary workaround, ugly, but works.
I solved by doing two things:
1) before using for createjs.Sound.loadComplete(url), I first check if createjs.Sound.activePlugin is true.
2) for handling audio files that weren't loaded yet, I added a conditional inside the
SoundJS.loadComplete() function to check if s._preloadHash[src] === undefined, and return false if so (that means that URLs that aren't registered urls will return false, as if the file hasn't been loaded):
s.loadComplete = function (src) {
var details = s._parsePath(src);
if (details) {
src = s._getSrcById(details.src).src;
} else {
src = s._getSrcById(src).src;
}
// song has not been registered. return false
if (s._preloadHash[src] === undefined)
return false;
return (s._preloadHash[src][0] == true); // src only loads once, so if it's true for the first it's true for all
};
...and in another JavaScript file:
loadSong: function(url){
currentSongURL = url;
// if Sound.activePlugin returns false, load the song.
if (!createjs.Sound.activePlugin){
createjs.Sound.addEventListener("fileload", createjs.proxy(this.loadHandler, this));
createjs.Sound.registerSound(url, url); // url, id
return;
}
// if song is already loaded, resolve deferred.
if(createjs.Sound.loadComplete(url)) {
songDeferred.resolve();
}else{
createjs.Sound.addEventListener("fileload", createjs.proxy(this.loadHandler, this));
createjs.Sound.registerSound(url, url); // url, id
}
},
I'm studying how to extend SoundJS.loadComplete() without messing the SoundJS class, and how to avoid the repeated code on my loadSong() function above. It looks very messy indeed but that's what I got for now to make it work the way I intend to. Any help on that would be deeply appreciated.
Related
I am working on a react file-upload component. I got stuck with a rather trivial issue – I want for each file to show icon corresponding to a file extension. Icons are loaded via css as background images (using inline styles). The problem arises when I don't have an icon for given extension and thus want to show a fallback icon.
– I tried to use multiple css background-image declarations like this:
style={{
backgroundImage: `url(./icons/fallback.svg), url(./icons/${item.extension}.svg)`,
}}
or like this:
style={{
backgroundImage: `url(./icons/fallback.svg)`,
backgroundImage: `url(./icons/${item.extension}.svg)`,
}}
But it doesn't work, the fallback icon is not being used (or in one case I am not able to reproduce, both icon are shown, which is also undesired).
I tried to fetch the file to determine if it does exist, but the node server (i use create-react-app) is configured in a way that returns 200 or 304 even if the file isn't actually present.
I tried to use a solution which creates an image and uses onload and onerror events as beeng suggested in this question, which actually works fine – i am currently using slightly modified implementation of image-exists npm module – but I wasn't able to figure out how to refactor this function to simply return a boolean. Using console.log() and callbacks works fine; returning a boolean results in undefined. I suppose it is due to an asynchronous behaviour od Image methods, but I wasn't able to create a workaround – maybe using a Promise API?
My code:
exists = src => {
const checks = {};
return callback => {
if (src in checks) {
return callback(checks[src]);
}
let img = new Image();
img.onload = function() {
checks[src] = true;
callback(true);
};
img.onerror = function() {
checks[src] = false;
callback(false);
};
img.src = src;
};
};
Render method:
render() {
// So far so good, logs as expected, but not such useful
console.log(this.exists('./icons/jpg.svg')(bool => {
if(bool) {
console.log('yes')
} else {
console.log('no');
}
}));
// ...
}
If I try to return a boolean directly, it results in undefined:
render() {
console.log(this.exists('./icons/jpg.svg')(bool => bool));
// ...
}
You are right, the function does not return a boolean because this is the parameter of the callback of your exists function, which is called asynchronously. The solution is to render your icon asynchronously too, something like...
this.exists(img)(bool => {
if (bool) {
render(img)
} else {
render('fallback.svg');
}
}
O.K. I finally promisify the whole thing. I hooked the former exists function (now checkImage) to a promise chain(saw… massacre…) which is triggered by reading files to upload and results in setState and rerender:
The url checking function:
checkImage = (path, fallback) => {
return new Promise(resolve => {
const img = new Image();
img.src = path;
img.onload = () => resolve(path);
img.onerror = () => resolve(fallback);
});
};
Calling with Promise.all():
// items are array of objects which contains file contents, name, extension etc...
checkIcons = items =>
Promise.all(
items.map(item => {
const url = `./icons/${item.extension}.svg`;
return this.checkImage(url, this.state.fallbackIconUrl).then(result => {
return { ...item, icon: result };
});
})
);
Definitely not the slickiest one in town and it would possibly need some caching (or may not – it does seem the browser can handle this by itself), but works fine.
I can return a value if I send a sync message:
// frame script
var chromeBtnText = sendSyncMessage("getChromeToolbarButtonText");
if (chromeBtnText == 'blah') {
alert('tool is blah');
}
// chrome script
messageManager.addMessageListener("getChromeToolbarButtonText", listener);
function listener(message) {
return document.getElementById('myChromeToolbarButton').label.value;
}
How do I achieve this with a callback with sendAsyncMessage?
I was hoping to do something like:
// frame script
function myCallback(val) {
var chromeBtnText = val;
if (chromeBtnText == 'blah') {
alert('tool is blah');
}
}
var chromeBtnText = sendAsyncMessage("getChromeToolbarButtonText", null, myCallback);
There is no callback for replies. In fact, there is no reply at all. The return value from the chrome message listener is simply ignored for async messages.
To do fully async communication, you'd have to send another message containing the reply.
Frame script
addMessageListener("getChromeToolbarButtonTextReply", function(message) {
alert(message.data.btnText);
});
sendAsyncMessage("getChromeToolbarButtonText");
Chrome
messageManager.addMessageListener("getChromeToolbarButtonText", function(message) {
var btnText = document.getElementById('myChromeToolbarButton').label.value;
// Only send message to the frame script/message manager
// that actually asked for it.
message.target.messageManager.sendAsyncMessage(
"getChromeToolbarButtonTextReply",
{btnText: btnText}
);
});
PS: All messages share a namespace. So to avoid conflicts when another piece of code wants to use the same name getChromeToolbarButtonText, you better choose a more unique name in the first place, like prefixing your messages with your add-on name my-unique-addoon:getChromeToolbarButtonText or something like that. ;)
I was also hoping to do something similar:
messageManager.sendAsyncMessage("my-addon-framescript-message", null, myCallback);
I'm going the other direction so the myCallback would be in chrome but it's exactly the same principle.
I'd used similar approaches to #Noitidart and #nmaier before but in this new case I wanted to bind to some local data so myCallback can behave differently based on the application state at the time the first message was sent rather than at the time the callback is executed, all while allowing for the possibility of multiple message round-trips being in progress concurrently.
Chrome:
let someLocalState = { "hello": "world" };
let callbackName = "my-addon-somethingUnique"; // based on current state or maybe generate a UUID
let myCallback = function(message) {
messageManager.removeMessageListener(callbackName, myCallback);
//message.data.foo == "bar"
//someLocalState.hello == "world"
}.bind(this); // .bind(this) is optional but useful if the local state is attached to the current object
messageManager.addMessageListener(callbackName, myCallback);
messageManager.sendAsyncMessage("my-addon-framescript-message", { callbackName: callbackName } );
Framescript:
let messageHandler = function(message) {
let responseData = { foo: "bar" };
sendAsyncMessage(message.data.callbackName, responseData);
};
addMessageListener("my-addon-framescript-message", messageHandler);
There's a real-world example here: https://github.com/luckyrat/KeeFox/commit/c50f99033d2d07068140438816f8cc5e5e290da9
It should be possible for Firefox to be improved to encapsulate this functionality in the built-in messageManager one day but in the mean-time this approach works well and with a surprisingly small amount of boiler-plate code.
in this snippet below. i add the callback before sendAsyncMessage('my-addon-id#jetpack:getChromeToolbarbuttonText'... as i know it will send back. Then I remove it after callback executes. I know I don't have to but just to kind of make it act like real callback, just to kind of show people, maybe it helps someone understand.
Frame:
/////// frame script
function CALLBACK_getChromeToolbarButtonText(val) {
removeMessageListner('my-addon-id#jetpack:getChromeToolbarButtonTextCallbackMessage', CALLBACK_getChromeToolbarButtonText); //remove the callback
var chromeBtnText = val;
if (chromeBtnText == 'blah') {
alert('tool is blah');
}
}
addMessageListener('my-addon-id#jetpack:getChromeToolbarButtonTextCallbackMessage', CALLBACK_getChromeToolbarButtonText); //add the callback
var chromeBtnText = sendAsyncMessage("my-addon-id#jetpack:getChromeToolbarButtonText", null);
Chrome:
////// chrome script
messageManager.addMessageListener("my-addon-id#jetpack:getChromeToolbarButtonText", listener);
function listener() {
var val = document.getElementById('myChromeToolbarButton').label.value;
sendAsyncMessage('my-addon-id#jetpack:getChromeToolbarButtonTextCallbackMessage',val);
}
I read about this SoundCloud's API bug that returns a 404 error while trying to stream a track even if the "streamable" property was set to true.
I found some other questions about the topic (see "Tracks for “The Hives” are not streaming via the api" or "Soundcloud API SC.stream (track not loading)"), but what I would like to know is how to detect the error, and how to workaround it. I tried with some try {} catch() {} but it seems that I can't detect the problem.
Here is some background information:
I have a php returning a JSON array with a list of tracks to play. My script reads the array one-by-one, and then tries to play the current one. Everything works fine, but when a 404 error is returned the script ends, without jumping to the next track. It seems that I am unable to detect that kind of problem.
Here is the JavaScript method that manages the stream:
playTrack = function() {
// console.log('playTrack');
SC.get(
"/tracks/" + playlist[ now_playing ].sndc_id,
function(track, error) {
if (error) {
sendErrorReport(playlist[ now_playing ].id);
loadNext();
} else {
try {
SC.stream(
"/tracks/" + playlist[ now_playing ].sndc_id,
function(sound, error) {
sound_object = sound;
if (error || !sound_object) {
sendErrorReport(playlist[ now_playing ].id);
loadNext();
} else {
sound_object.play(
{
'from': parseInt(current_position),
'onfinish': function() {
current_position = 0;
updateCounter();
$('#radio-waveform-position').css( { 'opacity': '0', 'width' : '0%' } );
loadNext();
},
'onplay': function() {
$('#radio-like').removeClass('liked');
playing = true;
updateInfo();
},
'whileplaying': function() {
current_position = this.position;
$('#radio-waveform-position').css( { 'opacity': '1', 'width' : (100 * current_position / this.duration).toFixed(3) + '%' } );
},
'ondataerror': function() {
sendErrorReport(playlist[ now_playing ].id);
loadNext();
}
}
);
if ($('html').hasClass('touch') || !autoplay) $('#radio-play-pause').click();
}
}
);
} catch (err) {
sendErrorReport(playlist[ now_playing ].id);
loadNext();
}
}
}
);
}
It seems that both the SC.get and SC.stream "error" return parameters are always empty for this kind of "faulty" tracks. As you can see, I tried to wrap everything in a try() {} catch() {} but without success. Also, the sound_object.play() ondataerror method is completely ignored.
The script can be seen in action here: http://foggetabout.it/
I read that someone found a workaround for that, but there was no explanation. Has anyone any idea on how to solve it?
Run into something similar. Not sure if it is connected but it appears some tracks are only streamable with flash so the GET method states they’re streamable but the stream can fail if flash is not available.
One way to get around it is to check when SoundManager has loaded the track using the onload & readyState, which (from docs: http://www.schillmania.com/projects/soundmanager2/doc/) has 4 states:
0 = uninitialised
1 = loading
2 = failed/error
3 = loaded/success
So:
sound_object.play({
// Exisiting…
onload: function() {
if (this.readyState == 2) {
// Handle error
}
});
There are some caveats on when the state changes so if this becomes a problem you could also try using durationEstimate as it returns null for the failed tracks I’m coming across.
For a lot of songs streamable is set to true and its stream_url is defined, but the resource itself is not there.
Using curl, hitting the i1 endpoint with your desired track_id and a registered API_KEY is the first step to checking for the actual resources on a SoundCloud hosted track. (Warning: the i1 endpoint is not officially documented in SoundCloud's HTTP API Reference)
curl https://api.soundcloud.com/i1/tracks/{{track_id}}/streams?client_id={{API_KEY}}
This will return a JSON object that looks something like:
{
"http_mp3_128_url": "{some_url}",
"preview_mp3_128_url": "{some_url}"
}
If the http_mp3_128_url key-value pair exists, then that track has an accessible HTTP resource and would be streamable.
More info here - https://github.com/francismakes/i1-soundcloud
Building off of roj's answer, I used a bit of code like this to first get into handling 404s on the SC track streaming:
sound.play({
onload: function () {
if (this.readyState === 2){
alert('error');
}
},
NOT A DUPLICATE AS I HAVE YET TO FOUND A SATISFYING ANSWER ON OTHER THREADS:
Load and execute javascript code SYNCHRONOUSLY
Loading HTML and Script order execution
Load and execute javascript code SYNCHRONOUSLY
Looking for native Javascript answers, no jQuery, no requireJS, and so forth please :)
SUMMARY OF THE ENTIRE QUESTION:
I want to asynchronously load scripts but have ordered execution
I am trying to enforce that the code in the inserted script elements execute exactly in the same order as they were added to the dom tree.
That is, if I insert two script tags, first and second, any code in first must fire before the second, no matter who finishes loading first.
I have tried with the async attribute and defer attribute when inserting into the head but doesn't seem to obey.
I have tried with element.setAttribute("defer", "") and element.setAttribute("async", false) and other combinations.
The issue I am experiencing currently has to do when including an external script, but that is also the only test I have performed where there is latency.
The second script, which is a local one is always fired before the first one, even though it is inserted afterwards in the dom tree ( head ).
A) Note that I am still trying to insert both script elements into the DOM. Ofcourse the above could be achieved by inserting first, let it finish and insert the second one, but I was hoping there would be another way because this might be slow.
My understanding is that RequireJS seems to be doing just this, so it should be possible. However, requireJS might be pulling it off by doing it as described in A).
Code if you would like to try directly in firebug, just copy and paste:
function loadScript(path, callback, errorCallback, options) {
var element = document.createElement('script');
element.setAttribute("type", 'text/javascript');
element.setAttribute("src", path);
return loadElement(element, callback, errorCallback, options);
}
function loadElement(element, callback, errorCallback, options) {
element.setAttribute("defer", "");
// element.setAttribute("async", "false");
element.loaded = false;
if (element.readyState){ // IE
element.onreadystatechange = function(){
if (element.readyState == "loaded" || element.readyState == "complete"){
element.onreadystatechange = null;
loadElementOnLoad(element, callback);
}
};
} else { // Others
element.onload = function() {
loadElementOnLoad(element, callback);
};
}
element.onerror = function() {
errorCallback && errorCallback(element);
};
(document.head || document.getElementsByTagName('head')[0] || document.body).appendChild(element);
return element;
}
function loadElementOnLoad(element, callback) {
if (element.loaded != true) {
element.loaded = true;
if ( callback ) callback(element);
}
}
loadScript("http://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js",function() {
alert(1);
})
loadScript("http://ajax.googleapis.com/ajax/libs/chrome-frame/1.0.3/CFInstall.min.js",function() {
alert(2);
})
If you try the above code in like firebug, most often it will fire 2, and then 1. I want to ensure 1 and then 2 but include both in the head.
if I insert two script tags, first and second, any code in first must fire before the second, no matter who finishes loading first. I have tried with the async attribute and defer attribute
No, async and defer won't help you here. Whenever you dynamically insert script elements into the DOM, they are loaded and executed asynchronically. You can't do anything against that.
My understanding is that RequireJS seems to be doing just this
No. Even with RequireJS the scripts are executed asynchronous and not in order. Only the module initialiser functions in those scripts are just define()d, not executed. Requirejs then does look when their dependencies are met and executes them later when the other modules are loaded.
Of course you can reinvent the wheel, but you will have to go with a requirejs-like structure.
Ok, I think I have now came up with a solution.
The trick is that we keep track of each script to be loaded and their order as we insert them into the dom tree. Each of their callback is then registered accordingly to their element.
Then we keep track of when all has finished loading and when they all have, we go through the stack and fire their callbacks.
var stack = [];
stack.loaded = 0;
function loadScriptNew(path, callback) {
var o = { callback: callback };
stack.push(o);
loadScript(path, function() {
o.callbackArgs = arguments;
stack.loaded++;
executeWhenReady();
});
}
function executeWhenReady() {
if ( stack.length == stack.loaded ) {
while(stack.length) {
var o = stack.pop();
o.callback.apply(undefined, o.callbackArgs);
}
stack.loaded = 0;
}
}
// The above is what has been added to the code in the question.
function loadScript(path, callback) {
var element = document.createElement('script');
element.setAttribute("type", 'text/javascript');
element.setAttribute("src", path);
return loadElement(element, callback);
}
function loadElement(element, callback) {
element.setAttribute("defer", "");
// element.setAttribute("async", "false");
element.loaded = false;
if (element.readyState){ // IE
element.onreadystatechange = function(){
if (element.readyState == "loaded" || element.readyState == "complete"){
element.onreadystatechange = null;
loadElementOnLoad(element, callback);
}
};
} else { // Others
element.onload = function() {
loadElementOnLoad(element, callback);
};
}
(document.head || document.getElementsByTagName('head')[0] || document.body).appendChild(element);
return element;
}
function loadElementOnLoad(element, callback) {
if (element.loaded != true) {
element.loaded = true;
if ( callback ) callback(element);
}
}
loadScriptNew("http://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.js",function() {
alert(1);
});
loadScriptNew("http://ajax.googleapis.com/ajax/libs/chrome-frame/1.0.3/CFInstall.min.js",function() {
alert(2);
});
Ok, some of you might argue that there is missing info in the question, which I will give you and here we are actually just solving the callback. You are right. The code in the script is still executed in the wrong order, but the callback is now.
But for me this is good enough, as I intend to wrap all code that is loaded in a method call, alá AMD, such as a require or define call and will put on stack there, and then fire them in the callback instead.
I am still hoping out for Asad and his iframe solution, which I believe might provide the best answer to this question. For me though, this solution will solve my problems :)
I am posting here just like a draft
This do not work because cross-domain police
Here the idea is to obtain all scripts first and when they are in memory, execute them in order.
function loadScript(order, path) {
var xhr = new XMLHttpRequest();
xhr.open("GET",path,true);
xhr.send();
xhr.onreadystatechange = function(){
if(xhr.readyState == 4){
if(xhr.status >= 200 && xhr.status < 300 || xhr == 304){
loadedScripts[order] = xhr.responseText;
}
else {
//deal with error
loadedScripts[order] = 'alert("this is a failure to load script '+order+'");';
// or loadedScripts[order] = ''; // this smoothly fails
}
alert(order+' - '+xhr.status+' > '+xhr.responseText); // this is to show the completion order. Careful, FF stacks aletrs so you see in reverse.
// am I the last one ???
executeAllScripts();
}
};
}
function executeAllScripts(){
if(loadedScripts.length!=scriptsToLoad.length) return;
for(var a=0; a<loadedScripts.length; a++) eval(loadedScripts[a]);
scriptsToLoad = [];
}
var loadedScripts = [];
var scriptsToLoad = [
"http://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js",
"http://ajax.googleapis.com/ajax/libs/chrome-frame/1.0.3/CFInstall.min.js",
"http://nowhere.existing.real_script.com.ar/return404.js"
];
// load all even in reverse order ... or randomly
for(var a=0; a<scriptsToLoad.length; a++) loadScript(a, scriptsToLoad[a]);
After a while of fiddling around with it, here is what I came up with. Requests for the scripts are sent off immediately, but they are executed only in a specified order.
The algorithm:
The algorithm is to maintain a tree (I didn't have time to implement this: right now it is just the degenerate case of a list) of scripts that need to be executed. Requests for all of these are dispatched nearly simultaneously. Every time a script is loaded, two things happen: 1) the script is added to a flat list of loaded scripts, and 2) going down from the root node, as many scripts in each branch that are loaded but have not been executed are executed.
The cool thing about this is that not all scripts need to be loaded in order for execution to begin.
The implementation:
For demonstration purposes, I am iterating backward over the scriptsToExecute array, so that the request for CFInstall is sent off before the request for angularJS. This does not necessarily mean CFInstall will load before angularJS, but there is a better chance of it happening. Regardless of this, angularJS will always be evaluated before CFInstall.
Note that I've used jQuery to make my life easier as far as creating the iframe element and assigning the load handler is concerned, but you can write this without jQuery:
// The array of scripts to load and execute
var scriptsToExecute = [
"http://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js?t=" + Date.now(),
"http://ajax.googleapis.com/ajax/libs/chrome-frame/1.0.3/CFInstall.min.js?t=" + Date.now()
];
// Loaded scripts are stored here
var loadedScripts = {};
// For demonstration purposes, the requests are sent in reverse order.
// They will still be executed in the order specified in the array.
(function start() {
for (var i = scriptsToExecute.length - 1; i >= 0; i--) {
(function () {
var addr = scriptsToExecute[i];
requestData(addr, function () {
console.log("loaded " + addr);
});
})();
}
})();
// This function executes as many scripts as it currently can, by
// inserting script tags with the corresponding src attribute. The
// scripts aren't reloaded, since they are in the cache. You could
// alternatively eval `script.code`
function executeScript(script) {
loadedScripts[script.URL] = script.code
while (loadedScripts.hasOwnProperty(scriptsToExecute[0])) {
var scriptToRun = scriptsToExecute.shift()
var element = document.createElement('script');
element.setAttribute("type", 'text/javascript');
element.setAttribute("src", scriptToRun);
$('head').append(element);
console.log("executed " + scriptToRun);
}
}
// This function fires off a request for a script
function requestData(path, loadCallback) {
var iframe = $("<iframe/>").load(function () {
loadCallback();
executeScript({
URL: $(this).attr("src"),
code: $(this).html()
});
}).attr({"src" : path, "display" : "none"}).appendTo($('body'));
}
You can see a demo here. Observe the console.
cant you nest the loading using ur callbacks?
ie:
loadScript("http://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js",function() {
alert(1);
loadScript("http://ajax.googleapis.com/ajax/libs/chrome-frame/1.0.3/CFInstall.min.js",function() {
alert(2);
})
})
We are having a little problem with a functional test with casper.js.
We request the same resource twice, first with the GET and then with POST method.
Now when waiting for the second resource (POST) it matches the first resource and directly goes to the "then" function.
We would like to be able to check for the HTTP method in the "test" function, that way we can identify the resource properly. For now we use the status code (res.status), but that doesn't solve our problem fully, we really need the http method.
// create new email
this.click(xPath('//div[#id="tab-content"]//a[#class="button create"]'));
// GET
this.waitForResource('/some/resource',
function then() {
this.test.assertExists(xPath('//form[#id="email_edit_form"]'), 'Email edit form is there');
this.fill('form#email_edit_form', {
'email_entity[email]': 'test.bruce#im.com',
'email_entity[isMain]': 1
}, true);
// POST
this.waitForResource(
function test(res) {
return res.url.search('/some/resource') !== -1 && res.status === 201;
},
function then() {
this.test.assert(true, 'Email creation worked.');
},
function timeout() {
this.test.fail('Email creation did not work.');
}
);
},
function timeout() {
this.test.fail('Email adress creation form has not been loaded');
});
Or maybe there is a better way to test this scenario? Although since this is a functional test we need to keep all those steps in one test.
You can try to alter the form action url to add some query string, therefore generating a new resource appended to the stack. Could be done this way:
casper.thenEvaluate(function() {
var form = __utils__.findOne('#email_edit_form');
form.setAttribute('action', form.getAttribute('action') + '?plop');
});
That's a hack though, and functional testing should never be achieved that way. Let's hope more information will be added to the response objects in the future.
The res parameter that is passed to the test function has an ID. I created a helper that tests against this ID and blacklists it, so the same resource won't get accepted a second time.
var blackListedResourceIds = [],
testUniqueResource = function (resourceUrl, statusCode) {
return function (res) {
// check if resource was already loaded
var resourceFound = res.url.search(resourceUrl) !== -1;
// check statuscode
if (statusCode !== undefined) {
resourceFound = resourceFound && res.status === statusCode;
}
// check blacklisting
if (!resourceFound || blackListedResourceIds[res.id] !== undefined) {
return false;
} else {
blackListedResourceIds[res.id] = true;
return true;
}
};
};