I'm trying to test a page, which otherwise uses massive asynchronously loaded JavaScript resources, with PHP script that uses Mink and Zombie. Unfortunately, this process fails (leaving behind hanging processes). I have managed finally to simulate this error on a minimal example, which I'll post here.
The first page - the page to be tested - is basically a self-contained file, that simulates the AJAX calls by calling itself:
test_JSload.php
<?php
if (array_key_exists("QUERY_STRING", $_SERVER)) {
if ($_SERVER["QUERY_STRING"] == "getone") {
echo "<!doctype html>
<html>
<head>
<script src='test_JSload.php?gettwo'></script>
</head>
</html>
";
exit;
}
if ($_SERVER["QUERY_STRING"] == "gettwo") {
header('Content-Type: application/javascript');
echo "
function person(firstName) {
this.firstName = firstName;
this.changeName = function (name) {
this.firstName = name;
};
}
";
exit;
}
}
?>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<style type="text/css">
.my_btn { background-color:yellow; }
</style>
<script src="http://code.jquery.com/jquery-1.12.4.min.js"></script>
<script type="text/javascript">
var thishref = window.location.href.slice(0, window.location.href.indexOf('?')+1);
var qstr = window.location.href.slice(window.location.href.indexOf('?')+1);
function OnGetdata(inbtn) {
console.log("OnGetdata; loading ?getone via AJAX call");
//~ $.ajax(thishref + "?getone", { // works
var ptest = {}; // init as empty object
console.log(" ptest pre ajax is ", ptest);
$.ajax({url: thishref + "?getone",
async: true, // still "Synchronous XMLHttpRequest on the main thread is deprecated", because we load a script; https://stackoverflow.com/q/24639335
success: function(data) {
console.log("got getone data "); //, data);
$("#dataholder").html(data);
ptest = new person("AHA");
console.log(" ptest post getone is ", ptest);
},
error: function(xhr, ajaxOptions, thrownError) {
console.log("getone error " + thishref + " : " + xhr.status + " / " + thrownError);
}
});
ptest.changeName("Somename");
console.log(" ptest post ajax is ", ptest);
}
ondocready = function() {
$("#getdatabtn").click(function(){
OnGetdata(this);
});
}
$(document).ready(ondocready);
</script>
</head>
<body>
<h1>Hello World!</h1>
<button type="button" id="getdatabtn" class="my_btn">Get Data!</button>
<div id="dataholder"></div>
</body>
</html>
So, when the page first loads, there's no action; and as soon as the button is pressed: first some HTML content, which contains <script>
, is returned (this is ?getone
) through an AJAX call; then this script itself loads "automatically" from ?gettwo
. I'm aware this is probably not the right thing to do (see JavaScript console.log causes error: "Synchronous XMLHttpRequest on the main thread is deprecated..."), but there is a similar organization in the actual page I'm trying to test.
To run this page, you can just have the command-line php
version > 5.3, and run in the same directory of the file:
php -S localhost:8080
... and then, in a browser, go to http://127.0.0.1:8080/test_JSload.php
.
In any case, once I click the button, Firefox console shows:
OnGetdata; loading ?getone via AJAX call test_JSload.php:13:3
ptest pre ajax is Object { } test_JSload.php:16:3
TypeError: ptest.changeName is not a function test_JSload.php:31:3
got getone data test_JSload.php:21:7
Synchronous XMLHttpRequest on the main thread is deprecated because of its detrimental effects to the end user's experience. For more help http://xhr.spec.whatwg.org/ jquery-1.12.4.min.js:4:26272
ptest post getone is Object { firstName: "AHA", changeName: person/this.changeName(name) } test_JSload.php:24:7
Btw, the error 'TypeError: ... is not a function
' is the exact same I get from Mink/Zombie with the actual page I'm trying to inspect; although I don't think the page itself exhibits that error when called standalone, as it does here.
Now, let's try inspect this page with Mink (install as on nodejs cannot find module 'zombie' with PHP mink):
test_JSload_mink.php
<?php
$nodeModPath = "/home/USERNAME/.nvm/versions/node/v4.0.0/lib/node_modules";
# composer autoload:
require_once __DIR__ . '/vendor/autoload.php';
$zsrv = new \Behat\Mink\Driver\NodeJS\Server\ZombieServer();
$zsrv->setNodeModulesPath($nodeModPath . "/"); # needs to end with a trailing '/'
$driver = new \Behat\Mink\Driver\ZombieDriver( $zsrv );
$session = new \Behat\Mink\Session($driver);
// start the session
$session->start();
$session->visit("http://127.0.0.1:8080/test_JSload.php");
$session->wait(20000, '(browser.statusCode > 0)'); ### THIS makes things work?!
$statcode = $session->getStatusCode();
echo " current URL: " . $session->getCurrentUrl() ."\n";
echo " status code: " . $statcode ."\n";
$page = $session->getPage();
$el_button = $page->findButton('getdatabtn');
echo "Check: el_b " . gettype($el_button) . "\n";
echo " pressing/clicking the button\n";
$el_button->click();
echo "Page URL after click: ". $session->getCurrentUrl() . "\n";
?>
Running this from another terminal shell (while the first one still runs the php server process that serves test_JSload.php
) results with:
$ php test_JSload_mink.php
current URL: http://127.0.0.1:8080/test_JSload.php
status code: 200
Check: el_b object
pressing/clicking the button
PHP Fatal error: Uncaught exception 'Behat\Mink\Exception\DriverException' with message 'Error while processing event 'click': "ReferenceError: person is not defined\n at Object.OnGetdata.$.ajax.success (http://127.0.0.1:8080/test_JSload.php:script:16:19)\n at i (http://code.jquery.com/jquery-1.12.4.min.js:2:27449)\n at Object.j.fireWith [as resolveWith] (http://code.jquery.com/jquery-1.12.4.min.js:2:28213)\n at y (http://code.jquery.com/jquery-1.12.4.min.js:4:22721)\n at XMLHttpRequest.c (http://code.jquery.com/jquery-1.12.4.min.js:4:26925)\n at callListeners (/home/USERNAME/.nvm/versions/node/v4.0.0/lib/node_modules/zombie/node_modules/jsdom/lib/jsdom/events/EventTarget.js:170:34)\n at dispatchPhase (/home/USERNAME/.nvm/versions/node/v4.0.0/lib/node_modules/zombie/node_modules/jsdom/lib/jsdom/events/EventTarget.js:159:7)\n at XMLHttpRequest.EventTarget.dispatchEvent (/home/USERNAME/.nvm/versions/node/v4.0.0/lib/node_modules/zombie/node_modules/jsdom/lib/jsdom/events/EventTarget.js:115: in /media/Data1/work/bbs/gits/econdk-vis-01_git/test/test_php_mink/vendor/behat/mink-zombie-driver/src/ZombieDriver.php on line 880
Now, here the error from Mink/Zombie is 'ReferenceError: person is not defined
', whereas in my actual page inspection, it is 'TypeError: ... is not a function
' - but I think they are similar enough.
The question is - how can I handle errors like this?
For instance, here there is already '$session->wait(20000, '(browser.statusCode > 0)');
' used, which checks for browser.statusCode
inside JavaScript; I'm aware there is also:
$returnstring = $session->evaluateScript('document.readyState');
... which can also ask for data from JavaScript. So something like this - in principle - could be used to "wait" until given resources are loaded.
But the problem here is that the ptest
variable, which exposes the problem, is in the local scope of function OnGetdata()
, and so I have no idea how to construct a statement that would "check" it from PHP - where I expect only global variables like browser
and document
to be accessible.
So, does anyone now how could I have a Mink/Zombie PHP script "wait" for such "nested" JavaScript loading - so I could complete the loading of the page, after the virtual click, without an error?
EDIT: Managed to reduce this problem to a plain Zombie JavaScript file:
test_JSload_zombie.js
// call with:
// DEBUG=zombie NODE_PATH=/home/USERNAME/.nvm/versions/node/v4.0.0/lib/node_modules node test_JSload_zombie.js
var Browser = require("zombie");
browser = new Browser({waitDuration: 30*1000, runScripts: true});
browser.visit("http://127.0.0.1:8080/test_JSload.php", function () {
browser.pressButton('Get Data!'); //, done);
console.log(' person, post-click pre-wait:', browser.evaluate('typeof(person)'), browser.evaluate('$("script").length') ); // $("script").length is 2
var checkCondition = function () {
var ret =browser.evaluate("(typeof(person) != 'undefined')");
console.log("ret is " + ret);
return ret;
};
browser.wait({function: checkCondition, duration: 100000}, function() {
console.log(' person, post-wait:', browser.evaluate('typeof(person)'), browser.evaluate('$("script").length')); // $("script").length is 3
/// browser.dump(); // not too much info
});
});
Running this file, logs the exact same error:
zombie Opened window http://127.0.0.1:8080/test_JSload.php +0ms
zombie GET http://127.0.0.1:8080/test_JSload.php => 200 +198ms
zombie Loaded document http://127.0.0.1:8080/test_JSload.php +233ms
zombie GET http://code.jquery.com/jquery-1.12.4.min.js => 200 +207ms
zombie Event loop is empty +476ms
OnGetdata; loading ?getone via AJAX call
ptest pre ajax is Object {}
zombie XHR readystatechange http://127.0.0.1:8080/test_JSload.php?getone +54ms
zombie XHR loadstart http://127.0.0.1:8080/test_JSload.php?getone +3ms
zombie TypeError: ptest.changeName is not a function
at OnGetdata (http://127.0.0.1:8080/test_JSload.php:script:24:9)
at null.<anonymous> (http://127.0.0.1:8080/test_JSload.php:script:30:5)
at n.event.dispatch (http://code.jquery.com/jquery-1.12.4.min.js:3:12444)
at r.handle (http://code.jquery.com/jquery-1.12.4.min.js:3:9173)
at callListeners (/home/USERNAME/.nvm/versions/node/v4.0.0/lib/node_modules/zombie/node_modules/jsdom/lib/jsdom/events/EventTarget.js:170:34)
at dispatchPhase (/home/USERNAME/.nvm/versions/node/v4.0.0/lib/node_modules/zombie/node_modules/jsdom/lib/jsdom/events/EventTarget.js:159:7)
at EventTarget.dispatchEvent (/home/USERNAME/.nvm/versions/node/v4.0.0/lib/node_modules/zombie/node_modules/jsdom/lib/jsdom/events/EventTarget.js:115:3)
at DOM.EventTarget.dispatchEvent (/home/USERNAME/.nvm/versions/node/v4.0.0/lib/node_modules/zombie/lib/dom/jsdom_patches.js:155:31)
at define.proto.dispatchEvent (/home/USERNAME/.nvm/versions/node/v4.0.0/lib/node_modules/zombie/node_modules/jsdom/lib/jsdom/level2/html.js:365:55)
at Browser.fire (/home/USERNAME/.nvm/versions/node/v4.0.0/lib/node_modules/zombie/lib/index.js:424:14)
in http://127.0.0.1:8080/test_JSload.php +24ms
person, post-click pre-wait: undefined 2
ret is false
zombie GET http://127.0.0.1:8080/test_JSload.php?getone => 200 +36ms
zombie XHR readystatechange http://127.0.0.1:8080/test_JSload.php?getone +7ms
zombie XHR readystatechange http://127.0.0.1:8080/test_JSload.php?getone +0ms
ret is false
got getone data
zombie ReferenceError: person is not defined
at Object.OnGetdata.$.ajax.success (http://127.0.0.1:8080/test_JSload.php:script:16:19)
at i (http://code.jquery.com/jquery-1.12.4.min.js:2:27449)
at Object.j.fireWith [as resolveWith] (http://code.jquery.com/jquery-1.12.4.min.js:2:28213)
at y (http://code.jquery.com/jquery-1.12.4.min.js:4:22721)
at XMLHttpRequest.c (http://code.jquery.com/jquery-1.12.4.min.js:4:26925)
at callListeners (/home/USERNAME/.nvm/versions/node/v4.0.0/lib/node_modules/zombie/node_modules/jsdom/lib/jsdom/events/EventTarget.js:170:34)
at dispatchPhase (/home/USERNAME/.nvm/versions/node/v4.0.0/lib/node_modules/zombie/node_modules/jsdom/lib/jsdom/events/EventTarget.js:159:7)
at XMLHttpRequest.EventTarget.dispatchEvent (/home/USERNAME/.nvm/versions/node/v4.0.0/lib/node_modules/zombie/node_modules/jsdom/lib/jsdom/events/EventTarget.js:115:3)
at XMLHttpRequest.DOM.EventTarget.dispatchEvent (/home/USERNAME/.nvm/versions/node/v4.0.0/lib/node_modules/zombie/lib/dom/jsdom_patches.js:155:31)
at XMLHttpRequest._fire (/home/USERNAME/.nvm/versions/node/v4.0.0/lib/node_modules/zombie/lib/xhr.js:242:12)
in http://127.0.0.1:8080/test_JSload.php +73ms
person, post-wait: undefined 3
zombie XHR readystatechange http://127.0.0.1:8080/test_JSload.php?getone +4ms
zombie XHR progress http://127.0.0.1:8080/test_JSload.php?getone +0ms
zombie XHR load http://127.0.0.1:8080/test_JSload.php?getone +1ms
zombie XHR loadend http://127.0.0.1:8080/test_JSload.php?getone +0ms
zombie GET http://127.0.0.1:8080/test_JSload.php?gettwo => 200 +18ms
So, I guess if this gets solved on a Zombie/JS level, then it can eventually be solved on a Mink/PHP level... Note however, that the call to ?gettwo
happens only after the error has been emitted (and is the final entry in the log) - it seems that Zombie simply cannot wait for synchronous requests..
Try this too:
Or try other wait condition, for example a do while to wait for an object, also please not that findButton may return null if the element is not found/loaded so you can try a do-while for some seconds and if button !== null then break/return the button.
I always use wait for element that returns the object or throws an exception and click, for example something like:
If you need an example of waitForElement please let me know.
Here are some ajax conditions that you can try how-to-make-behat-wait-for-an-ajax-call