[MERGE] Backport of chrome headless support

For unknown [1] reason, phantomjs starts to segfault when running qunit
tests.
Be radical with simply not using phantomjs anymore.

[1] Any guidance on tracking the root cause may help...
This commit is contained in:
Christophe Simonis
2018-10-18 14:26:26 +02:00
3 changed files with 432 additions and 292 deletions
@@ -437,7 +437,8 @@ return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
console.log(log);
});
console.log(document.body.outerHTML);
console.log("error " + error); // phantomJS wait for message starting by error
console.error(error); // will be displayed as error info
console.log("error"); // phantomJS wait for message starting by error to stop
} else {
console.log(_.str.sprintf("Tour %s succeeded", tour_name));
console.log("ok"); // phantomJS wait for exact message "ok"
+430 -146
View File
@@ -4,38 +4,49 @@ The module :mod:`odoo.tests.common` provides unittest test cases and a few
helpers and classes to write tests.
"""
import base64
import collections
import errno
import glob
import importlib
import inspect
import itertools
import json
import logging
import operator
import os
import platform
import re
import select
import requests
import shutil
import signal
import subprocess
import tempfile
import threading
import time
import unittest
import werkzeug.urls
from contextlib import contextmanager
from datetime import datetime, timedelta, date
from pprint import pformat
from datetime import datetime, date
import requests
from decorator import decorator
from lxml import etree, html
from odoo.models import BaseModel
from odoo.osv.expression import normalize_domain
from odoo.tools import pycompat
from odoo.tools.misc import find_in_path
from odoo.tools.safe_eval import safe_eval
try:
from itertools import zip_longest as izip_longest
except ImportError:
from itertools import izip_longest
try:
import websocket
except ImportError:
# chrome headless tests will be skipped
websocket = None
try:
from xmlrpc import client as xmlrpclib
except ImportError:
@@ -326,10 +337,350 @@ class SavepointCase(SingleTransactionCase):
super(SavepointCase, self).tearDown()
class ChromeBrowser():
""" Helper object to control a Chrome headless process. """
def __init__(self, logger):
self._logger = logger
if websocket is None:
self._logger.warning("websocket-client module is not installed")
raise unittest.SkipTest("websocket-client module is not installed")
self.devtools_port = PORT + 2
self.ws_url = '' # WebSocketUrl
self.ws = None # websocket
self.request_id = 0
self.user_data_dir = tempfile.mkdtemp(suffix='_chrome_odoo')
self.chrome_process = None
self.screencast_frames = []
self._chrome_start()
self._find_websocket()
self._logger.info('Websocket url found: %s', self.ws_url)
self._open_websocket()
self._logger.info('Enable chrome headless console log notification')
self._websocket_send('Runtime.enable')
self._logger.info('Chrome headless enable page notifications')
self._websocket_send('Page.enable')
self.sigxcpu_handler = None
if os.name == 'posix':
self.sigxcpu_handler = signal.getsignal(signal.SIGXCPU)
signal.signal(signal.SIGXCPU, self.signal_handler)
def signal_handler(self, sig, frame):
if sig == signal.SIGXCPU:
_logger.info('CPU time limit reached, stopping Chrome and shutting down')
self.stop()
os._exit(0)
def stop(self):
if self.chrome_process is not None:
self._logger.info("Closing chrome headless with pid %s", self.chrome_process.pid)
self._websocket_send('Browser.close')
if self.chrome_process.poll() is None:
self._logger.info("Terminating chrome headless with pid %s", self.chrome_process.pid)
self.chrome_process.terminate()
self.chrome_process.wait()
if self.user_data_dir and os.path.isdir(self.user_data_dir) and self.user_data_dir != '/':
self._logger.info('Removing chrome user profile "%s"', self.user_data_dir)
shutil.rmtree(self.user_data_dir)
# Restore previous signal handler
if self.sigxcpu_handler and os.name == 'posix':
signal.signal(signal.SIGXCPU, self.sigxcpu_handler)
@property
def executable(self):
system = platform.system()
if system == 'Linux':
for bin_ in ['google-chrome', 'chromium', 'chromium-browser']:
try:
return find_in_path(bin_)
except IOError:
continue
elif system == 'Darwin':
bins = [
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
]
for bin_ in bins:
if os.path.exists(bin_):
return bin_
elif system == 'Windows':
# TODO: handle windows platform: https://stackoverflow.com/a/40674915
pass
raise unittest.SkipTest("Chrome executable not found")
def _chrome_start(self):
if self.chrome_process is not None:
return
switches = {
'--headless': '',
'--enable-logging': 'stderr',
'--no-default-browser-check': '',
'--no-first-run': '',
'--disable-extensions': '',
'--user-data-dir': self.user_data_dir,
'--disable-translate': '',
'--window-size': '1366x768',
'--remote-debugging-port': str(self.devtools_port)
}
cmd = [self.executable]
cmd += ['%s=%s' % (k, v) if v else k for k, v in switches.items()]
url = 'about:blank'
cmd.append(url)
self._logger.info('chrome_run executing %s', ' '.join(cmd))
try:
self.chrome_process = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except OSError:
raise unittest.SkipTest("%s not found" % cmd[0])
self._logger.info('Chrome pid: %s', self.chrome_process.pid)
def _find_websocket(self):
version = self._json_command('version')
self._logger.info('Browser version: %s', version['Browser'])
try:
infos = self._json_command('')[0] # Infos about the first tab
except IndexError:
self._logger.warning('No tab found in Chrome')
self.stop()
raise unittest.SkipTest('No tab found in Chrome')
self.ws_url = infos['webSocketDebuggerUrl']
self._logger.info('Chrome headless temporary user profile dir: %s', self.user_data_dir)
def _json_command(self, command, timeout=3):
"""
Inspect dev tools with get
Available commands:
'' : return list of tabs with their id
list (or json/): list tabs
new : open a new tab
activate/ + an id: activate a tab
close/ + and id: close a tab
version : get chrome and dev tools version
protocol : get the full protocol
"""
self._logger.info("Issuing json command %s", command)
command = os.path.join('json', command).strip('/')
while timeout > 0:
try:
url = werkzeug.urls.url_join('http://127.0.0.1:%s/' % self.devtools_port, command)
self._logger.info('Url : %s', url)
r = requests.get(url, timeout=3)
if r.ok:
return r.json()
return {'status_code': r.status_code}
except requests.ConnectionError:
time.sleep(0.1)
timeout -= 0.1
except requests.exceptions.ReadTimeout:
break
self._logger.error('Could not connect to chrome debugger')
raise unittest.SkipTest("Cannot connect to chrome headless")
def _open_websocket(self):
self.ws = websocket.create_connection(self.ws_url)
if self.ws.getstatus() != 101:
raise unittest.SkipTest("Cannot connect to chrome dev tools")
self.ws.settimeout(0.01)
def _websocket_send(self, method, params=None):
"""
send chrome devtools protocol commands through websocket
"""
sent_id = self.request_id
payload = {
'method': method,
'id': sent_id,
}
if params:
payload.update({'params': params})
self.ws.send(json.dumps(payload))
self.request_id += 1
return sent_id
def _websocket_wait_id(self, awaited_id, timeout=10):
"""
blocking wait for a certain id in a response
warning other messages are discarded
"""
start_time = time.time()
while time.time() - start_time < timeout:
try:
res = json.loads(self.ws.recv())
except websocket.WebSocketTimeoutException:
res = None
if res and res.get('id') == awaited_id:
return res
self._logger.info('timeout exceeded while waiting for id : %d', awaited_id)
return {}
def _websocket_wait_event(self, method, params=None, timeout=10):
"""
blocking wait for a particular event method and eventually a dict of params
"""
start_time = time.time()
while time.time() - start_time < timeout:
try:
res = json.loads(self.ws.recv())
except websocket.WebSocketTimeoutException:
res = None
if res and res.get('method', '') == method:
if params:
if set(params).issubset(set(res.get('params', {}))):
return res
else:
return res
elif res:
self._logger.debug('chrome devtools protocol event: %s', res)
self._logger.info('timeout exceeded while waiting for : %s', method)
def _get_shotname(self, prefix, ext):
""" return a unique filename for screenshot or screencast"""
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S_%f')
base_file = os.path.splitext(odoo.tools.config['logfile'])[0]
name = '%s_%s_%s.%s' % (base_file, prefix, timestamp, ext)
return name
def take_screenshot(self, prefix='failed'):
if not odoo.tools.config['logfile']:
self._logger.info('Screenshot disabled !')
return None
ss_id = self._websocket_send('Page.captureScreenshot')
self._logger.info('Asked for screenshot (id: %s)', ss_id)
res = self._websocket_wait_id(ss_id)
base_png = res.get('result', {}).get('data')
decoded = base64.decodebytes(bytes(base_png.encode('utf-8')))
outfile = self._get_shotname(prefix, 'png')
with open(outfile, 'wb') as f:
f.write(decoded)
self._logger.info('Screenshot in: %s', outfile)
def _save_screencast(self, prefix='failed'):
# could be encododed with something like that
# ffmpeg -framerate 3 -i frame_%05d.png output.mp4
if not odoo.tools.config['logfile']:
self._logger.info('Screencast disabled !')
return None
sdir = tempfile.mkdtemp(suffix='_chrome_odoo_screencast')
nb = 0
for frame in self.screencast_frames:
outfile = os.path.join(sdir, 'frame_%05d.png' % nb)
with open(outfile, 'wb') as f:
f.write(base64.decodebytes(bytes(frame.get('data').encode('utf-8'))))
nb += 1
framerate = int(nb / (self.screencast_frames[nb-1].get('metadata').get('timestamp') - self.screencast_frames[0].get('metadata').get('timestamp')))
outfile = self._get_shotname(prefix, 'mp4')
r = subprocess.run(['ffmpeg', '-framerate', str(framerate), '-i', '%s/frame_%%05d.png' % sdir, outfile])
shutil.rmtree(sdir)
if r.returncode == 0:
self._logger.info('Screencast in: %s', outfile)
def start_screencast(self):
self._websocket_send('Page.startScreencast', {'params': {'everyNthFrame': 5, 'maxWidth': 683, 'maxHeight': 384}})
def set_cookie(self, name, value, path, domain):
params = {'name': name, 'value': value, 'path': path, 'domain': domain}
self._websocket_send('Network.setCookie', params=params)
def _wait_ready(self, ready_code, timeout=60):
self._logger.info('Evaluate ready code "%s"', ready_code)
awaited_result = {'result': {'type': 'boolean', 'value': True}}
ready_id = self._websocket_send('Runtime.evaluate', params={'expression': ready_code})
last_bad_res = ''
start_time = time.time()
tdiff = time.time() - start_time
has_exceeded = False
while tdiff < timeout:
try:
res = json.loads(self.ws.recv())
except websocket.WebSocketTimeoutException:
res = None
if res and res.get('id') == ready_id:
if res.get('result') == awaited_result:
return True
else:
last_bad_res = res
ready_id = self._websocket_send('Runtime.evaluate', params={'expression': ready_code})
tdiff = time.time() - start_time
if tdiff >= 2 and not has_exceeded:
has_exceeded = True
self._logger.warning('The ready code takes too much time : %s', tdiff)
self.take_screenshot(prefix='failed_ready')
self._logger.info('Ready code last try result: %s', last_bad_res or res)
return False
def _wait_code_ok(self, code, timeout):
self._logger.info('Evaluate test code "%s"', code)
code_id = self._websocket_send('Runtime.evaluate', params={'expression': code})
start_time = time.time()
logged_error = False
while time.time() - start_time < timeout:
try:
res = json.loads(self.ws.recv())
except websocket.WebSocketTimeoutException:
res = None
if res and res.get('id', -1) == code_id:
self._logger.info('Code start result: %s', res)
if res.get('result', {}).get('result').get('subtype', '') == 'error':
self._logger.error("Running code returned an error")
return False
elif res and res.get('method') == 'Runtime.consoleAPICalled' and res.get('params', {}).get('type') in ('log', 'error'):
logs = res.get('params', {}).get('args')
log_type = res.get('params', {}).get('type')
content = " ".join([str(log.get('value', '')) for log in logs])
if log_type == 'error':
self._logger.error(content)
logged_error = True
else:
self._logger.info('console log: %s', content)
for log in logs:
if log.get('type', '') == 'string' and log.get('value', '').lower() == 'ok':
# it is possible that some tests returns ok while an error was shown in logs.
# since runbot should always be red in this case, better explicitly fail.
if logged_error:
return False
return True
elif log.get('type', '') == 'string' and log.get('value', '').lower().startswith('error'):
self.take_screenshot()
self._save_screencast()
return False
elif res and res.get('method') == 'Page.screencastFrame':
self.screencast_frames.append(res.get('params'))
elif res:
self._logger.debug('chrome devtools protocol event: %s', res)
self._logger.error('Script timeout exceeded : %s', (time.time() - start_time))
self.take_screenshot()
return False
def navigate_to(self, url, wait_stop=False):
self._logger.info('Navigating to: "%s"', url)
nav_id = self._websocket_send('Page.navigate', params={'url': url})
nav_result = self._websocket_wait_id(nav_id)
self._logger.info("Navigation result: %s", nav_result)
frame_id = nav_result.get('result', {}).get('frameId', '')
if wait_stop and frame_id:
self._logger.info('Waiting for frame "%s" to stop loading', frame_id)
self._websocket_wait_event('Page.frameStoppedLoading', params={'frameId': frame_id})
def clear(self):
self._websocket_send('Page.stopScreencast')
self.screencast_frames = []
self._websocket_send('Page.stopLoading')
self._logger.info('Deleting cookies and clearing local storage')
dc_id = self._websocket_send('Network.clearBrowserCookies')
self._websocket_wait_id(dc_id)
cl_id = self._websocket_send('Runtime.evaluate', params={'expression': 'localStorage.clear()'})
self._websocket_wait_id(cl_id)
self.navigate_to('about:blank', wait_stop=True)
class HttpCase(TransactionCase):
""" Transactional HTTP TestCase with url_open and phantomjs helpers.
""" Transactional HTTP TestCase with url_open and Chrome headless helpers.
"""
registry_test_mode = True
browser = None
def __init__(self, methodName='runTest'):
super(HttpCase, self).__init__(methodName)
@@ -338,9 +689,25 @@ class HttpCase(TransactionCase):
self.xmlrpc_common = xmlrpclib.ServerProxy(url_8 + 'common')
self.xmlrpc_db = xmlrpclib.ServerProxy(url_8 + 'db')
self.xmlrpc_object = xmlrpclib.ServerProxy(url_8 + 'object')
cls = type(self)
self._logger = logging.getLogger('%s.%s' % (cls.__module__, cls.__name__))
@classmethod
def start_browser(cls, logger):
# start browser on demand
if cls.browser is None:
cls.browser = ChromeBrowser(logger)
@classmethod
def tearDownClass(cls):
if cls.browser:
cls.browser.stop()
cls.browser = None
super(HttpCase, cls).tearDownClass()
def setUp(self):
super(HttpCase, self).setUp()
if self.registry_test_mode:
self.registry.enter_test_mode(self.cr)
self.addCleanup(self.registry.leave_test_mode)
@@ -360,6 +727,27 @@ class HttpCase(TransactionCase):
return self.opener.post(url, data=data, timeout=timeout)
return self.opener.get(url, timeout=timeout)
def _wait_remaining_requests(self):
t0 = int(time.time())
for thread in threading.enumerate():
if thread.name.startswith('odoo.service.http.request.'):
join_retry_count = 10
while thread.isAlive():
# Need a busyloop here as thread.join() masks signals
# and would prevent the forced shutdown.
thread.join(0.05)
join_retry_count -= 1
if join_retry_count < 0:
self._logger.warning("Stop waiting for thread %s handling request for url %s",
thread.name, getattr(thread, 'url', '<UNKNOWN>'))
break
time.sleep(0.5)
t1 = int(time.time())
if t0 != t1:
self._logger.info('remaining requests')
odoo.tools.misc.dumpstacks()
t0 = t1
def authenticate(self, user, password):
# stay non-authenticated
if user is None:
@@ -383,130 +771,11 @@ class HttpCase(TransactionCase):
session._fix_lang(session.context)
odoo.http.root.session_store.save(session)
if self.browser:
self._logger.info('Setting session cookie in browser')
self.browser.set_cookie('session_id', self.session_id, '/', '127.0.0.1')
def phantom_poll(self, phantom, timeout):
""" Phantomjs Test protocol.
Use console.log in phantomjs to output test results:
- for a success: console.log("ok")
- for an error: console.log("error")
Other lines are relayed to the test log.
"""
logger = _logger.getChild('phantomjs')
t0 = datetime.now()
td = timedelta(seconds=timeout)
buf = bytearray()
pid = phantom.stdout.fileno()
while True:
# timeout
self.assertLess(datetime.now() - t0, td,
"PhantomJS tests should take less than %s seconds" % timeout)
# read a byte
try:
ready, _, _ = select.select([pid], [], [], 0.5)
except select.error as e:
# In Python 2, select.error has no relation to IOError or
# OSError, and no errno/strerror/filename, only a pair of
# unnamed arguments (matching errno and strerror)
err, _ = e.args
if err == errno.EINTR:
continue
raise
if not ready:
continue
s = os.read(pid, 4096)
if not s:
self.fail("Ran out of data to read")
buf.extend(s)
# process lines
while b'\n' in buf and (not buf.startswith(b'<phantomLog>') or b'</phantomLog>' in buf):
if buf.startswith(b'<phantomLog>'):
line, buf = buf[12:].split(b'</phantomLog>\n', 1)
else:
line, buf = buf.split(b'\n', 1)
line = line.decode('utf-8')
lline = line.lower()
if lline.startswith(("error", "server application error")):
try:
# when errors occur the execution stack may be sent as a JSON
prefix = lline.index('error') + 6
self.fail(pformat(json.loads(line[prefix:])))
except ValueError:
self.fail(lline)
elif lline.startswith("warning"):
logger.warn(line)
else:
logger.info(line)
if line == "ok":
return True
def phantom_run(self, cmd, timeout):
_logger.info('phantom_run executing %s', ' '.join(cmd))
ls_glob = os.path.expanduser('~/.qws/share/data/Ofi Labs/PhantomJS/http_%s_%s.*' % (HOST, PORT))
ls_glob2 = os.path.expanduser('~/.local/share/Ofi Labs/PhantomJS/http_%s_%s.*' % (HOST, PORT))
for i in (glob.glob(ls_glob) + glob.glob(ls_glob2)):
_logger.info('phantomjs unlink localstorage %s', i)
os.unlink(i)
try:
phantom = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=None)
except OSError:
raise unittest.SkipTest("PhantomJS not found")
try:
result = self.phantom_poll(phantom, timeout)
self.assertTrue(
result,
"PhantomJS test completed without reporting success; "
"the log may contain errors or hints.")
finally:
# kill phantomjs if phantom.exit() wasn't called in the test
if phantom.poll() is None:
_logger.info("Terminating phantomjs")
phantom.terminate()
phantom.wait()
else:
# if we had to terminate phantomjs its return code is
# always -15 so we don't care
# check PhantomJS health
from signal import SIGSEGV
_logger.info("Phantom JS return code: %d" % phantom.returncode)
if phantom.returncode == -SIGSEGV:
_logger.error("Phantom JS has crashed (segmentation fault) during testing; log may not be relevant")
self._wait_remaining_requests()
def _wait_remaining_requests(self):
t0 = int(time.time())
for thread in threading.enumerate():
if thread.name.startswith('odoo.service.http.request.'):
join_retry_count = 10
while thread.isAlive():
# Need a busyloop here as thread.join() masks signals
# and would prevent the forced shutdown.
thread.join(0.05)
join_retry_count -= 1
if join_retry_count < 0:
_logger.warning("Stop waiting for thread %s handling request for url %s",
thread.name, thread.url)
break
time.sleep(0.5)
t1 = int(time.time())
if t0 != t1:
_logger.info('remaining requests')
odoo.tools.misc.dumpstacks()
t0 = t1
def phantom_js(self, url_path, code, ready="window", login=None, timeout=60, **kw):
def browser_js(self, url_path, code, ready='', login=None, timeout=60, **kw):
""" Test js code running in the browser
- optionnally log as 'login'
- load page given by url_path
@@ -521,23 +790,37 @@ class HttpCase(TransactionCase):
If neither are done before timeout test fails.
"""
options = {
'port': PORT,
'db': get_db_name(),
'url_path': url_path,
'code': code,
'ready': ready,
'timeout' : timeout,
'session_id': self.session_id,
}
options.update(kw)
# increase timeout if coverage is running
if any(f.filename.endswith('/coverage/execfile.py') for f in inspect.stack() if f.filename):
timeout = timeout * 1.5
self.start_browser(self._logger)
self.authenticate(login, login)
try:
self.authenticate(login, login)
url = "http://%s:%s%s" % (HOST, PORT, url_path or '/')
self._logger.info('Open "%s" in browser', url)
phantomtest = os.path.join(os.path.dirname(__file__), 'phantomtest.js')
cmd = ['phantomjs', phantomtest, json.dumps(options)]
self.phantom_run(cmd, timeout)
if odoo.tools.config['logfile']:
self._logger.info('Starting screen cast')
self.browser.start_screencast()
self.browser.navigate_to(url)
# Needed because tests like test01.js (qunit tests) are passing a ready
# code = ""
ready = ready or "document.readyState === 'complete'"
self.assertTrue(self.browser._wait_ready(ready), 'The ready "%s" code was always falsy' % ready)
if code:
message = 'The test code "%s" failed' % code
else:
message = "Some js test failed"
self.assertTrue(self.browser._wait_code_ok(code, timeout), message)
finally:
# clear browser to make it stop sending requests, in case we call
# the method several times in a test method
self.browser.clear()
self._wait_remaining_requests()
phantom_js = browser_js
def users(*logins):
""" Decorate a method to execute it once for each given user. """
@@ -1332,10 +1615,11 @@ class TagsSelector(object):
tags = getattr(arg, 'test_tags', set())
inter_no_test = self.exclude.intersection(tags)
if inter_no_test:
_logger.debug("Test '%s' not selected because of following tag(s): '%s'", arg, inter_no_test)
_logger.debug("Test '%s' not selected because it is tagged with : %s (exclusions: %s)", arg, inter_no_test, self.exclude)
return False
inter_to_test = self.include.intersection(tags)
if not inter_to_test:
_logger.debug("Test '%s' not selected because it was not tagged with '%s'", arg, self.include)
_logger.debug("Test '%s' not selected because it was not tagged with %s", arg, self.include)
return False
_logger.debug("Test '%s' selected: tagged with %s, exclusions: %s, inclusions: %s", arg, tags, self.exclude, self.include)
return True
-145
View File
@@ -1,145 +0,0 @@
// Phantomjs odoo helper
// jshint evil: true, loopfunc: true
var system = require('system');
var HOST = '127.0.0.1';
function waitFor (condition, callback, timeout, timeoutMessageCallback) {
timeout = timeout || 10000;
var start = new Date();
(function waitLoop() {
if(new Date() - start > timeout) {
console.log('error', timeoutMessageCallback ? timeoutMessageCallback() : "Timeout after "+timeout+" ms");
phantom.exit(1);
} else if (condition()) {
callback();
} else {
setTimeout(waitLoop, 250);
}
}());
}
function PhantomTest() {
var self = this;
this.options = JSON.parse(system.args[system.args.length-1]);
this.inject = this.options.inject || [];
this.timeout = this.options.timeout ? Math.round(parseFloat(this.options.timeout)*1000 - 5000) : 10000;
this.origin = 'http://' + HOST;
this.origin += this.options.port ? ':' + this.options.port : '';
// ----------------------------------------------------
// configure phantom and page
// ----------------------------------------------------
phantom.addCookie({
'domain': HOST,
'name': 'session_id',
'value': this.options.session_id,
});
this.page = require('webpage').create();
this.page.viewportSize = { width: 1366, height: 768 };
this.page.onError = function(message, trace) {
var msg = [message];
if (trace && trace.length) {
msg.push.apply(msg, trace.map(function (frame) {
var result = [' at ', frame.file, ':', frame.line];
if (frame.function) {
result.push(' (in ', frame.function, ')');
}
return result.join('');
}));
msg.push('(leaf frame on top)');
}
console.log('error', JSON.stringify(msg.join('\n')));
phantom.exit(1);
};
this.page.onAlert = function(message) {
console.log('error', message);
phantom.exit(1);
};
this.page.onConsoleMessage = function(message) {
console.log('<phantomLog>'+message+'</phantomLog>');
};
this.page.onLoadFinished = function(status) {
console.log('PhantomJS: onLoadFinished. Status=', status);
if (status === "success") {
for (var k in self.inject) {
var found = false;
var v = self.inject[k];
var need = v;
var src = v;
if (v[0]) {
need = v[0];
src = v[1];
found = self.page.evaluate(function(code) {
try {
return !!eval(code);
} catch (e) {
return false;
}
}, need);
}
if(!found) {
console.log('Injecting', src, 'needed for', need);
if(!self.page.injectJs(src)) {
console.log('error', "Cannot inject " + src);
phantom.exit(1);
}
}
}
}
};
setTimeout(function () {
self.page.evaluate(function () {
var message = ("Timeout\nhref: " + window.location.href +
"\nreferrer: " + document.referrer +
"\n\n" + (document.body && document.body.innerHTML)).replace(/[^a-z0-9\s~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, "*");
console.log('<phantomLog>error ' + message + '</phantomLog>');
});
phantom.exit(1);
}, self.timeout);
// ----------------------------------------------------
// run test
// ----------------------------------------------------
this.run = function(url_path, code, ready) {
var url = self.origin + url_path;
code = code || "true";
ready = ready || "true";
self.page.open(url, function(status) {
if (status !== 'success') {
console.log('error', "failed to load " + url);
phantom.exit(1);
} else {
console.log('loaded', url, status);
// clear localstorage leftovers
self.page.evaluate(function () { localStorage.clear() });
// process ready
waitFor(function() {
console.log("PhantomTest.run: wait for condition:", ready);
return self.page.evaluate(function (ready) {
var r = false;
try {
console.log("page.evaluate eval expr:", ready);
r = !!eval(ready);
} catch(ex) {
}
console.log("page.evaluate eval result:", r);
return r;
}, ready);
// run test
}, function() {
console.log("PhantomTest.run: condition statified, executing: " + code);
self.page.evaluate(function (code) { return eval(code); }, code);
console.log("PhantomTest.run: execution launched, waiting for console.log('ok')...");
});
}
});
};
}
// js mode or jsfile mode
if(system.args.length === 2) {
pt = new PhantomTest();
pt.run(pt.options.url_path, pt.options.code, pt.options.ready);
}