Protractor Wait for Animation to Complete - javascript

In our application, we have some questions to answer that will update a progress bar. Currently, I have a function that waits for HTML Attribute changes which works for most things, but it's a little finicky for the progress bar since the animation occurs over 1-2 seconds as the bar moves from 0 - 10% etc. So the failure I'm currently facing is things like: Expected 11 to be within range 12, 14.
Code:
Util.prototype.waitForAttributeChange = function (el, attr, time) {
var timeout = time || 0,
currentAttr;
el.getAttribute(attr).then(function (val) {
currentAttr = val;
return currentAttr;
}).then(function () {
return browser.wait(function () {
return el.getAttribute(attr).then(function (val) {
return val !== currentAttr;
});
}, timeout);
});
};
Usage:
Util.waitForAttributeChange(Page.progressBar(), 'style', 10000).then(function () {
expect(Page.getProgressBarValue()).toBeWithinRange(12, 14);
};
Problem: The value grabbed is not the end result of the progress bar, it's still moving when it's grabbing it (because my function waits for Attribute changes, and the attribute did change at this point)
Question: Is there another way I can wait for an animation, specifically waiting for it to be completed? And/or is this possible without using browser.sleep()?

You might be able to solve this problem by using Expected Conditions.
I use the below methods whenever I need to wait for an element to be visible then wait for it to go away before executing the next step. This is helpful for temporary confirmation modals that may block interaction with other elements.
let waitTimeInSeconds = 15;
let EC = protractor.ExpectedConditions;
secondsToMillis(seconds) {
return seconds * 1000;
}
waitToBeVisible(element: ElementFinder) {
browser.wait(EC.visibilityOf(element), this.secondsToMillis(waitTimeInSeconds), 'The element \'' + element.locator() + '\' did not appear within ' + waitTimeInSeconds + ' seconds.');
}
waitToNotBeVisible(element: ElementFinder) {
browser.wait(EC.not(EC.visibilityOf(element)), this.secondsToMillis(waitTimeInSeconds), 'The element \'' + element.locator() + '\' still appeared within ' + waitTimeInSeconds + ' seconds.');
}

Related

Check if Javascript process is already running

Disclaimer: I'm totally 'new' to javascript, so please excuse the absolute amateur code.
I have a progress bar script that polls a URL which dumps some json data. The script then updates a few html values based on the contents of said json, including a progress bar.
Here's a simplified version of the code:
function update_progress(status_url, index) {
bar_id = document.getElementById(String('bar' + index));
stage_id = document.getElementById(String('stage' + index));
$.getJSON(status_url, function(data) {
percent = parseInt(data['current'] * 100 / data['total']);
// change progress bar and stage_id values
bar_id.innerHTML = (percent + '%');
bar_id.style.width = (percent + '%');
stage_id.title = (data['status']);
stage_id.value = (data['status']);
stage_id.innerHTML = (data['status']);
// if state is unexpected then end poll
if (data['state'] != 'PENDING' && data['state'] != 'PROGRESS' && data['state'] != "COMPLETE") {
stage_id.innerHTML = (data['state']);
// otherwise keep polling every 1.5 seconds
} else {
setTimeout(function() {
update_progress(status_url, index);
}, 1500);
}
}
Sometimes the status_url can return a 500 error if the json data hasn't yet been compiled. So I have a button that restarts this poll by calling update_progress again.
The problem:
If a user clicks the 'restart poll' button, there will be two active polls for the same status_url.
There can be many polls ongoing at once to a bunch of different status_url's - the more there are, the slower the response.
So I'd like to avoid being able to poll something that is already ongoing
Question: is there a way to check if a process with the same values is already ongoing in JS?
I'm aware that this is inefficient, and I'll be moving away from JS completely for this polling system at some point - however I need to do some quick efficiency patching on this current release before I get to that.
Thanks in advance
 Cancelling a javaScript interval
You can assign setTimeout to a variable:
var timer = setTimeout(...updateProgress...);
And then, when the user clicks the button to refresh the poll, you can do:
clearTimeout(timer)
To delete the original timeout process, then create another one.
This answer your question I think. However:
 There is no need to force the user to re-try the polling manually.
If the getJson call fails due to a 500 error, instead of clearing and launching the polling again, you can just keep polling. Now, I'm not used to getJSON syntax, but from what I read in this answer, you would do something like the following:
function update_progress(status_url, index) {
bar_id = document.getElementById(String('bar' + index));
stage_id = document.getElementById(String('stage' + index));
$.getJSON(status_url, function(data) {
percent = parseInt(data['current'] * 100 / data['total']);
// change progress bar and stage_id values
bar_id.innerHTML = (percent + '%');
bar_id.style.width = (percent + '%');
stage_id.title = (data['status']);
stage_id.value = (data['status']);
stage_id.innerHTML = (data['status']);
// if state is unexpected then end poll
if (data['state'] != 'PENDING' && data['state'] != 'PROGRESS' && data['state'] != "COMPLETE") {
stage_id.innerHTML = (data['state']);
// otherwise keep polling every 1.5 seconds
} else {
setTimeout(function() {
update_progress(status_url, index);
}, 1500);
}
})
.done(() => {}) // is this needed? I really don't know to be honest,
// maybe you can skip this right away
.fail(() => {
setTimeout(function() {
update_progress(status_url, index);
}, 1500); // we keep polling if the request fail!
})
}
You could separate your code on several callbacks, that would be executed on success, on failure, and other callback that would be executed always regardless of success or failure. You could then guess when the user clicks the submit button and disable it, then enable it again when the request is finished.
var ongoing = true; // Disable submit button
$.getJSON(status_url, function(data) {
// Your success code here
})
.fail(jqXHR, textStatus, errorThrown) {
// Your error/retry code here
})
.always(function() {
ongoing = false; // Enable submit button
});

Exact time of display: requestAnimationFrame usage and timeline

What I want to achieve is to detect the precise time of when a certain change appeared on the screen (primarily with Google Chrome). For example I show an item using $("xelement").show(); or change it using $("#xelement").text("sth new"); and then I would want to see what the performance.now() was exactly when the change appeared on the user's screen with the given screen repaint. So I'm totally open to any solutions - below I just refer primarily to requestAnimationFrame (rAF) because that is the function that is supposed to help achieve exactly this, only it doesn't seem to; see below.
Basically, as I imagine, rAF should execute everything inside it in about 0-17 ms (whenever the next frame appears on my standard 60 Hz screen). Moreover, the timestamp argument should give the value of the time of this execution (and this value is based on the same DOMHighResTimeStamp measure as performance.now()).
Now here is one of the many tests I made for this: https://jsfiddle.net/gasparl/k5nx7zvh/31/
function item_display() {
var before = performance.now();
requestAnimationFrame(function(timest){
var r_start = performance.now();
var r_ts = timest;
console.log("before:", before);
console.log("RAF callback start:", r_start);
console.log("RAF stamp:", r_ts);
console.log("before vs. RAF callback start:", r_start - before);
console.log("before vs. RAF stamp:", r_ts - before);
console.log("")
});
}
setInterval(item_display, Math.floor(Math.random() * (1000 - 500 + 1)) + 500);
What I see in Chrome is: the function inside rAF is executed always within about 0-3 ms (counting from a performance.now() immediately before it), and, what's weirdest, the rAF timestamp is something totally different from what I get with the performance.now() inside the rAF, being usually about 0-17 ms earlier than the performance.now() called before the rAF (but sometimes about 0-1 ms afterwards).
Here is a typical example:
before: 409265.00000001397
RAF callback start: 409266.30000001758
RAF stamp: 409260.832
before vs. RAF callback start: 1.30000000353902
before vs. RAF stamp: -4.168000013974961
In Firefox and in IE it is different. In Firefox the "before vs. RAF callback start" is either around 1-3 ms or around 16-17 ms. The "before vs. RAF stamp" is always positive, usually around 0-3 ms, but sometimes anything between 3-17 ms. In IE both differences are almost always around 15-18 ms (positive). These are more or less the same of different PCs. However, when I run it on my phone's Chrome, then, and only then, it seems plausibly correct: "before vs. RAF stamp" randomly around 0-17, and "RAF callback start" always a few ms afterwards.
For more context: This is for an online response-time experiment where users use their own PC (but I typically restrict browser to Chrome, so that's the only browser that really matters to me). I show various items repeatedly, and measure the response time as "from the moment of the display of the element (when the person sees it) to the moment when they press a key", and count an average from the recorded response times for specific items, and then check the difference between certain item types. This also means that it doesn't matter much if the recorded time is always a bit skewed in a direction (e.g. always 3 ms before the actual appearance of the element) as long as this skew is consistent for each display, because only the difference really matters. A 1-2 ms precision would be the ideal, but anything that mitigates the random "refresh rate noise" (0-17 ms) would be nice.
I also gave a try to jQuery.show() callback, but it does not take refresh rate into account: https://jsfiddle.net/gasparl/k5nx7zvh/67/
var r_start;
function shown() {
r_start = performance.now();
}
function item_display() {
var before = performance.now();
$("#stim_id").show(complete = shown())
var after = performance.now();
var text = "before: " + before + "<br>callback RT: " + r_start + "<br>after: " + after + "<br>before vs. callback: " + (r_start - before) + "<br>before vs. after: " + (after - r_start)
console.log("")
console.log(text)
$("p").html(text);
setTimeout(function(){ $("#stim_id").hide(); }, 500);
}
setInterval(item_display, Math.floor(Math.random() * (1000 - 500 + 1)) + 800);
With HTML:
<p><br><br><br><br><br></p>
<span id="stim_id">STIMULUS</span>
The solution (based on Kaiido's answer) along with working display example:
function monkeyPatchRequestPostAnimationFrame() {
const channel = new MessageChannel();
const callbacks = [];
let timestamp = 0;
let called = false;
channel.port2.onmessage = e => {
called = false;
const toCall = callbacks.slice();
callbacks.length = 0;
toCall.forEach(fn => {
try {
fn(timestamp);
} catch (e) {}
});
};
window.requestPostAnimationFrame = function(callback) {
if (typeof callback !== 'function') {
throw new TypeError('Argument 1 is not callable');
}
callbacks.push(callback);
if (!called) {
requestAnimationFrame((time) => {
timestamp = time;
channel.port1.postMessage('');
});
called = true;
}
};
}
if (typeof requestPostAnimationFrame !== 'function') {
monkeyPatchRequestPostAnimationFrame();
}
function chromeWorkaroundLoop() {
if (needed) {
requestAnimationFrame(chromeWorkaroundLoop);
}
}
// here is how I display items
// includes a 100 ms "warm-up"
function item_display() {
window.needed = true;
chromeWorkaroundLoop();
setTimeout(function() {
var before = performance.now();
$("#stim_id").text("Random new text: " + Math.round(Math.random()*1000) + ".");
$("#stim_id").show();
// I ask for display above, and get display time below
requestPostAnimationFrame(function() {
var rPAF_now = performance.now();
console.log("before vs. rPAF now:", rPAF_now - before);
console.log("");
needed = false;
});
}, 100);
}
// below is just running example instances of displaying stuff
function example_loop(count) {
$("#stim_id").hide();
setTimeout(function() {
item_display();
if (count > 1) {
example_loop(--count);
}
}, Math.floor(Math.random() * (1000 - 500 + 1)) + 500);
}
example_loop(10);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.3/jquery.min.js"></script>
<div id="stim_id">Any text</div>
EDIT: So, based on empirical measurements, out of all this, it turns out that all that matters is the rAF loop. The rPAF makes no real difference.
What you are experiencing is a Chrome bug (and even two).
Basically, when the pool of requestAnimationFrame callbacks is empty, they'll call it directly at the end of the current event loop, without waiting for the actual painting frame as the specs require.
To workaround this bug, you can keep an ever-going requestAnimationFrame loop, but beware this will mark your document as "animated" and will trigger a bunch of side-effects on your page (like forcing a repaint at every screen refresh). So I'm not sure what you are doing, but it's generally not a great idea to do this, and I would rather invite you to run this animation loop only when required.
let needed = true; // set to false when you don't need the rAF loop anymore
function item_display() {
var before = performance.now();
requestAnimationFrame(function(timest) {
var r_start = performance.now();
var r_ts = timest;
console.log("before:", before);
console.log("RAF callback start:", r_start);
console.log("RAF stamp:", r_ts);
console.log("before vs. RAF callback start:", r_start - before);
console.log("before vs. RAF stamp:", r_ts - before);
console.log("")
setTimeout(item_display, Math.floor(Math.random() * (1000 - 500 + 1)) + 500);
});
}
chromeWorkaroundLoop();
item_display();
function chromeWorkaroundLoop() {
if (needed) {
requestAnimationFrame(chromeWorkaroundLoop);
}
};
Now, requestAnimationFrame callbacks fire before the next paint (actually in the same event loop), and the TimeStamp argument should represent the time after all main tasks and microtasks of the current frame were executed, before it's starts its "update the rendering" sub-task (step 9 here).
[edit]: However it's not really what browsers implement, see this Q/A for more details.
So it's not the most precise you can have, and you are right that using performance.now() inside this callback should get you closer to the actual painting time.
Moreover when Chrome faces yet an other bug here, probably related to the first one, when they do set this rAF timeStamp to... I must admit I don't know what... maybe the previous painting frame's timeStamp.
(function() {
let raf_id,
eventLoopReport = {
id: 0,
timeStamp: 0,
now: 0
},
report = {
nb_of_loops_between_call_and_start: -1,
mouseClick_timeStamp: 0,
calling_task: {
eventLoop: null,
now: 0
},
rAF_task: {
eventLoop: null,
now: 0,
timeStamp: 0
}
};
startEventLoopCounter();
btn.onclick = triggerSingleFrame;
// increments eventLoop_id at every event loop
// (or at least every time our postMessage loop fires)
function startEventLoopCounter() {
const channel = new MessageChannel()
channel.port2.onmessage = e => {
eventLoopReport.id ++;
eventLoopReport.timeStamp = e.timeStamp;
eventLoopReport.now = performance.now();
channel.port1.postMessage('*');
};
channel.port1.postMessage('*');
}
function triggerSingleFrame(e) {
// mouseClick Event should be generated at least the previous event loop, so its timeStamp should be in the past
report.mouseClick_timeStamp = e.timeStamp;
const report_calling = report.calling_task;
report_calling.now = performance.now();
report_calling.eventLoop = Object.assign({}, eventLoopReport);
cancelAnimationFrame(raf_id);
raf_id = requestAnimationFrame((raf_ts) => {
const report_rAF = report.rAF_task;
report_rAF.now = performance.now();
report_rAF.timeStamp = raf_ts;
report_rAF.eventLoop = Object.assign({}, eventLoopReport);
report.nb_of_loops_between_call_and_start = report_rAF.eventLoop.id - report_calling.eventLoop.id;
// this should always be positive
report_el.textContent = "rAF.timeStamp - mouse_click.timeStamp: " +
(report.rAF_task.timeStamp - report.mouseClick_timeStamp) + '\n\n' +
// verbose
JSON.stringify(report, null, 2) ;
});
}
})();
<button id="btn">flash</button>
<div id="out"></div>
<pre id="report_el"></pre>
Once again, running an infinite rAF loop will fix this weird bug.
So one thing you might want to check is the maybe incoming requestPostAnimationFrame method.
You can access it in Chrome,1 after you enable "Experimental Web Platform features" in chrome:flags. This method if accepted by html standards will allow us to fire callbacks immediately after the paint operation occurred.
From there, you should be at the closest of the painting.
var needed = true;
function item_display() {
var before = performance.now();
requestAnimationFrame(function() {
requestPostAnimationFrame(function() {
var rPAF_now = performance.now();
console.log("before vs. rPAF now:", rPAF_now - before);
console.log("");
setTimeout(item_display, Math.floor(Math.random() * (1000 - 500 + 1)) + 500);
});
});
}
if (typeof requestPostAnimationFrame === 'function') {
chromeWorkaroundLoop();
item_display();
} else {
console.error("Your browser doesn't support 'requestPostAnimationFrame' method, be sure you enabled 'Experimental Web Platform features' in chrome:flags");
}
function chromeWorkaroundLoop() {
if (needed) {
requestAnimationFrame(chromeWorkaroundLoop);
}
};
And for the browsers that do not yet implement this proposal, or if this proposal never does it through the specs, you can try to polyfill it using a MessageEvent, which should be the first thing to fire at the next event loop.
// polyfills requestPostAnimationFrame
// requestPostAnimationFrame polyfill
if (typeof requestPostAnimationFrame !== "function") {
(() => {
const channel = new MessageChannel();
const callbacks = [];
let timestamp = 0;
let called = false;
let scheduled = false; // to make it work from rAF
let inRAF = false; // to make it work from rAF
channel.port2.onmessage = e => {
called = false;
const toCall = callbacks.slice();
callbacks.length = 0;
toCall.forEach(fn => {
try {
fn(timestamp);
} catch (e) {}
});
}
// We need to overwrite rAF to let us know we are inside an rAF callback
// as to avoid scheduling yet an other rAF, which would be one painting frame late
// We could have hooked an infinite loop on rAF, but this means
// forcing the document to be animated all the time
// which is bad for perfs
const rAF = globalThis.requestAnimationFrame;
globalThis.requestAnimationFrame = function(...args) {
if (!scheduled) {
scheduled = true;
rAF.call(globalThis, (time) => inRAF = time);
globalThis.requestPostAnimationFrame(() => {
scheduled = false;
inRAF = false;
});
}
rAF.apply(globalThis, args);
};
globalThis.requestPostAnimationFrame = function(callback) {
if (typeof callback !== "function") {
throw new TypeError("Argument 1 is not callable");
}
callbacks.push(callback);
if (!called) {
if (inRAF) {
timestamp = inRAF;
channel.port1.postMessage("");
} else {
requestAnimationFrame((time) => {
timestamp = time;
channel.port1.postMessage("");
});
}
called = true;
}
};
})();
}
var needed = true;
function item_display() {
var before = performance.now();
requestPostAnimationFrame(function() {
var rPAF_now = performance.now();
console.log("before vs. rPAF now:", rPAF_now - before);
console.log("");
setTimeout(item_display, Math.floor(Math.random() * (1000 - 500 + 1)) + 500);
});
}
chromeWorkaroundLoop();
item_display();
function chromeWorkaroundLoop() {
if (needed) {
requestAnimationFrame(chromeWorkaroundLoop);
}
};
Turns out this feature has apparently been removed from Chrome experiments. Looking at the implementation issue I can't find why, when, nor if they plan to still work on it.

Execution order bahaviour

I'm writing a function that upon call should populate the page with tiles. Data on tiles are acquired from the remote DB (hence the AJAX request). I'm also using jQuery 3.0 in the code.
Here is the function:
function populateDashboard() {
var tileBlueprint = '<div class="dashboard_tile">\
<div class="dashboard_tile_content">\
<table class="tile_table">\
<tr class="tile_title">\
<td>$title</td>\
</tr>\
<tr class="tile_data">\
<td>$value</td>\
</tr>\
</table>\
</div>\
</div>';
$.ajax({
url: 'http://' + AppVar.ServerUrl + '/restapi/Dashboard_GetData',
type: 'POST',
data: JSON.stringify({
'SessionId': AppVar.SessionId
}),
dataType: 'json',
contentType: "application/json",
success: function (data) {
if (data.ResultCode === '0') {
//current tiles get wiped
$('.dashboard_tile').fadeOut(100, function () {
$(".tile_handler").empty();
console.log("1 - " + Date.now())
});
//new tiles are parsed and data is injected into the string which represents the tile
//tiles are saved into an array
var json = $.parseJSON(data.TileData);
var tileArr = [];
$.each(json.tiles, function (index, element) {
tile = tileBlueprint.replace("$title", $.i18n("dashboard-" + element.title));
tile = tile.replace("$value", element.value);
tileArr[index] = tile;
console.log("2 - " + Date.now())
});
//now I loop trough the previously created array to populate the page
$.each(tileArr, function (index, element) {
setTimeout(function () {
$(element).hide().appendTo(".tile_handler").fadeIn(1000);
}, 1000 * index); //delay made longer to see the effect better
console.log("3 - " + Date.now())
});
} else {
ons.notification.alert($.i18n('error-retriving_data_problem'));
return;
}
},
error: function (request, error) {
ons.notification.alert($.i18n('error-conn_error'));
return;
}
});
}
I don't think the HTML where this is getting injected is relevat as that part is working fine.
The problem is that the fadeout and both each loops that get called upon success, get callout out of order. I tried to log the time at which each get executed and this is what I get:
//first run
2 - 1469707068268 (6 times)
3 - 1469707068269 (6 times)
//second run
2 - 1469707181179 (2 times)
2 - 1469707181180 (3 times)
2 - 1469707181181
3 - 1469707181181
3 - 1469707181182 (4 times)
3 - 1469707181183
1 - 1469707181283
1 - 1469707181284 (2 times)
1 - 1469707181285 (2 times)
1 - 1469707181286
I'm displaying 6 tiles, so comments 2 and 3 should fire 6 times and 1 only once.
Why doesn't 1 execute first?
Why is 1 executed 6 times? EDIT: Figured that one myself just now.
If 1 is executed last, why doesn't it delete all the tiles created before?
Another problem is that the first time it displays 6 tiles, but second (and onwards), it only displays 5 tiles (first is missing).
Anyone can help me explain what is going on and how can I avoid such bahaviour?
Thank you.
Why doesn't 1 execute first and why is 1 executed 6 times?
From the documentation for .fadeOut, the second parameter is "A function to call once the animation is complete, called once per matched element".
So in this case the function will be called after ~100ms (the delay you provide as the first parameter) and will be called six times (once for each matched element).
If 1 is executed last, why doesn't it delete all the tiles created before?
As seen above, 1 is run after 100ms. However, the actual nodes are added after 1000 * index ms:
setTimeout(function () {
$(element).hide().appendTo(".tile_handler").fadeIn(1000);
}, 1000 * index);
So for all but the first node the code actually appending the node is run after 1. However, for the first node (note: index of 0 => 1000 * 0 = 0ms delay), the appendTo code is run directly which means that it will in fact be removed when the .empty() is called after 100ms, which means you will only see 5 of the 6 nodes.
The solution to these problems is to somehow "synchronize" the code so that it runs in the way you expect it to. This is generally what callbacks are used for, you put the code you want to run after something is completed into the callback function. In this case, one solution could be to move the "adding" code into the fadeOut callback:
$('.dashboard_tile').fadeOut(100).promise().done(function () {
$(".tile_handler").empty();
var json = $.parseJSON(data.TileData);
var tileArr = [];
$.each(json.tiles, function (index, element) {
tile = tileBlueprint.replace("$title", $.i18n("dashboard-" + element.title));
tile = tile.replace("$value", element.value);
tileArr[index] = tile;
});
// ...
});
Notice the usage of .promise.done, which gives us a single callback once all elements have finished animating instead of one for each element.
I see multiple issues with your code so here is what I can recommend:
data: JSON.stringify({
'SessionId': AppVar.SessionId
}),
should just be
data: {'SessionId': AppVar.SessionId},
because jQuery's AJAX function will convert it for you.
Try console.log(data.TileData);; if you are already receiving a JS object/array then there is ZERO reason to call var json = $.parseJSON(data.TileData); so you should remove that.
Instead of
$.each(json.tiles, function (index, element) {`
use
$.each(data.TileData.tiles, function (index, element) {
Now for the final issue, fadeOut() and fadeIn() getting called out of order.
Try this:
// Make sure the fadeOut() finishes before calling more stuff!!!
//current tiles get wiped
$('.dashboard_tile').fadeOut(100, function () {
$(".tile_handler").empty();
console.log("1 - " + Date.now())
//new tiles are parsed and data is injected into the string which represents the tile
//tiles are saved into an array
var tileArr = [];
$.each(data.TileData.tiles, function (index, element) {
tile = tileBlueprint.replace("$title", $.i18n("dashboard-" + element.title));
tile = tile.replace("$value", element.value);
tileArr[index] = tile;
console.log("2 - " + Date.now())
});
//now I loop trough the previously created array to populate the page
$.each(tileArr, function (index, element) {
setTimeout(function () {
$(element).hide().appendTo(".tile_handler").fadeIn(1000);
}, 1000 * index); //delay made longer to see the effect better
console.log("3 - " + Date.now())
});
});

Scraping dynamically rendered links from an infinite scrollbar in CasperJS

I'm trying to scrape the links on the left sidebar of this page using CasperJS.
The page has hundreds of links in the sidebar, but only loads 20 at a time when you scroll down. This code successfully grabs the first 20 (needs casperjs and phantomjs globally installed to run):
var casper = require('casper').create();
// helper function that gets all of the resume links on the page:
var getAllLinks = function() {
var linksOnThisPage = []
$("a[href^='/ResumeB']").each(function(index, linkDiv) {
$linkDiv = $(linkDiv)
linksOnThisPage.push('http://www.super-resume.com' + $linkDiv.attr('href'))
});
return linksOnThisPage
};
//start casper, go to page, run helper function:
casper.start('http://www.super-resume.com/ResumeBuilder.jtp?query=Database+Administrator', function() {
allLinks=casper.evaluate(getAllLinks)
console.log('number of links found:', allLinks.length);
});
casper.run();
I can make the page scroll down in the actual browser with this:
$('#search-left-inner').scrollTop(10000);
10000 is an arbitrarily big number; every time you run that code in the browser, it loads 20 more links. (Ideally I'd like to be able to grab all at once without having to keep reloading 20 at a time, but that's less pressing for now.)
If I put that line inside the getAllLinks function like so:
var getAllLinks = function() {
$('#search-left-inner').scrollTop(10000);
var linksOnThisPage = []
//etc, etc,
it still only loads 20 links. Many similar posts discuss synchronicity issues, so I've I've tried to get the it to wait for the sidebar to finish loading a few ways, including this:
var getAllLinks = function() {
casper.then(function () {
$('#search-left-inner').scrollTop(100000);
});
casper.then(function () {
var linksOnThisPage = []
//etc. etc.
}
but now for some reason it only finds one link instead of 20.
I presume that if you scroll, it doesn't immediately load the next items, because loading takes time. You need to wait a little after scrolling before you can attempt to scrape all of the elements again.
casper.start(url)
.thenEvaluate(scroll)
.wait(5000, function(){
var links = this.evaluate(getAllLinks);
this.echo(links.length);
})
.run();
If this produces more links, then you can try the next step and that is infinite scrolling until no new elements are loaded. This can be done with asynchronous recursion in CasperJS:
var linkCount = -1;
function getAllLinks() {
var linksOnThisPage = []
$("a[href^='/ResumeB']").each(function(index, linkDiv) {
$linkDiv = $(linkDiv)
linksOnThisPage.push('http://www.super-resume.com' + $linkDiv.attr('href'))
});
return linksOnThisPage
}
function scroll() {
$('#search-left-inner').scrollTop(10000);
}
/**
* Returns true if more elements were loaded that were before
*/
function checkMore(){
var newLinks = this.evaluate(getAllLinks);
var newCount = newLinks.length;
if (linkCount === -1) {
linkCount = newCount;
}
return linkCount < newCount
}
/**
* Executes the a single iteration step and waits for a change in numbers.
* Terminates if there are no changes in 6 seconds.
*/
function step(){
this.thenEvaluate(scroll)
.waitFor(check, step, function _onTimeout(){
var links = this.evaluate(getAllLinks);
this.echo("finished with " + links.length + " links\n\n"+links.join("\n"));
}, 6000);
}
casper.start(url, step).run();
Keep in mind that it makes only sense to use jQuery in the DOM context (page context) which is inside of casper.evaluate(). I suggest that you also read the PhantomJS documentation of that function.

How can I run a function after another is done in D3.js?

I am using d3.js to draw several bars. After the drawing is done, I want to change the color of selected bars, but it always fails. It seems drawBars() and updateColor() are called at the same time.
function redraw(data){
drawBars(data);
updateColor();
}
How can I ensure drawBars() is done before the updateColor() is called?
At first, I added the update function code at the end of drawBar, it didn't work. Later, I moved it to end of the redraw function, it didn't work either.
This is specific code:
function drawBar(drawData){
var w = 1060,h = 600;
d3.selectAll("svg").remove();
var svg = d3.select("#chart").append("svg").attr("width",w).attr("height",h);
uni.forEach(function(element,index,array){
$('.uni').css("top",function(index){
return (index + 1) * 18 + 28;
});
});
$(".rankingheader").css("display","block");
$("#switch input").prop("checked", false);
starData = drawData;
revCountData = drawData;
sentData = drawData;
bizCountData = drawData;
drawAxis(drawData);
drawStar(starData);
drawRev(revCountData);
drawSent(sentData);
drawBiz(bizCountData);
drawLineA(starData);
drawLineB(starData,revCountData);
drawLineC(revCountData,sentData);
drawLineD(sentData,bizCountData);
// cart is a list which stored the id selected by user
if(cart.length > 0){
cart.forEach(function(element,index,array){
d3.select("#starHoverLine" + element).attr("visibility","visible");
d3.select("#starHintText" + element).attr("visibility","visible");
d3.select("#revHoverLine" + element).attr("visibility","visible");
d3.select("#revHintText" + element).attr("visibility","visible");
d3.select("#sentHoverLine" + element).attr("visibility","visible");
d3.select("#sentHintText" + element).attr("visibility","visible");
d3.select("#bizHoverLine" + element).attr("visibility","visible");
d3.select("#bizHintText" + element).attr("visibility","visible");
d3.select("#lineA" + element).style("stroke","red");
d3.select("#lineB" + element).style("stroke","red");
d3.select("#lineC" + element).style("stroke","red");
d3.select("#lineD" + element).style("stroke","red");
d3.select("#starBar" + element).attr("fill","red");
d3.select("#revBar" + element).attr("fill","red");
d3.select("#sentBar" + element).attr("fill","red");
d3.select("#bizBar" + element).attr("fill","red");
});
}
}
Your two functions are not called simultaneously; they're called one after another. If it appears that they're being called simultaneously, one of two things is happening:
drawBars is starting something that finishes asynchronously (like an ajax request or an animation), or
drawBars is making changes that aren't shown by the browser immediately and you want to yield back to the browser briefly to allow it to show (render) those changes before calling updateColor.
If it's #1, then you'll need to look at the documentation for the asynchronous operation that drawBars is starting to find out how to know when it finishes. Usually this is a callback or a promise. Depending on which it is, you'll either need to pass updateColors into drawBars as an argument (so you can then call it from the asynchronous callback), or have drawBars return the promise and use the promise's then method to add updateColors to the queue of functions to call when the promise is fulfilled.
If it's #2, that's much simpler:
setTimeout(updateColors, 100); // Wait 100ms (1/10th second) and call `updateColors`

Categories

Resources