Test your websites with this simple GUI based scripted webtester. Generate simple testscripts directly from surfng on the webpage, enhance them with your commands, with variables, loops, checks, … and finally run automated web tests.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

3390 lines
116 KiB

/*! @file
@id $Id$
*/
// 1 2 3 4 5 6 7 8
// 45678901234567890123456789012345678901234567890123456789012345678901234567890
#ifndef COMMANDS_HXX
#define COMMANDS_HXX
#include <exceptions.hxx>
#include <webpage.hxx>
#include <QNetworkReply>
#include <QAuthenticator>
#include <QCoreApplication>
#include <QStringList>
#include <QWebFrame>
#include <QWebView>
#include <QWebElement>
#include <QPainter>
#include <QImage>
#include <QSslKey>
#include <QTimer>
#include <QProcess>
#include <QMouseEvent>
#include <QRegularExpression>
#include <QNetworkCookieJar>
#include <QNetworkCookie>
#include <QSet>
#include <vector>
#include <queue>
#include <map>
#include <memory>
#include <sstream>
#include <cassert>
#include <istream>
#include <ostream>
#include <xml-cxx/xml.hxx>
#include <mrw/stdext.hxx>
#ifdef HAVE_CXXABI_H
#include <cxxabi.h>
inline QString demangle(const char* mangled) {
int status;
std::unique_ptr<char[], void (*)(void*)> result(
abi::__cxa_demangle(mangled, 0, 0, &status), free);
return QString(result.get() ? result.get() : mangled);
}
#else
inline QString demangle(const char* mangled) {
return QString(mangled);
}
#endif
/* redefinition of ‘class std::optional<_Tp>’ */
/*
namespace std {
template<typename T> class optional {
private:
T* _opt;
bool _set;
public:
optional():
_opt(0), _set(false) {
}
optional(const T& other):
_opt(new T(other)), _set(true) {
}
~optional() {
if (_set) delete _opt;
}
optional& operator=(const T& other) {
if (_set) delete _opt;
_set = true;
_opt = new T(other);
return *this;
}
T* operator->() {
return _opt;
}
T& operator*() {
return *_opt;
}
const T* operator->() const {
return _opt;
}
const T& operator*() const {
return *_opt;
}
operator bool() const {
return _set;
}
};
}
*/
class Script;
class Command;
class Function;
class Logger {
public:
Logger(Command* command, Script* script, bool showLines = true);
void operator[](QString txt);
void operator()(QString txt);
~Logger();
private:
Command* _command;
Script* _script;
};
class Command: public QObject {
Q_OBJECT
public:
Command(): _log(true), _line(-1), _indent(0) {}
virtual ~Command() {}
virtual QString tag() const = 0;
virtual QString description() const = 0;
virtual QString command() const = 0;
virtual std::shared_ptr<Command> parse(Script*, QString,
QStringList&, QString, int, int) = 0;
virtual bool execute(Script*, QWebFrame*) = 0;
static void error(Logger& log, const Exception& e) {
log(QString(" FAILED[")+demangle(typeid(e).name())+"]: "+e.what());
throw e;
}
virtual int steps(Script*) {
return 1;
}
void line(int linenr) {
_line = linenr;
}
int line() const {
return _line;
}
void file(QString filename) {
_file = filename;
}
QString file() const {
return _file;
}
void indent(int i) {
_indent = i;
}
int indent() const {
return _indent;
}
bool log() {
return _log;
}
void log(bool l) {
_log = l;
}
QString result() {
return _result;
}
virtual bool isTestcase() {
return true;
}
static void realMouseClick(Logger& log, QWebFrame* frame,
Script* script, QString selector);
static void realMouseClick(const QWebElement& element) {
QWidget* web(element.webFrame()->page()->view());
QRect elGeom=element.geometry();
QPoint elPoint=elGeom.center();
int elX=elPoint.x();
int elY=elPoint.y();
int webWidth=web->width();
int webHeight=web->height();
int pixelsToScrolRight=0;
int pixelsToScrolDown=0;
if (elX>webWidth)
pixelsToScrolRight = //the +10 scrolls a bit further
elX-webWidth+elGeom.width()/2+10;
if (elY>webHeight)
pixelsToScrolDown = //the +10 scrolls a bit further
elY-webHeight+elGeom.height()/2+10;
element.webFrame()->setScrollBarValue(Qt::Horizontal, pixelsToScrolRight);
element.webFrame()->setScrollBarValue(Qt::Vertical, pixelsToScrolDown);
QPoint pointToClick(elX-pixelsToScrolRight, elY-pixelsToScrolDown);
QMouseEvent pressEvent(QMouseEvent::MouseButtonPress,
pointToClick, Qt::LeftButton, Qt::LeftButton,
Qt::NoModifier);
QCoreApplication::sendEvent(web, &pressEvent);
QMouseEvent releaseEvent(QMouseEvent::MouseButtonRelease,
pointToClick, Qt::LeftButton, Qt::LeftButton,
Qt::NoModifier);
QCoreApplication::sendEvent(web, &releaseEvent);
QCoreApplication::processEvents();
}
static void sleep(int s) {
QTime dieTime= QTime::currentTime().addSecs(s);
while (QTime::currentTime()<dieTime)
QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
}
protected:
std::shared_ptr<Script> subParser(Script* parent, const QStringList& in,
const QString& file, int line, int indent);
bool runScript(Logger& log, Command* parentCommand,
std::shared_ptr<Script> script,
Script* parent, QWebFrame* frame,
QStringList vars = QStringList(),
QStringList args = QStringList());
QStringList subCommandBlock(QStringList& in) {
QStringList commands;
std::string::size_type pos(std::string::npos);
while (in.size() && in[0].size() && in[0][0]==' '
&& (pos==std::string::npos ||
pos<=in[0].toStdString().find_first_not_of(' '))) {
if (pos==std::string::npos) pos=in[0].toStdString().find_first_not_of(' ');
commands += in.takeFirst().mid(pos);
}
return commands;
}
QStringList quotedStrings(QString value,
QString delimiter = " ",
bool keepDelimiters = false,
unsigned int max = 0) const {
QStringList res;
QString quot("'\"");
unsigned int found(0);
while (value=value.trimmed(), value.size()) {
QRegularExpression re;
int start(0);
if (quot.contains(value[0])) {
re = QRegularExpression(value[0]+" *((("+delimiter+") *)|$)");
start = 1;
} else {
re = QRegularExpression(" *((("+delimiter+") *)|$)");
}
int pos(value.indexOf(re, start));
if (pos<start) throw BadArgument("quote missmatch in "+value);
QRegularExpressionMatch m(re.match(value, start));
res += value.mid(start, m.capturedStart()-start);
value.remove(0, m.capturedEnd());
if (keepDelimiters && !m.captured(3).isEmpty()) res+=m.captured(3).trimmed();
if (++found==max) return value.isEmpty() ? res : res += value;
}
return res;
}
QStringList commaSeparatedList(QString value) const {
return quotedStrings(value, ",");
}
static QWebElement find(QWebFrame* frame, QString selector,
int repeat = 2, int sleepsec = 1) {
QWebElement element;
element = find1(frame, selector, repeat, sleepsec);
if (!element.isNull()) return element;
element = find1(frame->page()->currentFrame(), selector,
repeat, sleepsec);
if (!element.isNull()) return element;
element = find1(frame->page()->mainFrame(), selector,
repeat, sleepsec);
if (!element.isNull()) return element;
return element;
}
static QWebElement find1(QWebFrame* frame, QString selector,
int repeat = 2, int sleepsec = 1) {
QWebElement element;
if (repeat<1) repeat=1;
for (int i=0; i<repeat; ++i) {
element = frame->findFirstElement(selector);
if (!element.isNull()) return element;
for (QWebFrame* childFrame: frame->childFrames()) {
element = find1(childFrame, selector, 1, 0);
if (!element.isNull()) return element;
}
if (i+1<repeat && sleepsec) sleep(sleepsec);
}
return element;
}
void log(Script*);
bool _log;
protected:
QString _result;
private:
int _line;
QString _file;
int _indent;
};
class Empty: public Command {
public:
QString tag() const {
return "";
}
QString description() const {
return
""
"\n\n--\n\n"
"Empty lines are allowed";
}
QString command() const {
return tag();
}
std::shared_ptr<Command> parse(Script*, QString,
QStringList&, QString, int, int) {
std::shared_ptr<Empty> cmd(new Empty());
return cmd;
}
bool execute(Script*, QWebFrame*) {
return true;
}
virtual bool isTestcase() {
return false;
}
};
class Comment: public Command {
public:
Comment(QString comment): _comment(comment) {}
QString tag() const {
return "#";
}
QString description() const {
return
"# comment"
"\n\n--\n\n"
"Comments are lines that start with #";
}
QString command() const {
return _comment;
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList&, QString, int, int) {
std::shared_ptr<Comment> cmd(new Comment(args));
return cmd;
}
bool execute(Script* script, QWebFrame*) {
this->log(false);
Logger log(this, script);
this->log(true);
log(_comment);
this->log(false);
return true;
}
virtual bool isTestcase() {
return false;
}
private:
QString _comment;
};
class Screenshot: public Command {
public:
static QString sourceHtml(Logger& log, int line, QString target, QString base,
QString name, QWebFrame* frame) {
if (!QDir(target).exists() && !QDir().mkpath(target))
error(log, DirectoryCannotBeCreated(target));
QCoreApplication::processEvents();
QString filename(target+QDir::separator()+
QString("%4-%1-%2-%3.html")
.arg(base)
.arg(line, 4, 10, QChar('0'))
.arg(name)
.arg(QDateTime::currentDateTime()
.toString("yyyyMMddHHmmss")));
QFile file(filename);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
error(log, CannotWriteSouceHTML(filename));
QTextStream out(&file);
out<<frame->toHtml();
if (out.status()!=QTextStream::Ok) error(log, CannotWriteSouceHTML(filename));
return QDir(filename).absolutePath();
}
static QString screenshot(Logger& log, int line, QString target, QString base,
QString name, QWebFrame* frame) {
bool wasShown(frame->page()->view()->isVisible());
frame->page()->view()->show();
QCoreApplication::processEvents();
QImage image(frame->page()->view()->size(), QImage::Format_RGB32);
QPainter painter(&image);
frame->render(&painter);
painter.end();
if (!wasShown) frame->page()->view()->hide();
if (!QDir(target).exists() && !QDir().mkpath(target))
error(log, DirectoryCannotBeCreated(target));
QCoreApplication::processEvents();
QString filename(target+QDir::separator()+
QString("%4-%1-%2-%3.png")
.arg(base)
.arg(line, 4, 10, QChar('0'))
.arg(name)
.arg(QDateTime::currentDateTime()
.toString("yyyyMMddHHmmss")));
if (!image.save(filename)) error(log, CannotWriteScreenshot(filename));
return QDir(filename).absolutePath();
}
QString tag() const {
return "screenshot";
}
QString description() const {
return
tag()+" <filename-base>"
"\n\n--\n\n"
"Create a PNG screenshot of the actual web page and store it in the "
"file <filename-base>.png. If not already opened, a browser window "
"will pop up to take the screenshot.";
}
QString command() const {
return tag()+" "+_filename;
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList&, QString, int, int) {
std::shared_ptr<Screenshot> cmd(new Screenshot());
cmd->_filename = args;
return cmd;
}
bool execute(Script* script, QWebFrame* frame);
private:
QString _filename;
};
class RunDownload: public QObject {
Q_OBJECT
public:
RunDownload(QNetworkReply* reply, QString filename):
_reply(reply), _file(filename) {
assert(connect(_reply, SIGNAL(finished()), SLOT(finished())));
assert(connect(_reply, SIGNAL(downloadProgress(qint64, qint64)),
SLOT(downloadProgress(qint64, qint64))));
_file.open(QIODevice::WriteOnly);
}
~RunDownload() {
assert(disconnect(_reply, SIGNAL(finished()), this, SLOT(finished())));
assert(disconnect(_reply, SIGNAL(downloadProgress(qint64, qint64)),
this, SLOT(downloadProgress(qint64, qint64))));
delete _reply;
}
Q_SIGNALS:
void completed(bool netsuccess, bool filesuccess);
private Q_SLOTS:
void finished() {
_file.write(_reply->readAll());
_file.close();
completed(_reply->error()==QNetworkReply::NoError,
_file.error()==QFileDevice::NoError);
delete this;
}
void downloadProgress(qint64 bytes, qint64) {
_file.write(_reply->read(bytes));
}
private:
QNetworkReply* _reply;
QFile _file;
};
/** @mainpage
The WebTester web application testing framework consists of three parts:
-# a gui to create testcases, see @ref webtester
-# a command line tool to run testcases, see @ref webrunner
-# test scripts, see @ref testscript. */
class Script: public QObject {
Q_OBJECT
Q_SIGNALS:
void logging(QString);
void progress(QString, int, int, int);
public:
typedef std::map<QString, std::shared_ptr<Command>> Prototypes;
typedef std::pair<QString, QStringList> Signal;
enum ClickType {
REAL_MOUSE_CLICK,
JAVASCRIPT_CLICK
};
enum Formatting {
PLAIN,
HTML
};
/// QString that sorts first by string length, then by content
class LenString: public QString {
public:
LenString() {}
LenString(const QString& o): QString(o) {}
virtual bool operator<(const LenString& o) const {
return
size()<o.size() ? true : o.size()<size() ? false
: QString::operator<(o.toLatin1());
}
};
public:
static QString xmlattr(QString attr, bool br = false) {
if (br) return attr.toHtmlEscaped().replace("\n", "<br/>");
return attr.toHtmlEscaped();
}
static QString xmlstr(const std::string& attr) {
return xmlstr(QString::fromStdString(attr));
}
static QString xmlstr(QString attr) {
return attr
.replace("&gt;", ">").replace("&lt;", "<")
.replace("<br/>", "\n").replace("&quot;", "\"")
.replace("&nbsp;", " ").replace("&amp;", "&");
}
public:
Script():
_step(0), _clicktype(JAVASCRIPT_CLICK),
_screenshots(true),
_defaultTimeout(10),
_defaultTimeoutFalse(3) {
initPrototypes();
}
Script(const Script& o):
QObject(),
_prototypes(o._prototypes),
_step(0),
_script(o._script),
_screenshots(true) {
set(o);
}
QString syntax() const {
return
"Script syntax is a text file that consists of a list of commands. Each "
"command starts at the begin of a new line. Empty lines are allowed. "
"Lines that start with \"#\" are treated as comments."
"\n\n"
"Subcommands are indented. The first indented line defines the level of "
"indentation. All following lines must be indented at least by the same level."
"\n\n"
"Note: When a selector is required as parameter, then the selector "
"is a CSS selector."
"\n\n"
"Thanks to the filter script doxygen-webtester.sed, you can use the "
"comments for producing doxygen documenation. Just start comments with "
"\"##\" to import them to doxygen. This script is automatically configured, "
"when you use the autotools bootstrap from:\n"
"https://mrw.sh/development/bootstrap-build-environment";
}
/// set workdir
void path(QString path) {
_path = (path.size()?path:".")+QDir::separator();
}
/// get workdir
QString path() const {
return _path;
}
/// get all command prototypes
const Prototypes& prototypes() const {
return _prototypes;
}
QString commands(Formatting f = PLAIN) const {
QString cmds;
switch (f) {
case PLAIN: {
for (auto it(_prototypes.begin()); it!=_prototypes.end(); ++it)
cmds+="\n\n\nCOMMAND: "+it->first+"\n\n"+it->second->description();
} break;
case HTML: {
auto format = [](QString s) {
return s
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace(QRegularExpression("&lt;([-_A-Za-z0-9]+)&gt;"), "<i>\\1</i>")
.replace(QRegularExpression("(\n[^ ][^\n]*)(\n +-)"), "\\1<ul>\\2")
.replace(QRegularExpression("(\n +-[^\n]*\n)([^ ])"), "\\1</ul>\\2")
.replace(QRegularExpression("\n +- ([^\n]*)(</ul>)?"), "<li>\\1</li>\\2")
.replace("</li>\n", "</li>")
.replace("\n ", "\n&nbsp;&nbsp;")
.replace("\n\n", "</p><p>")
.replace("\n", "<br/>")
.replace(QRegularExpression("(http(s)?://[-/^a-z0-9.]+)"), "<a href=\"\\1\">\\1</a>");
};
cmds = "<style>div#table-of-contents {border: 2px solid black; background-color: red; width: 100%; column-width: 5em;}</style>"
"<h1>Contents</h1><ul>"
"<li id=\"top-syntax-description\"><a href=\"#syntax-description\">Syntax</a></li>"
"<li id=\"top-command-list\"><a href=\"#command-list\">Commands</a><ul>";
for (auto[name,command]: _prototypes)
cmds += "<li id=\"top-"+name+"\"><a href=\"#"+name+"\">"+name+"</a></li>";
cmds += "</ul></li></ul>"
"<h1 id=\"syntax-description\"><a href=\"#top-syntax-description\">Syntax</a></h1>"
+format(syntax())+
"<h1 id=\"command-list\"><a href=\"#top-command-list\">Commands</a></h1>";
for (auto[name,command]: _prototypes) {
QStringList doc(command->description().split("\n\n--\n\n"));
assert(doc.size()==2); // description does not match expected format
QString usage(doc.takeFirst());
QString description(doc.takeFirst());
cmds += "<h2 id=\""+name+"\"><a href=\"#top-"+name+"\">"+name+"</a></h2><h3>Usage</h3><p>"
+ format(usage)
+ "</p><h3>Description</h3><p>"
+ format(description)
+ "</p>";
}
} break;
}
return cmds.trimmed();
}
void reset() {
_script.clear();
while (!_signals.empty()) _signals.pop();
_timer.stop();
_ignores.clear();
_cout.clear();
_cerr.clear();
_ignoreSignalsUntil.clear();
}
void cleanup() {
reset();
_ignore.clear();
_variables.clear();
_rvariables.clear();
_timeout = _defaultTimeout;
_timeoutFalse = _defaultTimeoutFalse;
_step = 0;
_clicktype = JAVASCRIPT_CLICK;
}
std::shared_ptr<Command> parseLine(QStringList& in,
QString filename, int linenr,
int indent) try {
std::shared_ptr<Command> command;
if (!in.size()) throw MissingLine();
QString line(in.takeFirst().trimmed());
QString cmd(line), args;
int space(line.indexOf(' '));
if (space>0) {
cmd = line.left(space);
args = line.right(line.size()-space-1);
}
Prototypes::const_iterator it(_prototypes.find(cmd));
if (it!=_prototypes.end()) {
command = it->second->parse(this, args, in, filename, linenr, indent);
} else {
command = unknown(line);
}
command->file(filename);
command->line(linenr);
command->indent(indent);
return command;
} catch (Exception& e) {
e.prependFileLine(filename, linenr);
throw;
}
void parse(QStringList in, QString filename, int line = 1, int indent = 0) {
_filename = filename;
for (int linenr(0), oldsize(0);
oldsize=in.size(), in.size();
linenr+=oldsize-in.size())
_script.push_back(parseLine(in, filename, line+linenr, indent));
}
QStringList print() {
QStringList result;
for (auto cmd(_script.begin()); cmd!=_script.end(); ++cmd) {
result += (*cmd)->command();
}
return result;
}
bool run(QWebFrame* frame) {
return run(frame, _testsuites, targetdir(), _screenshots, _maxretries);
}
bool run(QWebFrame* frame, std::shared_ptr<xml::Node> testsuites,
QString td = QString(), bool screenshots = true,
int maxretries = 0) {
bool res(true);
_step = 0;
_testsuites = testsuites;
_timeout = _defaultTimeout;
_timeoutFalse = _defaultTimeoutFalse;
_ignoreSignalsUntil.clear();
addSignals(frame);
_screenshots = screenshots;
_maxretries = maxretries;
_timer.setSingleShot(true);
targetdir(!td.isEmpty()
? td
: _testsuites->children()
? xmlstr(_testsuites->last().attr("name"))
: "attachments");
if (!_testsuites->children()) {
xml::Node testsuite("testsuite");
testsuite.attr("name") = "Unnamed Test Suite";
(*_testsuites)<<testsuite;
}
int retries(0), back(0);
for (auto cmd(_script.begin()); cmd!=_script.end();
_step+=(*cmd)->steps(this), ++cmd) {
progress((*cmd)->file(), (*cmd)->line(), _step, countSteps());
xml::Node testcase("testcase");
try {
testcase.attr("classname") =
xmlattr(_testclass.size()
?_testclass
:"file."+(*cmd)->file()
.replace(QRegularExpression(".wt$"), "")
.replace(".", "-"), true)
.toStdString();
testcase.attr("name") =
xmlattr(QString("%1: %2")
.arg((*cmd)->line())
.arg((*cmd)->command().split('\n').takeFirst(), true))
.toStdString();
if (!_ignores.size() || (*cmd)->tag()=="label") { // not ignored
_timer.start(_timeout*1000);
try {
command(*cmd);
if (!(res=(*cmd)->execute(this, frame))) {
_timer.stop();
if (!back) retries = 0; else --back;
testcase<<(xml::String("system-out") =
xmlattr(_cout).toStdString());
testcase<<(xml::String("system-err") =
xmlattr(_cerr).toStdString());
_cout.clear();
_cerr.clear();
_testsuites->last()<<testcase;
break; // test is successfully finished
}
progress((*cmd)->file(), (*cmd)->line(), _step, countSteps());
} catch (PossibleRetryLoad& e) {
_timer.stop();
// timeout may happen during load due to bad internet connection
if (screenshots)
try { // take a screenshot on error
Logger log(0, this);
QString filename(Screenshot::screenshot
(log, (*cmd)->line(), targetdir(),
_testclass,
QString("retry-%1")
.arg((ulong)retries, 2, 10,
QLatin1Char('0')),
frame));
plainlog("[[ATTACHMENT|"+filename+"]]");
} catch (... ) {} // ignore exception in screenshot
if (++retries<=maxretries) { // retry in that case
QUrl url(frame->url());
if ((*cmd)->command()=="expect loadFinished true") {
------cmd;
------_step;
back += 3;
_ignoreSignalsUntil = "loadStarted";
frame->load(url);
} else if ((*cmd)->command()=="expect loadStarted") {
----cmd;
----_step;
back += 2;
_ignoreSignalsUntil = "loadStarted";
frame->page()->triggerAction(QWebPage::Stop);
} else if ((*cmd)->command().startsWith("expect urlChanged")) {
QString url2((*cmd)->command());
url2.remove("expect urlChanged");
if (url2.size()) url=url2.trimmed();
----cmd;
----_step;
back += 2;
_ignoreSignalsUntil = "loadStarted";
frame->load(url);
} else if ((*cmd)->command().startsWith("expect load")) {
QString url2((*cmd)->command());
url2.remove("expect load");
if (url2.size()) url=url2.trimmed();
----cmd;
----_step;
back += 2;
_ignoreSignalsUntil = "loadStarted";
frame->load(url);
} else {
throw;
}
} else {
throw;
}
log(QString("WARNING: retry#%1, redo last %2 steps; error: %3")
.arg(retries).arg(back).arg(e.what()));
}
_timer.stop();
if (!back) retries = 0; else --back;
testcase<<(xml::String("system-out") =
xmlattr(_cout).toStdString());
testcase<<(xml::String("system-err") =
xmlattr(_cerr).toStdString());
_cout.clear();
_cerr.clear();
if ((*cmd)->isTestcase())
_testsuites->last()<<testcase;
}
} catch (Exception& e) {
_timer.stop();
if (!back) retries = 0; else --back;
testcase<<(xml::String("system-out") =
xmlattr(_cout).toStdString());
testcase<<(xml::String("system-err") =
xmlattr(_cerr).toStdString());
_cout.clear();
_cerr.clear();
if ((*cmd)->isTestcase())
_testsuites->last()<<testcase;
removeSignals(frame);
e.prependFileLine((*cmd)->file(), (*cmd)->line());
if (screenshots)
try { // write html source and take a last screenshot on error
{
Logger log(0, this);
QString filename(Screenshot::sourceHtml
(log, (*cmd)->line(), targetdir(),
_testclass,
"error", frame));
plainlog("[[ATTACHMENT|"+filename+"]]");
} {
Logger log(0, this);
QString filename(Screenshot::screenshot
(log, (*cmd)->line(), targetdir(),
_testclass,
"error", frame));
plainlog("[[ATTACHMENT|"+filename+"]]");
}
} catch (... ) {} // ignore exception in screenshot
throw;
}
}
removeSignals(frame);
if (!_signals.empty()) error(UnhandledSignals(_signals));
progress(QString(), 0, 0, 0);
return res;
}
std::shared_ptr<Command> command() {
return _command;
}
void command(std::shared_ptr<Command> cmd) { // maintained by Logger
_command = cmd;
if (_parent) _parent->command(cmd);
}
void parent(Script* p) {
_parent = p;
}
QString& cout() {
return _cout;
}
QString& cerr() {
return _cerr;
}
int countSteps() {
int res(0);
for (auto cmd(_script.begin()); cmd!=_script.end(); ++cmd)
res += (*cmd)->steps(this);
return res;
}
bool screenshots() {
return _screenshots;
}
void targetdir(QString name) {
_targetdir = name;
}
QString targetdir() {
return _targetdir;
}
void testclass(QString tc) {
_testclass = tc;
}
QString testclass() {
return _testclass;
}
void testsuite(QString name) {
xml::Node testsuite("testsuite");
testsuite.attr("name") = xmlattr(name, true).toStdString();
testsuite.attr("timestamp") =
QDateTime::currentDateTime().toString(Qt::ISODate).toStdString();
_testsuites->last().attr("failures") = "0";
(*_testsuites)<<testsuite;
}
QStringList getSignals() {
QStringList res;
for (std::queue<Signal> sigs(_signals); !sigs.empty(); sigs.pop())
res+=sigs.front().first;
return res;
}
Signal getSignal() {
while (_signals.empty()) QCoreApplication::processEvents();
Signal res(_signals.front());
_signals.pop();
return res;
}
bool checkSignal(const QString& name, const QStringList& args = QStringList()) {
if (_ignore.contains(name)) {
log(QString("ignoring signal check for %1").arg(name));
return true;
}
while (_signals.empty()) QCoreApplication::processEvents();
Signal res(_signals.front());
if (res.first==name && (args.empty() || res.second==args)) {
_signals.pop();
return true;
}
return false;
}
QTimer& timer() {
return _timer;
}
void ignore(const Script& o) {
_ignore = o._ignore;
}
bool ignores(const QString& sig = QString()) {
if (sig.isEmpty()) return !_ignore.empty();
return _ignore.contains(sig);
}
void ignore(QStringList sigs = QStringList()) {
if (sigs.empty())
sigs<<"loadFinished"<<"loadStarted"<<"frameChanged"<<"titleChanged"<<"urlChanged";
for (const QString& sig: sigs) {
log("start ignoring: '"+sig+"'");
_ignore<<sig;
}
}
void unignore(QStringList sigs = QStringList()) {
if (sigs.empty())
sigs<<"loadFinished"<<"loadStarted"<<"frameChanged"<<"titleChanged"<<"urlChanged";
for (const QString& sig: sigs) {
if (_ignore.contains(sig)) {
log("stop ignoring: '"+sig+"'");
_ignore.erase(_ignore.find(sig));
}
}
}
void ignoreto(const QString& l) {
_ignores.insert(l);
}
void label(const QString& l) {
_ignores.remove(l);
}
void set(QString name, QString value) {
_variables[name] = value;
_rvariables[value] = name;
}
QStringList variables() {
return _variables.keys();
}
QString variable(QString name) {
QMap<QString, QString>::iterator it(_variables.find(name));
if (it==_variables.end()) error(VariableNotFound(name));
return *it;
}
/// Copy context from other script
void set(const Script& o) {
_variables = o._variables;
_rvariables = o._rvariables;
_timeout = o._timeout;
_timeoutFalse = o._timeoutFalse;
_defaultTimeout = o._timeout;
_defaultTimeoutFalse = o._timeoutFalse;
_clicktype = o._clicktype;
_testsuites = o._testsuites;
_testclass = o._testclass;
_targetdir = o._targetdir;
_ignore = o._ignore;
_ignores = o._ignores;
_maxretries = o._maxretries;
_screenshots = o._screenshots;
_cout.clear();
_cerr.clear();
_ignoreSignalsUntil.clear();
_functions.unite(o._functions);
}
void unset(QString name) {
_rvariables.remove(_variables[name]);
_variables.remove(name);
}
QStringList functions() {
return _functions.keys();
}
void function(QString name, std::shared_ptr<Function> f) {
_functions[name] = f;
}
std::shared_ptr<Function> function(QString name) {
QMap<QString, std::shared_ptr<Function> >::iterator
it(_functions.find(name));
if (it==_functions.end()) error(FunctionNotFound(name));
return *it;
}
void timeout(int t) {
_timeout = t;
}
int timeout() {
return _timeout;
}
void timeoutFalse(int t) {
_timeoutFalse = t;
}
int timeoutFalse() {
return _timeoutFalse;
}
void defaultTimeout(int t) {
_defaultTimeout = t;
}
void defaultTimeoutFalse(int t) {
_defaultTimeoutFalse = t;
}
void auth(const QString& realm, const QString& username, const QString& password) {
if (!username.isEmpty() && !password.isEmpty())
_auth[realm] = {username, password};
else if (_auth.contains(realm))
_auth.erase(_auth.find(realm));
}
void clicktype(ClickType c) {
_clicktype = c;
}
ClickType clicktype() {
return _clicktype;
}
QString replacevars(QString txt) {
for (QMap<QString, QString>::iterator it(_variables.begin());
it!=_variables.end(); ++it)
txt.replace(it.key(), it.value(), Qt::CaseSensitive);
return txt;
}
QStringList replacevars(QStringList txts) {
for (QStringList::iterator txt(txts.begin()); txt!=txts.end(); ++txt)
*txt = replacevars(*txt);
return txts;
}
QString insertvars(QString txt) {
QMapIterator<LenString, LenString> it(_rvariables);
it.toBack();
while (it.hasPrevious()) {
it.previous();
txt.replace(it.key(), it.value(), Qt::CaseSensitive);
}
return txt;
}
QString result() {
if (_script.size()) return (*_script.rbegin())->result();
return QString();
}
void addSignals(QWebFrame* frame) {
assert(connect(dynamic_cast<NetworkAccessManager*>
(frame->page()->networkAccessManager()),
SIGNAL(log(QString)),
SLOT(log(QString))));
assert(connect(frame->page()->networkAccessManager(),
SIGNAL(authenticationRequired(QNetworkReply*, QAuthenticator*)),
SLOT(authenticationRequired(QNetworkReply*, QAuthenticator*))));
assert(connect(frame, SIGNAL(contentsSizeChanged(const QSize&)),
SLOT(contentsSizeChanged(const QSize&))));
assert(connect(frame, SIGNAL(iconChanged()),
SLOT(iconChanged())));
assert(connect(frame, SIGNAL(initialLayoutCompleted()),
SLOT(initialLayoutCompleted())));
assert(connect(frame, SIGNAL(javaScriptWindowObjectCleared()),
SLOT(javaScriptWindowObjectCleared())));
assert(connect(frame, SIGNAL(loadFinished(bool)),
SLOT(loadFinished(bool))));
assert(connect(frame, SIGNAL(loadStarted()),
SLOT(loadStarted())));
assert(connect(frame, SIGNAL(titleChanged(const QString&)),
SLOT(titleChanged(const QString&))));
assert(connect(frame, SIGNAL(urlChanged(const QUrl&)),
SLOT(urlChanged(const QUrl&))));
assert(connect(&_timer, SIGNAL(timeout()), SLOT(timedout())));
}
void removeSignals(QWebFrame* frame) {
disconnect(dynamic_cast<NetworkAccessManager*>
(frame->page()->networkAccessManager()),
SIGNAL(log(QString)),
this, SLOT(log(QString)));
disconnect(frame->page()->networkAccessManager(),
SIGNAL(authenticationRequired(QNetworkReply*, QAuthenticator*)),
this, SLOT(authenticationRequired(QNetworkReply*, QAuthenticator*)));
disconnect(frame, SIGNAL(contentsSizeChanged(const QSize&)),
this, SLOT(contentsSizeChanged(const QSize&)));
disconnect(frame, SIGNAL(iconChanged()),
this, SLOT(iconChanged()));
disconnect(frame, SIGNAL(initialLayoutCompleted()),
this, SLOT(initialLayoutCompleted()));
disconnect(frame, SIGNAL(javaScriptWindowObjectCleared()),
this, SLOT(javaScriptWindowObjectCleared()));
disconnect(frame, SIGNAL(loadFinished(bool)),
this, SLOT(loadFinished(bool)));
disconnect(frame, SIGNAL(loadStarted()),
this, SLOT(loadStarted()));
disconnect(frame, SIGNAL(titleChanged(const QString&)),
this, SLOT(titleChanged(const QString&)));
disconnect(frame, SIGNAL(urlChanged(const QUrl&)),
this, SLOT(urlChanged(const QUrl&)));
disconnect(frame, SIGNAL(urlChanged(const QUrl&)),
this, SLOT(urlChanged(const QUrl&)));
disconnect(&_timer, SIGNAL(timeout()), this, SLOT(timedout()));
}
public Q_SLOTS:
void log(QString text, Command* command = 0) {
QString prefix
(QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss "));
Command* cmd(command?command:_command.get());
for (QChar& c: text) if (c<32&&c!='\n') c='?';
if (cmd)
prefix += QString("%2:%3%1 ")
.arg(QString(cmd->indent()+2, QChar(' ')))
.arg(cmd->file(), 20, QChar(' '))
.arg(cmd->line(), -4, 10, QChar(' '));
else
prefix += " .... ";
text = prefix+text.split('\n').join("\n"+prefix+" ");
logging(text);
std::cout<<text<<std::endl<<std::flush;
_cout += text + "\n";
}
void plainlog(QString text) {
logging(text);
std::cout<<text<<std::endl<<std::flush;
_cout += text + "\n";
}
void parentlog(QString text) {
logging(text);
_cout += text + "\n";
}
private:
void error(const Exception& e) {
log(QString(" FAILED[")+demangle(typeid(e).name())+"]: "+e.what());
throw e;
}
std::shared_ptr<Command> unknown(QString command) {
if (!command.size())
return std::shared_ptr<Command>(new Empty());
if (command[0]=='#')
return std::shared_ptr<Command>(new Comment(command));
throw UnknownCommand(command); // error
}
void initPrototypes();
void add(Command* c) {
_prototypes[c->tag()] = std::shared_ptr<Command>(c);
}
private Q_SLOTS:
void innerProgress(QString file, int line, int delta) {
progress(file, line, _step+delta, countSteps());
}
void authenticationRequired(QNetworkReply*, QAuthenticator* a) {
if (_auth.contains(a->realm())) {
log("network: login to "+a->realm());
a->setUser(_auth[a->realm()].username);
a->setPassword(_auth[a->realm()].password);
} else {
log("network: no credentials for "+a->realm());
}
}
void contentsSizeChanged(const QSize&) {
}
void iconChanged() {
}
void initialLayoutCompleted() {
}
void javaScriptWindowObjectCleared() {
}
void loadFinished(bool ok) {
if (_ignore.contains("loadFinished")) {
log("ignored loadFinished");
return;
}
QString sig(ok?"true":"false");
if (_ignoreSignalsUntil.size() &&
_ignoreSignalsUntil != "loadFinished "+sig) {
log("warning: ignored loadFinished, waiting for "+_ignoreSignalsUntil);
return;
}
_ignoreSignalsUntil.clear();
log(" signal received: loadFinished "+QString(ok?"true":"false"));
_signals.push(std::make_pair("loadFinished", QStringList(sig)));
}
void loadStarted() {
if (_ignore.contains("loadStarted")) {
log("ignored loadStarted");
return;
}
if (_ignoreSignalsUntil.size() && _ignoreSignalsUntil != "loadStarted") {
log("warning: ignored loadStarted, waiting for "+_ignoreSignalsUntil);
return;
}
_ignoreSignalsUntil.clear();
log(" signal received: loadStarted");
_signals.push(std::make_pair("loadStarted", QStringList()));
}
void frameChanged() {
if (_ignore.contains("frameChanged")) {
log("ignored frameChanged");
return;
}
}
void titleChanged(const QString&) {
if (_ignore.contains("titleChanged")) {
log("ignored titleChanged");
return;
}
//_signals.push(std::make_pair("titleChanged", QStringList(title)));
}
void urlChanged(const QUrl& url) {
if (_ignore.contains("urlChanged")) {
log("ignored urlChanged");
return;
}
if (_ignoreSignalsUntil.size() && _ignoreSignalsUntil != "urlChanged") {
log("warning: ignored urlChanged, waiting for "+_ignoreSignalsUntil);
return;
}
_ignoreSignalsUntil.clear();
log(" signal received: urlChanged "+url.toString());
_signals.push(std::make_pair("urlChanged",
QStringList(url.toString())));
}
void timedout() {
error(TimeOut(_command->command()));
}
private:
struct AuthRealm {
QString username;
QString password;
};
typedef std::vector<std::shared_ptr<Command>> Commands;
Prototypes _prototypes;
Commands _script;
std::queue<Signal> _signals;
QSet<QString> _ignore; ///< signals to ignore
QTimer _timer;
int _step;
QSet<QString> _ignores;
QString _cout;
QString _cerr;
bool _screenshots;
int _maxretries;
QString _ignoreSignalsUntil;
QMap<QString, QString> _variables; ///< variable mapping
QMap<LenString, LenString> _rvariables; ///< reverse variable mapping
QMap<QString, std::shared_ptr<Function> > _functions;
int _timeout;
int _timeoutFalse;
int _defaultTimeout;
int _defaultTimeoutFalse;
ClickType _clicktype;
QString _targetdir;
std::shared_ptr<xml::Node> _testsuites; ///< only valid within run
QString _testclass;
std::shared_ptr<Command> _command;
QString _path;
QString _filename;
QMap<QString, AuthRealm> _auth;
Script* _parent = nullptr;
};
class CommandContainer: public Command {
public:
int steps(Script* parent) {
return countSteps(parent);
}
protected:
virtual int countSteps(Script*) const {
return _script->countSteps()+1;
}
std::shared_ptr<Script> _script;
};
class Do: public Command {
public:
QString tag() const {
return "do";
}
QString description() const {
return
tag()+" [<selector>]\n <javascript-line1>\n <javascript-line2>"
"\n\n--\n\n"
"Execute JavaScript on a CSS selected object. The object is the first "
"object in the DOM tree that matches the given CSS selector. You can "
"refere to the selected object within the scripy by \"this\". The "
"JavaScript code is on the following lines and at least intended by "
"one space";
}
QString command() const {
return tag()+" "+_selector+(_javascript.size()?"\n"+_javascript:"");
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList& in, QString, int, int) {
std::shared_ptr<Do> cmd(new Do());
cmd->_selector = args.trimmed();
cmd->_javascript = subCommandBlock(in).join("\n");
return cmd;
}
bool execute(Script* script, QWebFrame* frame) {
Logger log(this, script);
QWebElement element(frame->documentElement());
if (_selector.size()) {
element = find(frame, script->replacevars(_selector), script->timeout()-1);
if (element.isNull())
error(log, ElementNotFound(script->replacevars(_selector)));
}
_result =
element.evaluateJavaScript(script->replacevars(_javascript)).toString();
log("result: "+(_result.size()?_result:"(void)"));
return true;
}
private:
QString _selector;
QString _javascript;
};
class Load: public Command {
public:
QString tag() const {
return "load";
}
QString description() const {
return
tag()+" <url>"
"\n\n--\n\n"
"Load an URL, the URL is given as parameter in full syntax.";
}
QString command() const {
return tag()+" "+_url;
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList&, QString, int, int) {
std::shared_ptr<Load> cmd(new Load());
cmd->_url = args;
return cmd;
}
bool execute(Script* script, QWebFrame* frame) {
Logger log(this, script);
frame->load(script->replacevars(_url));
return true;
}
private:
QString _url;
};
class Expect: public Command {
public:
QString tag() const {
return "expect";
}
QString description() const {
return
tag()+" <signal> [<parameter>]"
"\n\n--\n\n"
"Expect a signal. Signals are emitted by webkit and may contain "
"parameter. If a parameter is given in the script, then the parameter "
"must match exactly. If no parameter is given, then the signal must "
"be emitted, but the parameters of the signal are not checked."
"\n\n"
"Known signals and parameters are:\n"
" - loadFinished <bool: \"true\" if ok, \"false\" on error>\n"
" - loadStarted\n"
" - urlChanged <url>\n"
"There is a short hand:\n"
" - load <url>\n"
" stands for the three signals:\n"
" - loadStarted (optional)\n"
" - urlChanged <url>\n"
" - loadFinished true";
}
QString command() const {
return tag()+" "+_signal._signal
+(_signal._args.size()?" "+_signal._args.join(' '):QString());
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList&, QString, int, int) {
std::shared_ptr<Expect> cmd(new Expect());
cmd->_signal._args = args.split(" ");
cmd->_signal._signal = cmd->_signal._args.takeFirst();
return cmd;
}
bool execute(Script* script, QWebFrame*) {
Logger log(this, script);
QStringList args(script->replacevars(_signal._args));
if (_signal._signal=="load") { // special signal load
while (!script->ignores("loadStarted") && script->checkSignal("loadStarted")) {
log("ignore signal: loadStarted"); // ignore optional loadStarted
}
if (!script->checkSignal("urlChanged", args))
error(log, WrongSignal("urlChanged", args, script->getSignal(),
script->getSignals()));
args=QStringList("true");
if (!script->checkSignal("loadFinished", args))
error(log, WrongSignal("loadFinished", args, script->getSignal(),
script->getSignals()));
} else {
if (!script->checkSignal(_signal._signal, args))
error(log, WrongSignal(_signal._signal, args, script->getSignal(),
script->getSignals()));
}
return true;
}
private:
struct Signal {
Signal(QString s, QStringList a): _signal(s), _args(a) {}
Signal(QString s, QString a): _signal(s), _args(a) {}
Signal(QString s): _signal(s) {}
Signal() {}
QString _signal;
QStringList _args;
};
Signal _signal;
};
class Open: public Command {
public:
QString tag() const {
return "open";
}
QString description() const {
return
tag()+
"\n\n--\n\n"
"Open the browser window, so you can follow the test steps visually.";
}
QString command() const {
return tag();
}
std::shared_ptr<Command> parse(Script*, QString,
QStringList&, QString, int, int) {
std::shared_ptr<Open> cmd(new Open());
return cmd;
}
bool execute(Script* script, QWebFrame* frame) {
Logger log(this, script);
frame->page()->view()->show();
return true;
}
};
class Sleep: public Command {
public:
QString tag() const {
return "sleep";
}
QString description() const {
return
tag()+" <seconds>"
"\n\n--\n\n"
"Sleep for a certain amount of seconds. This helps, if you must wait "
"for some javascript actions, i.e. AJAX or slow pages, and the "
"excpeted signals are not sufficient.";
}
QString command() const {
return tag()+" "+_time;
}
std::shared_ptr<Command> parse(Script*, QString time,
QStringList&, QString, int, int) {
std::shared_ptr<Sleep> cmd(new Sleep());
cmd->_time = "10"; // default: 10s
if (time.size()) cmd->_time = time;
return cmd;
}
bool execute(Script* script, QWebFrame*) {
Logger log(this, script);
script->timer().stop();
bool ok;
unsigned int time(script->replacevars(_time).toUInt(&ok));
if (!ok)
error(log, BadArgument(script->replacevars(_time)
+" should be a number of seconds"));
sleep(time);
return true;
}
private:
QString _time;
};
class Exit: public Command {
public:
QString tag() const {
return "exit";
}
QString description() const {
return
tag()+
"\n\n--\n\n"
"Successfully terminate script immediately. The following commands "
"are not executed. This helps when you debug your scripts and you "
"want the script stop at a certain point for investigations.";
}
QString command() const {
return tag();
}
std::shared_ptr<Command> parse(Script*, QString,
QStringList&, QString, int, int) {
std::shared_ptr<Exit> cmd(new Exit());
return cmd;
}
bool execute(Script* script, QWebFrame*) {
Logger log(this, script);
return false;
}
};
class IgnoreTo: public Command {
public:
QString tag() const {
return "ignoreto";
}
QString description() const {
return
tag()+" <label>"
"\n\n--\n\n"
"Ignore all following commands up to a given label. The following "
"commands are not executed until the given label appears in the "
"script. This helps when you debug your scripts and you "
"want to skip some lines of script code.";
}
QString command() const {
return tag()+" "+_label;
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList&, QString, int, int) {
std::shared_ptr<IgnoreTo> cmd(new IgnoreTo());
if (!args.size()) throw BadArgument("ignoreto needs a label");
cmd->_label=args;
return cmd;
}
bool execute(Script* script, QWebFrame*) {
Logger log(this, script);
script->ignoreto(script->replacevars(_label));
return true;
}
private:
QString _label;
};
class Label: public Command {
public:
QString tag() const {
return "label";
}
QString description() const {
return
tag()+" <label>"
"\n\n--\n\n"
"This marks the label refered by command \"ignoreto\".";
}
QString command() const {
return tag()+" "+_label;
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList&, QString, int, int) {
std::shared_ptr<Label> cmd(new Label());
if (!args.size()) throw BadArgument("label needs a label as parameter");
cmd->_label=args;
return cmd;
}
bool execute(Script* script, QWebFrame*) {
Logger log(this, script);
script->label(script->replacevars(_label));
return true;
}
private:
QString _label;
};
class Upload: public Command {
public:
QString tag() const {
return "upload";
}
QString description() const {
return
tag()+" <selector> -> <filename>"
"\n\n--\n\n"
"Presses the specified file upload button and passes a given file "
"name. The command requires a CSS selector followed by a filename. "
"The first object that matches the selector is used.";
}
QString command() const {
return tag()+" "+_selector+" -> "+_filename;
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList&, QString, int, int) {
std::shared_ptr<Upload> cmd(new Upload());
QStringList allargs(args.split("->"));
if (allargs.size()<2)
throw BadArgument(tag()+"requires <selector> -> <filename>, "
"instead of: \""+args+"\"");
cmd->_selector = allargs.takeFirst().trimmed();
cmd->_filename = allargs.join("->").trimmed();
return cmd;
}
bool execute(Script* script, QWebFrame* frame) {
Logger log(this, script);
TestWebPage* page(dynamic_cast<TestWebPage*>(frame->page()));
assert(page);
QString filename(script->path()+script->replacevars(_filename));
if (!QFileInfo(filename).exists()) {
QStringList files(QFileInfo(filename).dir()
.entryList(QStringList(filename)));
if (files.size()==1) filename=files[0];
}
if (!QFileInfo(filename).exists()) filename=script->replacevars(_filename);
if (!QFileInfo(filename).exists()) {
QStringList files(QFileInfo(filename).dir()
.entryList(QStringList(filename)));
if (files.size()==1) filename=files[0];
}
page->setNextUploadFile(filename);
realMouseClick(log, frame, script, script->replacevars(_selector));
if (page->uploadPrepared())
error(log, SetFileUploadFailed(script->replacevars(_selector), filename));
return true;
}
private:
QString _selector;
QString _filename;
};
class Exists: public Command {
public:
QString tag() const {
return "exists";
}
QString description() const {
return
tag()+" <selector> -> <text>"
"\n\n--\n\n"
"Assert that a certain text exists in the selected object, or if no "
"text is given, assert that the specified object exists. The object "
"is given by a CSS selector. All matching objects are search for the "
"text.";
}
QString command() const {
return tag()+" "+_selector+(_text.size()?" -> "+_text:QString());
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList&, QString, int, int) {
std::shared_ptr<Exists> cmd(new Exists());
QStringList allargs(args.split("->"));
if (allargs.size()<2) {
cmd->_selector = args;
} else {
cmd->_selector = allargs.takeFirst().trimmed();
cmd->_text = allargs.join("->").trimmed();
}
return cmd;
}
bool execute(Script* script, QWebFrame* frame) {
Logger log(this, script);
QString selector(script->replacevars(_selector));
QString text(script->replacevars(_text));
QStringList notfound;
QWebElement firstelement(find(frame, selector, script->timeout()-1));
for (QWebElement element: frame->findAllElements(selector)) {
if (text.isEmpty()) return true; // just find element
if (element.toOuterXml().indexOf(text)!=-1) return true;
if (element.toPlainText().indexOf(text)!=-1) return true;
notfound += element.toOuterXml();
}
if (text.isEmpty())
error(log, AssertionFailed("element not found: "+selector));
else if (firstelement.isNull())
error(log, AssertionFailed("expected \""+text+"\" in non existing element "
+selector));
else
error(log, AssertionFailed("not found \""+text+"\" in \""
+notfound.join("\", \"")+"\" on "+selector));
return true; // never reached due to error, above
}
private:
QString _selector;
QString _text;
};
class Not: public Command {
public:
QString tag() const {
return "not";
}
QString description() const {
return
tag()+" <selector> -> <text>"
"\n\n--\n\n"
"Assert that a certain text does not exists in the selected object, "
"or if no text is given, assert that the specified object does not "
"exists. The object is given by a CSS selector. All matching objects "
"are search for the text.";
}
QString command() const {
return tag()+" "+_selector+(_text.size()?" -> "+_text:QString());
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList&, QString, int, int) {
std::shared_ptr<Not> cmd(new Not());
QStringList allargs(args.split("->"));
if (allargs.size()<2) {
cmd->_selector = args;
} else {
cmd->_selector = allargs.takeFirst().trimmed();
cmd->_text = allargs.join("->").trimmed();
}
return cmd;
}
bool execute(Script* script, QWebFrame* frame) {
Logger log(this, script);
QString selector(script->replacevars(_selector));
QString text(script->replacevars(_text));
QWebElement firstelement(find(frame, selector, script->timeoutFalse()-1));
for (QWebElement element: frame->findAllElements(selector)) {
if (text.isEmpty())
error(log, AssertionFailed("element must not exists: "+selector));
if (element.toOuterXml().indexOf(text)!=-1)
error(log, AssertionFailed("\""+text+"\" must not be in \""
+element.toInnerXml()+"\" on "+selector));
}
return true;
}
private:
QString _selector;
QString _text;
};
class Execute: public Command {
public:
QString tag() const {
return "execute";
}
QString description() const {
return
tag()+" <command>\n <line1>\n <line2>\n <...>"
"\n\n--\n\n"
"Execute <command>. The command can have space separated arguments. "
"Following lines that are intended by at least "
"one space are streamed into the command. This way, you can e.g. "
"execute a bash script to check a file you downloaded from a page.";
}
QString command() const {
QStringList script(_script);
script.replaceInStrings(QRegularExpression("^"), " ");
return tag()+" "+_command
+(_args.size()?" "+_args.join(' '):QString())
+(script.size()?"\n "+script.join("\n "):QString());
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList& in, QString, int, int) {
std::shared_ptr<Execute> cmd(new Execute());
cmd->_args = args.split(' ');
cmd->_command = cmd->_args.takeFirst();
cmd->_script = subCommandBlock(in);
return cmd;
}
bool execute(Script* script, QWebFrame*) {
Logger log(this, script);
script->timer().stop();
QString command(script->replacevars(_command));
QStringList args;
QString scripttxt(script->replacevars(_script.join("\n")));
for (QString arg: _args) args.push_back(script->replacevars(arg));
QProcess exec;
exec.setProcessChannelMode(QProcess::MergedChannels);
exec.start(command, args);
if (!exec.waitForStarted())
error(log, CannotStartScript(command, args, scripttxt));
if (scripttxt.size()) {
if (exec.write(scripttxt.toUtf8())!=scripttxt.toUtf8().size() ||
!exec.waitForBytesWritten(60000))
error(log, CannotLoadScript(command, args, scripttxt));
}
exec.closeWriteChannel();
if (!exec.waitForFinished(60000) && exec.state()!=QProcess::NotRunning)
error(log, ScriptNotFinished(command, args, scripttxt));
QString sout(exec.readAllStandardOutput());
QString serr(exec.readAllStandardError());
_result = sout;
log("result: "+(_result.size()?_result:"(void)"));
script->log(sout);
if (exec.exitCode()!=0 || exec.exitStatus()!=QProcess::NormalExit)
error(log, ScriptExecutionFailed(command, args, scripttxt,
exec.exitCode(), sout, serr));
return true;
}
private:
QString _command;
QStringList _args;
QStringList _script;
};
class Download: public Command {
Q_OBJECT
public:
QString tag() const {
return "download";
}
QString description() const {
return
tag()+" <filename>\n"
" <command-to-start-download>"
"\n\n--\n\n"
"Set download file before loading a download link or clicking on a "
"download button. The next line must be exactly one command that "
"initiates the download.\n\n"
"Please note that variables are not substituted in the filename.";
}
QString command() const {
return tag()+(_filename.size()?" "+_filename:QString())+"\n"
+_next->command();
}
std::shared_ptr<Command> parse(Script* script, QString args,
QStringList& in, QString file, int line,
int indent) {
std::shared_ptr<Download> cmd(new Download());
cmd->_filename = args.trimmed();
cmd->_next = script->parseLine(in, file, line+1, indent+1);
cmd->_next->log(false); // suppress logging of subcommand
return cmd;
}
bool execute(Script* script, QWebFrame* frame) {
Logger log(this, script);
_realfilename = script->replacevars(_filename);
log("REALFILENAME="+_realfilename);
frame->page()->setForwardUnsupportedContent(true);
assert(connect(frame->page(), SIGNAL(unsupportedContent(QNetworkReply*)),
this, SLOT(unsupportedContent(QNetworkReply*))));
try {
bool res(_next->execute(script, frame)); // start download
script->timer().stop(); // no timeout during download
for (_done = false; !_done;) // wait for download finish
QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
log("download terminated "+
QString(_netsuccess&&_filesuccess?"successfully":"with error"));
if (!_netsuccess) error(log, DownloadFailed(_realfilename));
if (!_filesuccess) error(log, WriteFileFailed(_realfilename));
log["[[ATTACHMENT|"+QDir(_realfilename).absolutePath()+"]]"];
disconnect(frame->page(), SIGNAL(unsupportedContent(QNetworkReply*)),
this, SLOT(unsupportedContent(QNetworkReply*)));
return res;
} catch (...) {
disconnect(frame->page(), SIGNAL(unsupportedContent(QNetworkReply*)),
this, SLOT(unsupportedContent(QNetworkReply*)));
throw;
}
}
private Q_SLOTS:
void completed(bool netsuccess, bool filesuccess) {
_done = true;
_netsuccess = netsuccess;
_filesuccess = filesuccess;
}
void unsupportedContent(QNetworkReply* reply) {
if (!_realfilename.size()) {
_realfilename = reply->url().toString().split('/').last();
if (reply->header(QNetworkRequest::ContentDispositionHeader).isValid()) {
QString part(reply->header(QNetworkRequest::ContentDispositionHeader)
.toString());
if (part.contains(QRegularExpression("attachment; *filename="))) {
part.replace(QRegularExpression(".*attachment; *filename="), "");
if (part.size()) _realfilename = part;
}
}
}
assert(connect(new RunDownload(reply, _realfilename),
SIGNAL(completed(bool, bool)), SLOT(completed(bool, bool))));
}
private:
QString _filename;
QString _realfilename;
std::shared_ptr<Command> _next;
bool _done, _netsuccess, _filesuccess;
};
class Click: public Command {
public:
QString tag() const {
return "click";
}
QString description() const {
return
tag()+" [<clicktype>] <selector>"
"\n\n--\n\n"
"Click on the specified element. Either you explicitely specify a click"
" type, such as <realmouse> or <javascript>, or the previously set or"
" the default clicktype is used.";
}
QString command() const {
return tag()+(_clicktype
?(*_clicktype==Script::REAL_MOUSE_CLICK?" realmouse"
:*_clicktype==Script::JAVASCRIPT_CLICK?" javascript":"")
:"")+" "+_selector;
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList&, QString, int, int) {
std::shared_ptr<Click> cmd(new Click());
if (args.trimmed().contains(QRegularExpression("^realmouse ")))
cmd->_clicktype = Script::REAL_MOUSE_CLICK;
else if (args.trimmed().contains(QRegularExpression("^javascript ")))
cmd->_clicktype = Script::JAVASCRIPT_CLICK;
cmd->_selector =
args.remove(QRegularExpression("^ *(realmouse|javascript) +"));
return cmd;
}
bool execute(Script* script, QWebFrame* frame) {
Logger log(this, script);
QString clicktarget(script->replacevars(_selector));
switch (_clicktype ? *_clicktype : script->clicktype()) {
case Script::REAL_MOUSE_CLICK: {
realMouseClick(log, frame, script, clicktarget);
break;
}
case Script::JAVASCRIPT_CLICK:
default: {
QWebElement element(find(frame, clicktarget, script->timeout()-1));
if (element.isNull())
error(log, ElementNotFound(clicktarget));
_result = element.evaluateJavaScript("this.click();").toString();
if (_result.size()) log("result: "+_result.size());
break;
}
}
return true;
}
private:
QString _selector;
std::optional<Script::ClickType> _clicktype;
};
class Set: public Command {
public:
QString tag() const {
return "set";
}
QString description() const {
return
tag()+" <variable>=<value>\n\n"+
tag()+" <variable>\n"
" <command>"
"\n\n--\n\n"
"Sets the value of a variable either to a constant, or to the output"
" of a command. A command should be a command that produces an"
" output, such as <do>, which returns the result of JavaScript or"
" <execute>, which returns the output of the executed command, or"
" <call>, which returns the result of the last command."
" All variables are global with regard to functions.\n\n"
"To set a variable to the value of an input field, you can use:\n\n"
" "+tag()+" <variable>\n"
" do <selector>\n"
" this.value\n\n";
"To set a variable to the inner html content of an element, you can use:\n\n"
" "+tag()+" <variable>\n"
" do <selector>\n"
" this.innerHTML";
}
QString command() const {
if (_next)
return tag()+" "+_name+"\n "+_next->command();
else
return tag()+" "+_name+" = "+_value;
}
std::shared_ptr<Command> parse(Script* script, QString args,
QStringList& in, QString file, int line,
int indent) {
std::shared_ptr<Set> cmd(new Set());
cmd->_next = 0;
QStringList allargs(args.split("="));
cmd->_name = allargs.takeFirst().trimmed();
cmd->_value = allargs.join("=").trimmed();
if (!args.contains('=')) {
cmd->_next = script->parseLine(in, file, line+1, indent+1);
cmd->_next->log(false); // suppress logging of subcommand
}
return cmd;
}
bool execute(Script* script, QWebFrame* frame) {
Logger log(this, script);
if (_next) {
_next->execute(script, frame);
script->set(_name,
script->replacevars(_next->result()));
log(_name+"='"+_next->result().replace("\\", "\\\\").replace("'", "\\'")+"'");
} else {
script->set(_name,
script->replacevars(_value));
log(_name+"='"+_value.replace("\\", "\\\\").replace("'", "\\'")+"'");
}
return true;
}
private:
QString _name;
QString _value;
std::shared_ptr<Command> _next;
};
class UnSet: public Command {
public:
QString tag() const {
return "unset";
}
QString description() const {
return
tag()+" <variable>"
"\n\n--\n\n"
"Undo the setting of a variable. The opposite of «set».";
}
QString command() const {
return tag()+" "+_name;
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList&, QString, int, int) {
std::shared_ptr<UnSet> cmd(new UnSet());
cmd->_name = args;
return cmd;
}
bool execute(Script* script, QWebFrame*) {
Logger log(this, script);
script->unset(_name);
return true;
}
private:
QString _name;
};
class Timeout: public Command {
public:
QString tag() const {
return "timeout";
}
QString description() const {
return
tag()+" <seconds>"
"\n\n--\n\n"
"Set the timeout in seconds.";
}
QString command() const {
return tag()+" "+_timeout;
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList&, QString, int, int) {
std::shared_ptr<Timeout> cmd(new Timeout());
cmd->_timeout = args;
return cmd;
}
bool execute(Script* script, QWebFrame*) {
Logger log(this, script);
bool ok;
int timeout(script->replacevars(_timeout).toInt(&ok));
if (!ok) error(log, BadArgument(script->replacevars(_timeout)
+" should be a number of seconds"));
script->timeout(timeout);
script->defaultTimeout(timeout);
return true;
}
private:
QString _timeout;
};
class TimeoutFalse: public Command {
public:
QString tag() const {
return "timeout-false";
}
QString description() const {
return
tag()+" <seconds>"
"\n\n--\n\n"
"Set the timeout for negative testcases in seconds."
" Time to wait until a negative testcase is accepted. It is used in"
" commands such as not, if or while, where a fail is the expected or at"
" least a valid result. Normally, this timeout can be lower than the"
" normal timeout. This is, because to verify that something does not"
" exists, the test run has always to wait for the full timeout. So"
" setting this to a lower value increases the speed of the test run.";
}
QString command() const {
return tag()+" "+_timeout;
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList&, QString, int, int) {
std::shared_ptr<TimeoutFalse> cmd(new TimeoutFalse());
cmd->_timeout = args;
return cmd;
}
bool execute(Script* script, QWebFrame*) {
Logger log(this, script);
bool ok;
int timeout(script->replacevars(_timeout).toInt(&ok));
if (!ok) error(log, BadArgument(script->replacevars(_timeout)
+" should be a number of seconds"));
script->timeoutFalse(timeout);
script->defaultTimeoutFalse(timeout);
return true;
}
private:
QString _timeout;
};
class CaCertificate: public Command {
public:
QString tag() const {
return "ca-certificate";
}
QString description() const {
return
tag()+" <filename.pem>"
"\n\n--\n\n"
"Load a CA certificate that will be accepted on SSL connections.";
}
QString command() const {
return tag()+" "+_filename;
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList&, QString, int, int) {
std::shared_ptr<CaCertificate> cmd(new (CaCertificate));
cmd->_filename = args.trimmed();
return cmd;
}
bool execute(Script* script, QWebFrame*) {
Logger log(this, script);
QString filename(script->replacevars(_filename));
QFile cacertfile(script->path()+filename);
if (!cacertfile.exists()) cacertfile.setFileName(filename); // try without path
if (!cacertfile.exists() || !cacertfile.open(QIODevice::ReadOnly))
error(log, FileNotFound(filename));
QSslCertificate cacert(&cacertfile);
if (cacert.isNull()) error(log, NotACertificate(filename));
QSslSocket::addDefaultCaCertificate(cacert);
return true;
}
private:
QString _filename;
};
class ClientCertificate: public Command {
public:
QString tag() const {
return "client-certificate";
}
QString description() const {
return
tag()+" <certfile.pem> <keyfile.pem> <keypassword>"
"\n\n--\n\n"
"Load a client certificate to authenticate on SSL connections. "
"The password for the keyfile should not contain spaces. "
"Create the two files from a PKCS#12 file using OpenSSL:\n"
" openssl pkcs12 -in certfile.p12 -out certfile.pem -nodes -clcerts\n"
" openssl pkcs12 -in certfile.p12 -out keyfile.pem -nocerts\n";
}
QString command() const {
return tag()+" "+_certfile+" "+_keyfile+" "+_password;
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList&, QString, int, int) {
std::shared_ptr<ClientCertificate> cmd(new (ClientCertificate));
QStringList s(args.trimmed().split(' '));
if (s.size()<3) throw MissingArguments(args, "certfile keyfile password");
cmd->_certfile = s.takeFirst();
cmd->_keyfile = s.takeFirst();
cmd->_password = s.join(' ');
return cmd;
}
bool execute(Script* script, QWebFrame*) {
Logger log(this, script);
QSslConfiguration sslConfig(QSslConfiguration::defaultConfiguration());
sslConfig.setProtocol(QSsl::AnyProtocol);
sslConfig.setPeerVerifyMode(QSslSocket::AutoVerifyPeer);
QString filename(script->replacevars(_certfile));
QFile certfile(script->path()+filename);
if (!certfile.exists()) certfile.setFileName(filename);
if (!certfile.exists() || !certfile.open(QIODevice::ReadOnly))
error(log, FileNotFound(filename));
QSslCertificate cert(&certfile);
if (cert.isNull()) error(log, NotACertificate(filename));
sslConfig.setLocalCertificate(cert);
filename = script->replacevars(_keyfile);
QFile keyfile(script->path()+filename);
if (!keyfile.exists()) keyfile.setFileName(filename);
if (!keyfile.exists() || !keyfile.open(QIODevice::ReadOnly))
error(log, FileNotFound(filename));
keyfile.open(QIODevice::ReadOnly);
QSslKey k(&keyfile, QSsl::Rsa, QSsl::Pem,
QSsl::PrivateKey, script->replacevars(_password).toUtf8());
if (k.isNull()) error(log, KeyNotReadable(filename));
sslConfig.setPrivateKey(k);
QSslConfiguration::setDefaultConfiguration(sslConfig);
return true;
}
private:
QString _certfile;
QString _keyfile;
QString _password;
};
class ClickType: public Command {
public:
QString tag() const {
return "clicktype";
}
QString description() const {
return
tag()+" <type>"
"\n\n--\n\n"
"Set how mouseclicks should be mapped. The best solution depends on"
" your problem. Normally it is good to call \"click()\" on the element"
" using javascript. But with complex javascript infected websites, it"
" might be necessary to simulate a real mouse click.\n\n"
"<type> is one of:\n"
" - realmouse\n"
" - javascript (default)";
}
QString command() const {
switch (_clicktype) {
case Script::REAL_MOUSE_CLICK: return tag()+" realmouse";
case Script::JAVASCRIPT_CLICK: default:
return tag()+" javascript";
}
}
std::shared_ptr<Command> parse(Script* script, QString args,
QStringList&, QString, int, int) {
std::shared_ptr<ClickType> cmd(new ClickType());
QString choice(script->replacevars(args).trimmed());
if (choice=="realmouse")
cmd->_clicktype = Script::REAL_MOUSE_CLICK;
else if (choice=="javascript")
cmd->_clicktype = Script::JAVASCRIPT_CLICK;
else
throw BadArgument("unknown clicktype: "+choice);
return cmd;
}
bool execute(Script* script, QWebFrame* ) {
Logger log(this, script);
script->clicktype(_clicktype);
return true;
}
private:
Script::ClickType _clicktype;
};
class SetValue: public Command {
public:
QString tag() const {
return "setvalue";
}
QString description() const {
return
tag()+" <selector> -> '<value>'\n\n"+
tag()+" <selector> -> '<value1>', '<value2>', <...>"
"\n\n--\n\n"
"Set the selected element to a given value. It is mostly the same as"
" the following constuct, except that options in a select are evaluated"
" correctly:\n\n"
" do <selector>\n"
" this.value='<value>';\n\n"
"The second syntax with comma separated list of valus applies for"
" options in a select element\n\n"
"If you quote the values, then quote all values with the same"
" quotes. If you need a comma within a value, you must quote.";
}
QString command() const {
return tag()+" "+_selector+" -> "+_value;
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList&, QString, int, int) {
std::shared_ptr<SetValue> cmd(new SetValue());
QStringList allargs(args.split("->"));
if (allargs.size()<2)
throw BadArgument(tag()+" requires <selector> -> <value>, "
"instead of: \""+args+"\"");
cmd->_selector = allargs.takeFirst().trimmed();
cmd->_value = allargs.join("->").trimmed();
return cmd;
}
bool execute(Script* script, QWebFrame* frame) {
Logger log(this, script);
QWebElement element(find(frame, script->replacevars(_selector),
script->timeout()-1));
if (element.isNull())
error(log, ElementNotFound(script->replacevars(_selector)));
QString value(script->replacevars(_value));
if (element.tagName()=="SELECT") {
// value is a comma seperated list of option values
QStringList values(commaSeparatedList(value));
for (QWebElement option: element.findAll("option")) {
QString name(option.evaluateJavaScript("this.value").toString());
option.evaluateJavaScript
("this.selected="+QString(values.contains(name)?"true;":"false;"));
}
} else {
if (value.size()>1 && value[0]==value[value.size()-1] &&
(value[0]=='"' || value[0]=='\''))
element.evaluateJavaScript("this.value="+value+";");
else
element.evaluateJavaScript("this.value='"
+value.replace("'", "\\'")
+"';");
}
return true;
}
private:
QString _selector;
QString _value;
};
class Function: public CommandContainer {
public:
QString tag() const {
return "function";
}
QString description() const {
return
tag()+" <name> [<var1>, <var2>, <...>]\n"
" <command1>\n"
" <command2>\n"
" <...>"
"\n\n--\n\n"
"Define a function with arguments. The arguments are treated like"
" local variables. In a sequence of scripts within the same testrun,"
" functions are inherited from all followin scripts, so you can first"
" load a script file that contains all functions. Within the same file,"
" a function can be called before the definition.\n\n"
"If you quote the values, then quote all values with the same"
" quotes. If you need a comma within a value, you must quote.";
}
QString command() const {
return tag()+" "+_name+" "+_vars.join(", ");
}
std::shared_ptr<Command> parse(Script* script, QString args,
QStringList& in, QString file, int line,
int indent) {
std::shared_ptr<Function> cmd(new Function());
if (!args.size()) throw BadArgument(tag()+" requires a <name>");
QStringList allargs(args.split(" "));
cmd->_name = allargs.takeFirst().trimmed();
cmd->_vars = commaSeparatedList(allargs.join(' '));
script->function(cmd->_name, cmd);
cmd->_script = cmd->subParser(script, subCommandBlock(in), file, line, indent);
return cmd;
}
bool execute(Script* script, QWebFrame*) {
Logger log(this, script, false);
return true;
}
bool call(Logger& log, Command* parentCommand, QStringList args,
Script* script, QWebFrame* frame) {
try {
return runScript(log, parentCommand, _script, script, frame, _vars, args);
} catch (const Exception& x) {
error(log, FunctionCallFailed(_name, _vars, args, x));
return false; // never reached due to exception above
}
}
private:
QString _name;
QStringList _vars;
};
class Call: public Command {
public:
QString tag() const {
return "call";
}
QString description() const {
return
tag()+" <name> ['<arg1>', '<arg2>', ...]"
"\n\n--\n\n"
"Calls a function. The number of arguments must be exactly the same"
" as in the function definition.\n\n"
"If you quote the values, then quote all values with the same"
" quotes. If you need a comma within a value, you must quote.";
}
QString command() const {
return tag()+" "+_name+(_args.size()?" '"+_args.join("', '")+"'":"");
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList&, QString, int, int) {
std::shared_ptr<Call> cmd(new Call());
if (!args.size()) throw BadArgument(tag()+" requires a <name>");
QStringList allargs(args.split(" "));
cmd->_name = allargs.takeFirst().trimmed();
cmd->_args = commaSeparatedList(allargs.join(' '));
return cmd;
}
bool execute(Script* script, QWebFrame* frame) {
Logger log(this, script);
return script->function(_name)->call(log, this, script->replacevars(_args),
script, frame);
}
int steps(Script* parent) {
return parent->function(_name)->steps(parent);
}
public:
QString _name;
QStringList _args;
};
class If: public CommandContainer {
public:
QString tag() const {
return "if";
}
QString description() const {
return
tag()+" [not] <variable> <cmp> <value>\n"
" <command1>\n"
" <command2>\n"
" <...>\n"
"else\n"
" <command3>\n"
" <command4>\n"
" <...>\n"
"\n\n"+
tag()+" [not] <selector> -> <text>\n"
" <command1>\n"
" <command2>\n"
" <...>\n"
"else\n"
" <command3>\n"
" <command4>\n"
" <...>\n"
"\n\n--\n\n"
"Execute commands conditionally. "
"The first variant compares a variable to a value. "
"The comparision <cmp> can be = ! . ^ ~ < >, "
"which means equal, different, contains, contains not, match, "
"less (as integer), bigger (as integer). "
"Match allows a regular expression. "
"The second variant checks for a text in a selector, "
"similar to command exists. The text can be empty to just "
"check for the existence of a selector. "
"There is an optional else part. Optionally start with "
"not to invert the test.";
}
QString command() const {
return tag()+(_not?" not ":" ")+_variable+" "+_cmp+" "+_value
+(_script.get()?"\n "+_script->print().join("\n "):"")
+(_else.get()?"\nelse\n "+_else->print().join("\n "):"");
}
std::shared_ptr<Command> parse(Script* script, QString args,
QStringList& in, QString file, int line,
int indent) {
std::shared_ptr<If> cmd(new If());
QRegularExpressionMatch m;
if (args.contains(QRegularExpression("^ *not +"), &m)) {
args.remove(0, m.capturedLength());
cmd->_not = true;
}
int pos(args.indexOf(QRegularExpression("[=!.^~<>]")));
int len(1);
if (args.contains("->")) {
pos = args.indexOf("->");
len = 2;
cmd->_cmp = "->";
} else {
if (pos<0) throw BadArgument(tag()+" needs a comparision, not: "+args);
cmd->_cmp = args[pos];
}
cmd->_variable = args.left(pos).trimmed();
cmd->_value = args.mid(pos+len).trimmed();
cmd->_script = cmd->subParser(script, subCommandBlock(in), file, line, indent);
if (in.size() && in.first().contains(QRegularExpression("^ *else *$"))) {
in.removeFirst();
cmd->_else = cmd->subParser(script, subCommandBlock(in), file, line, indent);
}
return cmd;
}
bool execute(Script* script, QWebFrame* frame) {
Logger log(this, script, false);
QString value(script->replacevars(_value));
QString selector(script->replacevars(_variable));
bool check(false);
if (_cmp=="->") {
QWebElement firstelement(find(frame, selector, script->timeoutFalse()-1));
for (QWebElement element: frame->findAllElements(selector)) {
if (value.isEmpty() || // just find element
element.toOuterXml().indexOf(value)!=-1 ||
element.toPlainText().indexOf(value)!=-1) {
check = true;
break;
}
}
if (_not) check=!check;
log(QString("evaluated expression to ")+(check?"true":"false")
+(_not?": not ":": ")+selector+" "+_cmp+" "+value);
} else {
switch (_cmp[0].toLatin1()) {
case '=': check = script->variable(_variable)==value;
break;
case '!': check = script->variable(_variable)!=value;
break;
case '.': check = script->variable(_variable).contains(value);
break;
case '^': check = !script->variable(_variable).contains(value);
break;
case '~': check =
script->variable(_variable).contains(QRegularExpression(value));
break;
case '<': check = script->variable(_variable).toInt()<value.toInt();
break;
case '>': check = script->variable(_variable).toInt()>value.toInt();
break;
default:;
}
if (_not) check=!check;
log(QString("evaluated expression to ")+(check?"true":"false")
+(_not?": not ":": ")+script->variable(_variable)+" "+_cmp+" "+value);
}
if (check) return runScript(log, this, _script, script, frame);
else if (_else) return runScript(log, this, _else, script, frame);
return true;
}
protected:
int countSteps(Script* parent) {
int res1(CommandContainer::countSteps(parent)), res2(_else->countSteps()+1);
return res1 > res2 ? res1 : res2;
}
private:
QString _variable;
QString _cmp;
QString _value;
std::shared_ptr<Script> _else;
bool _not = false;
};
class While: public CommandContainer {
public:
QString tag() const {
return "while";
}
QString description() const {
return
tag()+ " [not] <variable> <cmp> <value>\n"
" <command1>\n"
" <command2>\n"
" <...>\n"
"\n\n"+
tag()+" [not] <selector> -> <text>\n"
" <command1>\n"
" <command2>\n"
" <...>\n"
"\n\n--\n\n"
"Repeats commands conditionally. "
"The first variant compares a variable to a value. "
"The comparision <cmp> can be = ! . ^ ~ < >, "
"which means equal, different, contains, contains not, match, "
"less (as integer), bigger (as integer). "
"Match allows a regular expression. "
"The second variant checks for a text in a selector, "
"similar to command exists. The text can be empty to just "
"check for the existence of a selector. Optionally start with "
"not to invert the test.";
}
QString command() const {
return tag()+(_not?" not ":" ")+_variable+" "+_cmp+" "+_value
+(_script.get()?"\n "+_script->print().join("\n "):"");
}
std::shared_ptr<Command> parse(Script* script, QString args,
QStringList& in, QString file, int line,
int indent) {
std::shared_ptr<While> cmd(new While());
QRegularExpressionMatch m;
if (args.contains(QRegularExpression("^ *not +"), &m)) {
args.remove(0, m.capturedLength());
cmd->_not = true;
}
int pos(args.indexOf(QRegularExpression("[=!.^~<>]")));
int len(1);
if (args.contains("->")) {
pos = args.indexOf("->");
len = 2;
cmd->_cmp = "->";
} else {
if (pos<0) throw BadArgument(tag()+" needs a comparision, not: "+args);
cmd->_cmp = args[pos];
}
cmd->_variable = args.left(pos).trimmed();
cmd->_value = args.mid(pos+len).trimmed();
cmd->_script = cmd->subParser(script, subCommandBlock(in), file, line, indent);
return cmd;
}
bool execute(Script* script, QWebFrame* frame) {
Logger log(this, script, false);
QString value(script->replacevars(_value));
QString selector(script->replacevars(_variable));
for (bool check(true); check;) {
if (_cmp=="->") {
check = false;
QWebElement firstelement(find(frame, selector, script->timeoutFalse()-1));
for (QWebElement element: frame->findAllElements(selector)) {
if (value.isEmpty() || // just find element
element.toOuterXml().indexOf(value)!=-1 ||
element.toPlainText().indexOf(value)!=-1) {
check = true;
break;
}
}
if (_not) check=!check;
log(QString("evaluated expression to ")+(check?"true":"false")
+(_not?": not ":": ")+selector+" "+_cmp+" "+value);
} else {
switch (_cmp[0].toLatin1()) {
case '=': check = script->variable(_variable)==value;
break;
case '!': check = script->variable(_variable)!=value;
break;
case '.': check = script->variable(_variable).contains(value);
break;
case '^': check = !script->variable(_variable).contains(value);
break;
case '~': check =
script->variable(_variable).contains(QRegularExpression(value));
break;
case '<': check = script->variable(_variable).toInt()<value.toInt();
break;
case '>': check = script->variable(_variable).toInt()>value.toInt();
break;
default:;
}
if (_not) check=!check;
log(QString("evaluated expression to ")+(check?"true":"false")
+(_not?": not ":": ")+script->variable(_variable)+" "+_cmp+" "+value);
}
if (check) if (!runScript(log, this, _script, script, frame)) return false;
}
return true;
}
private:
QString _variable;
QString _cmp;
QString _value;
bool _not = false;
};
class TestSuite: public Command {
public:
QString tag() const {
return "testsuite";
}
QString description() const {
return
tag()+" <name>"
"\n\n--\n\n"
"Start a testsuite and give it a name.";
}
QString command() const {
return tag()+" "+_name;
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList&, QString, int, int) {
std::shared_ptr<TestSuite> cmd(new TestSuite());
cmd->_name = args;
return cmd;
}
bool execute(Script* script, QWebFrame*) {
Logger log(this, script);
script->testsuite(script->replacevars(_name));
return true;
}
private:
QString _name;
};
class TestCase: public Command {
public:
QString tag() const {
return "testcase";
}
QString description() const {
return
tag()+" <name>"
"\n\n--\n\n"
"Start a testcase and give it a name.";
}
QString command() const {
return tag()+" "+_name;
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList&, QString, int, int) {
std::shared_ptr<TestCase> cmd(new TestCase());
cmd->_name = args;
return cmd;
}
bool execute(Script* script, QWebFrame*) {
Logger log(this, script);
script->testclass(script->replacevars(_name));
return true;
}
private:
QString _name;
};
class Check: public Command {
public:
QString tag() const {
return "check";
}
QString description() const {
return
tag()+" <value1> <cmp> <value2>\n\n"+
tag()+" <value1> <cmp>\n"
" <command>"
"\n\n--\n\n"
"Compares two values (you can use variables) or compares a value to the"
" result of a command. The command should be a command that produces an"
" output, such as <do>, which returns the result of JavaScript or"
" <execute>, which returns the output of the executed command, or"
" <call>, which returns the result of the last command. "
"The comparision <cmp> can be = ! . ^ ~ < >, "
"which means equal, different, contains, contains not, match, "
"less (as integer), bigger (as integer). "
"Match allows a regular expression. "
" less than < (integers), larger than > (integers)";
}
QString command() const {
if (_next)
return tag()+" "+_value1+" "+QString(_cmp)+"\n "+_next->command();
else
return tag()+" "+_value1+" "+QString(_cmp)+" "+_value2;
}
std::shared_ptr<Command> parse(Script* script, QString args,
QStringList& in, QString file, int line,
int indent) {
std::shared_ptr<Check> cmd(new Check());
cmd->_next = 0;
QString comp("[=!.^~<>]");
QStringList allargs = quotedStrings(args, comp, true, 1);
if (allargs.size()<2 || allargs[1].size()!=1 ||
!QRegularExpression("^"+comp+"$").match(allargs[1]).hasMatch())
throw BadArgument(tag()+" needs a comparision, not: "+args, allargs);
if (allargs.size()>3)
throw BadArgument(tag()+" has at most three arguments", allargs);
cmd->_value1 = allargs[0];
cmd->_cmp = allargs[1][0].toLatin1();
if (allargs.size()==3) {
cmd->_value2 = allargs[2];
} else {
if (in.size() && in.first().contains(QRegularExpression("^ "))) {
cmd->_next = script->parseLine(in, file, line+1, indent+1);
cmd->_next->log(false); // suppress logging of subcommand
} else throw BadArgument(tag()+" needs a third argument or a following command", allargs);
}
return cmd;
}
bool execute(Script* script, QWebFrame* frame) {
Logger log(this, script);
QString value1(script->replacevars(_value1));
QString value2(script->replacevars(_value2));
if (_next) {
_next->execute(script, frame);
value2 = script->replacevars(_next->result());
}
bool check(false);
switch (_cmp) {
case '=': check = value1==value2; break;
case '!': check = value1!=value2; break;
case '.': check = value1.contains(value2); break;
case '^': check = !value1.contains(value2); break;
case '~': check = value1.contains(QRegularExpression(value2)); break;
case '<': check = value1.toInt()<value2.toInt(); break;
case '>': check = value1.toInt()>value2.toInt(); break;
default:;
}
log("evaluated expression: "+value1+" "+_cmp+" "+value2);
if (!check) error(log, CheckFailed(value1, _cmp, value2));
return true;
}
private:
QString _value1;
QString _value2;
char _cmp;
std::shared_ptr<Command> _next;
};
class For: public CommandContainer {
public:
QString tag() const {
return "for";
}
QString description() const {
return
tag()+" <variable> -> <val1>, <val2>, <...>\n"
" <command1>\n"
" <command2>\n"
" <...>"
"\n\n--\n\n"
"Executes the given commands with the variable set to the specifier values,"
"repeated once per given value. The variable is treated like a local variale"
" in the loop.\n\n"
"Without values, if there is a global variable with the same name as the"
" local variable the global variable is parsed as if it were the line after"
" the dash (->).\n\n"
"If you quote the values, then quote all values with the same"
" quotes. If you need a comma within a value, you must quote.";
}
QString command() const {
return tag()+" "+_variable+" "+_vals.join(" ");
}
std::shared_ptr<Command> parse(Script* script, QString args,
QStringList& in, QString file, int line, int indent) {
std::shared_ptr<For> cmd(new For());
if (!args.size()) throw BadArgument(tag()+" requires a <variable>");
QStringList allargs(args.split("->"));
cmd->_variable = allargs.takeFirst().trimmed();
cmd->_vals = commaSeparatedList(allargs.join("->"));
cmd->_script = cmd->subParser(script, subCommandBlock(in), file, line, indent);
return cmd;
}
bool execute(Script* script, QWebFrame* frame) {
Logger log(this, script);
for (QString i: _vals.size()?_vals:commaSeparatedList(script->variable(_variable))) {
if (!runScript(log, this, _script, script, frame, QStringList()<<_variable, QStringList()<<i))
return false;
}
return true;
}
int countSteps(Script* parent) const {
int sz(1);
if (_vals.size())
sz = _vals.size();
else if (parent->variables().contains(_variable))
sz = commaSeparatedList(parent->variable(_variable)).size();
return (sz?sz:1)*CommandContainer::countSteps(parent);
}
private:
QString _variable;
QStringList _vals;
};
class Echo: public Command {
public:
QString tag() const {
return "echo";
}
QString description() const {
return
tag()+" <text>"
"\n\n--\n\n"
"Echoes a text to the log.";
}
QString command() const {
return tag();
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList&, QString, int, int) {
std::shared_ptr<Echo> cmd(new (Echo));
cmd->_text = args;
return cmd;
}
bool execute(Script* script, QWebFrame*) {
Logger log(this, script);
log(script->replacevars(_text));
return true;
}
private:
QString _text;
};
class OfflineStoragePath: public Command {
public:
QString tag() const {
return "offline-storage-path";
}
QString description() const {
return
tag()+" <path>"
"\n\n--\n\n"
"Set offline storage path. Defaults to /tmp.";
}
QString command() const {
return tag()+" "+_path;
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList&, QString, int, int) {
std::shared_ptr<OfflineStoragePath> cmd(new OfflineStoragePath());
cmd->_path = args;
return cmd;
}
bool execute(Script* script, QWebFrame* frame) {
Logger log(this, script);
QDir path(_path);
if (!path.exists() && !path.mkpath(_path))
error(log, DirectoryCannotBeCreated(_path));
TestWebPage* page(dynamic_cast<TestWebPage*>(frame->page()));
page->settings()->setOfflineStoragePath(_path);
return true;
}
private:
QString _path;
};
class ClearCookies: public Command {
public:
QString tag() const {
return "clear-cookies";
}
QString description() const {
return
tag()+" <url>"
"\n\n--\n\n"
"Clear all cookies of given URL <url>, or of the current URL if no"
" <url> is specified.";
}
QString command() const {
return tag()+(_url.isEmpty()?"":" "+_url);
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList&, QString, int, int) {
std::shared_ptr<ClearCookies> cmd(new ClearCookies());
if (args.size()) cmd->_url = args;
return cmd;
}
bool execute(Script* script, QWebFrame* frame) {
Logger log(this, script);
QString url(_url);
if (url.isEmpty()) url = frame->url().toString();
QNetworkCookieJar* cookies = frame->page()->networkAccessManager()->cookieJar();
for (QNetworkCookie cookie: cookies->cookiesForUrl(url)) {
log("delete cookie "+cookie.name());
cookies->deleteCookie(cookie);
}
return true;
}
private:
QString _url;
};
class Include: public CommandContainer {
public:
QString tag() const {
return "include";
}
QString description() const {
return
tag()+" <filename>"
"\n\n--\n\n"
"Include a test script.";
}
QString command() const {
return tag()+" "+_filename;
}
std::shared_ptr<Command> parse(Script* script, QString args,
QStringList&, QString, int, int indent) {
std::shared_ptr<Include> cmd(new Include());
cmd->_filename = args;
QFile f(cmd->_filename);
if (!f.open(QIODevice::ReadOnly)) throw OpenIncludeFailed(cmd->_filename);
QString txt(QString::fromUtf8(f.readAll()));
try {
cmd->_script = cmd->subParser(script, txt.split('\n'), cmd->_filename, 0, indent);
} catch (Exception& e) {
throw ParseIncludeFailed(cmd->_filename, e.what());
}
for (QString key: cmd->_script->functions()) // copy new functions to parent
script->function(key, cmd->_script->function(key));
return cmd;
}
bool execute(Script* script, QWebFrame* frame) {
Logger log(this, script);
try {
return runScript(log, this, _script, script, frame);
} catch (Exception& e) {
error(log, ExecuteIncludeFailed(_filename, e.what()));
return false; // never reached due to exception above
}
}
private:
QString _filename;
};
class Case: public Command {
public:
QString tag() const {
return "case";
}
QString description() const {
return
tag()+" <variable>\n"
" <cmp1> <value1>\n"
" <command>\n"
" <command>\n"
" <cmp2> <value2>\n"
" <command>\n"
" <command>\n"
" <...>\n"
" default\n"
" <command>\n"
" <command>\n"
" <...>\n"
"\n\n"+
tag()+" <selector>\n"
" -> <text1>\n"
" <command>\n"
" <command>\n"
" -> <text2>\n"
" <command>\n"
" <command>\n"
" <...>\n"
" default\n"
" <command>\n"
" <command>\n"
" <...>\n"
"\n\n--\n\n"
"Execute commands conditionally depending on a variable. "
"It is equivalent to neested if-else-if commands."
"The first variant compares a variable to a value. "
"The comparision <cmp> can be = ! . ^ ~ < >, "
"which means equal, different, contains, contains not, match, "
"less (as integer), bigger (as integer). "
"Match allows a regular expression. "
"The second variant checks for a text in a selector, "
"similar to command exists. "
"There is an optional default part that applies if none "
"of the previous conditions match.";
}
QString command() const {
QString body;
for (Condition condition: _conditions) {
body += "\n "+condition.cmp+" "+condition.value+"\n "
+condition.script->print().join("\n ");
}
return tag()+" "+_variable+body;
}
std::shared_ptr<Command> parse(Script* script, QString args,
QStringList& in, QString file, int line,
int indent) {
std::shared_ptr<Case> cmd(new Case());
if (!args.size()) throw BadArgument(tag()+" requires a <variable> or <selector>");
cmd->_variable = args;
QStringList body(subCommandBlock(in));
if (!body.size()) throw BadArgument(tag()+" requires a body");
while (body.size()) {
++line;
QStringList parts(body.takeFirst().split(' '));
QString cmp(parts.takeFirst());
QString value(parts.join(' '));
if (!cmp.contains(QRegularExpression("^[=!.^~<>]|->|default$")))
throw BadArgument(tag()+" needs a comparision, not: "+cmp);
std::shared_ptr<Script> sub
(cmd->subParser(script, subCommandBlock(body), file, line, indent+1));
cmd->_conditions.append(Condition(cmp, value, sub));
}
return cmd;
}
bool execute(Script* script, QWebFrame* frame) {
Logger log(this, script, false);
QString selector(script->replacevars(_variable));
for (Condition condition: _conditions) {
QString value(script->replacevars(condition.value));
bool check(false);
if (condition.cmp=="default") {
log("terminate with default branch");
check = true;
} else if (condition.cmp=="->") {
QWebElement firstelement(find(frame, selector, script->timeoutFalse()-1));
for (QWebElement element: frame->findAllElements(selector)) {
if (value.isEmpty() || // just find element
element.toOuterXml().indexOf(value)!=-1 ||
element.toPlainText().indexOf(value)!=-1) {
check = true;
break;
}
}
log(QString("evaluated expression to ")+(check?"true":"false")+": "
+selector+" "+condition.cmp+" "+value);
} else {
switch (condition.cmp[0].toLatin1()) {
case '=': check = script->variable(_variable)==value;
break;
case '!': check = script->variable(_variable)!=value;
break;
case '.': check = script->variable(_variable).contains(value);
break;
case '^': check = !script->variable(_variable).contains(value);
break;
case '~': check =
script->variable(_variable).contains(QRegularExpression(value));
break;
case '<': check = script->variable(_variable).toInt()<value.toInt();
break;
case '>': check = script->variable(_variable).toInt()>value.toInt();
break;
default:;
}
log(QString("evaluated expression to ")+(check?"true":"false")+": "
+script->variable(_variable)+" "+condition.cmp+" "+value);
}
if (check) return runScript(log, this, condition.script, script, frame);
}
return true;
}
private:
int countSteps(Script*) {
int res1(0);
for (auto condition: _conditions) {
int res2(condition.script->countSteps()+1);
if (res2>res1) res1=res2;
}
return res1;
}
struct Condition {
Condition(QString c, QString v, std::shared_ptr<Script> s):
cmp(c), value(v), script(s) {
}
Condition() {}
QString cmp;
QString value;
std::shared_ptr<Script> script;
};
QString _variable;
QVector<Condition> _conditions;
};
class Fail: public Command {
public:
QString tag() const {
return "fail";
}
QString description() const {
return
tag()+" <text>"
"\n\n--\n\n"
"Fail with error text.";
}
QString command() const {
return tag()+" "+_text;
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList&, QString, int, int) {
std::shared_ptr<Fail> cmd(new Fail());
cmd->_text = args;
return cmd;
}
bool execute(Script* script, QWebFrame*) {
Logger log(this, script);
error(log, TestFailed(script->replacevars(_text)));
return true; // dummy
}
private:
QString _text;
};
class Auth: public Command {
public:
QString tag() const {
return "auth";
}
QString description() const {
return
tag()+" <realm> <username> <password>"
"\n\n"+
tag()+" <realm>"
"\n\n--\n\n"
"Set basic authentication credentials for <realm> to"
" <username> and <password>. If no realm is given,"
" the credentials for the given realm are removed.";
}
QString command() const {
return tag()+" "+_username+" "+_password;
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList&, QString, int, int) {
std::shared_ptr<Auth> cmd(new Auth());
QStringList allargs = args.split(" ");
if (!allargs.size()) throw BadArgument("requires at least a <realm>");
cmd->_realm = allargs.takeFirst();
if (allargs.size() && allargs.size()==2) {
cmd->_username=allargs[0];
cmd->_password=allargs[1];
} else {
throw BadArgument(QString("requires <username> and <password>, but %1 was given")
.arg(args));
}
return cmd;
}
bool execute(Script* script, QWebFrame*) {
Logger log(this, script);
script->auth(_realm, _username, _password);
return true;
}
private:
QString _realm;
QString _username;
QString _password;
};
class Ignore: public Command {
public:
QString tag() const {
return "ignore";
}
QString description() const {
return
tag()+" <signal1> <signal2> <...>"
"\n\n--\n\n"
"Ignores a specific signal. It will not be placed in the queue "
"and any expect for this signal is always true. You can call "
"ignore with a space separated list of signal names. You cannot "
"specify signal arguments. An empty ignore ignores all signals.";
}
QString command() const {
return tag()+" "+_signals.join(" ");
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList&, QString, int, int) {
std::shared_ptr<Ignore> cmd(new Ignore());
cmd->_signals = args.split(' ', QString::SkipEmptyParts);
return cmd;
}
bool execute(Script* script, QWebFrame*) {
Logger log(this, script);
script->ignore(_signals);
return true;
}
private:
QStringList _signals;
};
class UnIgnore: public Command {
public:
QString tag() const {
return "unignore";
}
QString description() const {
return
tag()+" <signal1> <signal2> <...>"
"\n\n--\n\n"
"Undo ignoring a specific signal. It will be placed in the queue "
"and any expect this signal checks the queue. You can call "
"unignore with a space separated list of signal names. You cannot "
"specify signal arguments. An empty ignore activates all signals.";
}
QString command() const {
return tag()+" "+_signals.join(" ");
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList&, QString, int, int) {
std::shared_ptr<UnIgnore> cmd(new UnIgnore());
cmd->_signals = args.split(' ', QString::SkipEmptyParts);
return cmd;
}
bool execute(Script* script, QWebFrame*) {
Logger log(this, script);
script->unignore(_signals);
return true;
}
private:
QStringList _signals;
};
/* Template:
class : public Command {
public:
QString tag() const {
return "";
}
QString description() const {
return
tag()+
"\n\n--\n\n"
"";
}
QString command() const {
return tag();
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList&, QString, int, int) {
std::shared_ptr<> cmd(new ());
return cmd;
}
bool execute(Script* script, QWebFrame* frame) {
Logger log(this, script);
return true;
}
};
*/
inline bool Screenshot::execute(Script* script, QWebFrame* frame) {
Logger log(this, script);
if (!script->screenshots()) {
log("screenshots disabled");
return true;
}
QString filename(screenshot(log, line(), script->targetdir(),
script->testclass(),
script->replacevars(_filename), frame));
log["[[ATTACHMENT|"+filename+"]]"];
return true;
}
inline Logger::Logger(Command* command, Script* script, bool showLines):
_command(command), _script(script) {
if (command) {
if (_command->log()) {
if (showLines)
_script->log("\\ "+_command->command(), _command);
else
_script->log("\\ "+_command->command().split('\n').first(), _command);
}
}
}
inline void Logger::operator()(QString txt) {
if (!_command || _command->log()) _script->log(" "+txt, _command);
}
inline void Logger::operator[](QString txt) {
_script->plainlog(txt);
}
inline Logger::~Logger() {
if (_command) {
if (_command->log()) _script->log("/ "+_command->tag(), _command);
}
}
inline void Command::realMouseClick(Logger& log, QWebFrame* frame,
Script* script, QString selector) {
QWebElement element(find(frame, selector, script->timeout()-1));
if (element.isNull()) error(log, ElementNotFound(selector));
realMouseClick(element);
}
inline std::shared_ptr<Script> Command::subParser(Script* parent, const QStringList& in,
const QString& file,
int line, int indent) {
std::shared_ptr<Script> res(new Script);
for (QString key: parent->functions()) // copy functions from parent
res->function(key, parent->function(key));
res->parse(in, file, line+1, indent+1);
return res;
}
inline bool Command::runScript(Logger& log, Command* parentCommand,
std::shared_ptr<Script> script,
Script* parent, QWebFrame* frame,
QStringList vars,
QStringList args) {
Script scriptCopy(*script); // only work with a copy of script
scriptCopy.set(*parent);
scriptCopy.parent(parent);
if (args.size()!=vars.size())
error(log, WrongNumberOfArguments(vars, args));
for (QStringList::iterator var(vars.begin()), arg(args.begin());
var<vars.end() && arg<args.end(); ++var, ++arg) {
parent->log("argument: "+*var+" = "+parent->replacevars(*arg),
parentCommand);
scriptCopy.set(*var, parent->replacevars(*arg));
}
try {
assert(connect(&scriptCopy, SIGNAL(logging(QString)),
parent, SLOT(parentlog(QString))));
assert(connect(&scriptCopy, SIGNAL(progress(QString, int, int, int)),
parent, SLOT(innerProgress(QString, int, int))));
parent->removeSignals(frame);
bool res(scriptCopy.run(frame));
parent->addSignals(frame);
disconnect(&scriptCopy, SIGNAL(progress(QString, int, int, int)),
parent, SLOT(innerProgress(QString, int, int)));
disconnect(&scriptCopy, SIGNAL(logging(QString)),
parent, SLOT(parentlog(QString)));
parentCommand->_result = scriptCopy.result();
for (QString key: scriptCopy.variables()) // copy new variables to parent
if (!vars.contains(key)) parent->set(key, scriptCopy.variable(key));
parent->ignore(scriptCopy); // copy ignore list
if (parentCommand->_result.size())
parent->log("result: "+parentCommand->_result);
return res;
} catch (const Exception& x) {
parent->addSignals(frame);
disconnect(&scriptCopy, SIGNAL(progress(QString, int, int, int)),
parent, SLOT(innerProgress(QString, int, int)));
disconnect(&scriptCopy, SIGNAL(logging(QString)),
parent, SLOT(parentlog(QString)));
throw;
}
}
inline void Script::initPrototypes() {
add(new Do);
add(new Load);
add(new Expect);
add(new Screenshot);
add(new Open);
add(new Sleep);
add(new Exit);
add(new IgnoreTo);
add(new Label);
add(new Upload);
add(new Exists);
add(new Not);
add(new Execute);
add(new Download);
add(new Click);
add(new Set);
add(new UnSet);
add(new Timeout);
add(new TimeoutFalse);
add(new CaCertificate);
add(new ClientCertificate);
add(new ::ClickType);
add(new SetValue);
add(new Function);
add(new Call);
add(new If);
add(new While);
add(new TestSuite);
add(new TestCase);
add(new Check);
add(new For);
add(new Echo);
add(new OfflineStoragePath);
add(new ClearCookies);
add(new Include);
add(new Case);
add(new Fail);
add(new Auth);
add(new Ignore);
add(new UnIgnore);
}
#endif