2810 lines
94 KiB
C++
2810 lines
94 KiB
C++
/*! @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 <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 <vector>
|
|
#include <queue>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <sstream>
|
|
#include <cassert>
|
|
#include <xml-cxx/xml.hxx>
|
|
|
|
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;
|
|
Command* _previous;
|
|
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;
|
|
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(QWebFrame* frame, QString selector) {
|
|
QWebElement element(find(frame, selector));
|
|
if (element.isNull()) throw ElementNotFound(selector);
|
|
realMouseClick(element);
|
|
}
|
|
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:
|
|
bool runScript(Command* parentCommand,
|
|
std::shared_ptr<Script> script,
|
|
Script* parent, QWebFrame* frame,
|
|
QStringList vars = QStringList(),
|
|
QStringList args = QStringList());
|
|
QStringList subCommandBlock(QStringList& in) {
|
|
QStringList commands;
|
|
int pos(-1);
|
|
while (in.size() && in[0].size() && in[0][0]==' '
|
|
&& pos<=(signed)in[0].toStdString().find_first_not_of(' ')) {
|
|
if (pos<0) pos=in[0].toStdString().find_first_not_of(' ');
|
|
commands += in.takeFirst().mid(pos);
|
|
}
|
|
return commands;
|
|
}
|
|
QStringList commaSeparatedList(QString value) {
|
|
value=value.trimmed();
|
|
if (!value.size()) return QStringList();
|
|
switch (value.size()>1&&value.at(0)==value.at(value.size()-1)
|
|
?value.at(0).toLatin1():'\0') {
|
|
case '"': case '\'': {
|
|
return value.mid(1, value.size()-2)
|
|
.split(QRegularExpression(QString(value[0])+", *"
|
|
+QString(value[0])));
|
|
} break;
|
|
default: {
|
|
return value.split(QRegularExpression(", *"));
|
|
}
|
|
}
|
|
}
|
|
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 = 5, int sleepsec = 1) {
|
|
QWebElement element;
|
|
for (int i=0; i<repeat; ++i) {
|
|
element = frame->findFirstElement(selector);
|
|
if (!element.isNull()) return element;
|
|
Q_FOREACH(QWebFrame* childFrame, frame->childFrames()) {
|
|
element = find1(childFrame, selector, 1, 0);
|
|
if (!element.isNull()) return element;
|
|
}
|
|
if (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"
|
|
"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"
|
|
"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(int line, QString target, QString base,
|
|
QString name, QWebFrame* frame) {
|
|
if (!QDir(target).exists() && !QDir().mkpath(target))
|
|
throw 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))
|
|
throw CannotWriteSouceHTML(filename);
|
|
QTextStream out(&file);
|
|
out<<frame->toHtml();
|
|
if (out.status()!=QTextStream::Ok) throw CannotWriteSouceHTML(filename);
|
|
return QDir(filename).absolutePath();
|
|
}
|
|
static QString screenshot(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))
|
|
throw 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)) throw CannotWriteScreenshot(filename);
|
|
return QDir(filename).absolutePath();
|
|
}
|
|
QString tag() const {
|
|
return "screenshot";
|
|
}
|
|
QString description() const {
|
|
return
|
|
tag()+" <filename-base>"
|
|
"\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) {
|
|
connect(_reply, SIGNAL(finished()), SLOT(finished()));
|
|
connect(_reply, SIGNAL(downloadProgress(qint64, qint64)),
|
|
SLOT(downloadProgress(qint64, qint64)));
|
|
_file.open(QIODevice::WriteOnly);
|
|
}
|
|
~RunDownload() {
|
|
disconnect(_reply, SIGNAL(finished()), this, SLOT(finished()));
|
|
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);
|
|
public:
|
|
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(">", ">").replace("<", "<")
|
|
.replace("<br/>", "\n").replace(""", "\"")
|
|
.replace(" ", " ").replace("&", "&");
|
|
}
|
|
public:
|
|
Script(): _clicktype(JAVASCRIPT_CLICK), _command(0), _screenshots(true) {
|
|
initPrototypes();
|
|
}
|
|
Script(const Script& o):
|
|
QObject(),
|
|
_prototypes(o._prototypes),
|
|
_script(o._script),
|
|
_command(0),
|
|
_screenshots(true) {
|
|
set(o);
|
|
}
|
|
QString syntax() const {
|
|
return
|
|
"Script syntax is a text file that consists of 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 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 cab 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://dev.marc.waeckerlin.org/redmine/projects/bootstrap-build-environment";
|
|
}
|
|
/// set workdir
|
|
void path(QString path) {
|
|
_path = (path.size()?path:".")+QDir::separator();
|
|
}
|
|
/// get workdir
|
|
QString path() {
|
|
return _path;
|
|
}
|
|
QString commands(Formatting f = PLAIN) const {
|
|
QString cmds;
|
|
for (auto it(_prototypes.begin()); it!=_prototypes.end(); ++it)
|
|
switch (f) {
|
|
case PLAIN: {
|
|
cmds+="\n\n\nCOMMAND: "+it->first+"\n\n"+it->second->description();
|
|
} break;
|
|
case HTML: {
|
|
cmds+="<h1>"+it->first+"</h1><p>"
|
|
+it->second->description()
|
|
.replace("&", "&")
|
|
.replace("<", "<")
|
|
.replace(">", ">")
|
|
.replace(QRegularExpression("<([^ ]+)>"),
|
|
"<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 ")
|
|
.replace("\n\n", "</p><p>")
|
|
.replace("\n", "<br/>")
|
|
+"</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();
|
|
_variables.clear();
|
|
_rvariables.clear();
|
|
_functions.clear();
|
|
_timeout = 20;
|
|
_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.line(linenr);
|
|
e.file(filename);
|
|
throw;
|
|
}
|
|
void parse(QStringList in, QString filename, int line = 1, int indent = 0) {
|
|
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);
|
|
_testsuites = testsuites;
|
|
_timeout = 20; // defaults to 20s
|
|
_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(); ++cmd) {
|
|
xml::Node testcase("testcase");
|
|
try {
|
|
testcase.attr("classname") = xmlattr(_testclass, true).toStdString();
|
|
testcase.attr("name") =
|
|
xmlattr((*cmd)->command(), true).toStdString();
|
|
if (!_ignores.size() || (*cmd)->tag()=="label") { // not ignored
|
|
_timer.start(_timeout*1000);
|
|
try {
|
|
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
|
|
}
|
|
} catch (PossibleRetryLoad& e) {
|
|
_timer.stop();
|
|
// timeout may happen during load due to bad internet connection
|
|
if (screenshots)
|
|
try { // take a screenshot on error
|
|
QString filename(Screenshot::screenshot
|
|
((*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;
|
|
back += 3;
|
|
_ignoreSignalsUntil = "loadStarted";
|
|
frame->load(url);
|
|
} else if ((*cmd)->command()=="expect loadStarted") {
|
|
----cmd;
|
|
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;
|
|
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;
|
|
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.line((*cmd)->line());
|
|
e.file((*cmd)->file());
|
|
if (screenshots)
|
|
try { // write html source and take a last screenshot on error
|
|
{
|
|
QString filename(Screenshot::sourceHtml
|
|
((*cmd)->line(), targetdir(),
|
|
_testclass,
|
|
"error", frame));
|
|
plainlog("[[ATTACHMENT|"+filename+"]]");
|
|
} {
|
|
QString filename(Screenshot::screenshot
|
|
((*cmd)->line(), targetdir(),
|
|
_testclass,
|
|
"error", frame));
|
|
plainlog("[[ATTACHMENT|"+filename+"]]");
|
|
}
|
|
} catch (... ) {} // ignore exception in screenshot
|
|
throw;
|
|
}
|
|
}
|
|
removeSignals(frame);
|
|
if (!_signals.empty()) throw UnhandledSignals(_signals);
|
|
return res;
|
|
}
|
|
Command* command() {
|
|
return _command;
|
|
}
|
|
void command(Command* cmd) {
|
|
_command = cmd;
|
|
}
|
|
QString& cout() {
|
|
return _cout;
|
|
}
|
|
QString& cerr() {
|
|
return _cerr;
|
|
}
|
|
int steps() {
|
|
return _script.size();
|
|
}
|
|
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;
|
|
}
|
|
Signal getSignal() {
|
|
while (!_signals.size()) QCoreApplication::processEvents();
|
|
Signal res(_signals.front());
|
|
_signals.pop();
|
|
return res;
|
|
}
|
|
QTimer& timer() {
|
|
return _timer;
|
|
}
|
|
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()) throw VariableNotFound(name);
|
|
return *it;
|
|
}
|
|
/// Copy context from other script
|
|
void set(const Script& o) {
|
|
_variables = o._variables;
|
|
_rvariables = o._rvariables;
|
|
_timeout = o._timeout;
|
|
_clicktype = o._clicktype;
|
|
_testsuites = o._testsuites;
|
|
_testclass = o._testclass;
|
|
_targetdir = o._targetdir;
|
|
_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()) throw FunctionNotFound(name);
|
|
return *it;
|
|
}
|
|
void timeout(int t) {
|
|
_timeout = t;
|
|
}
|
|
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;
|
|
}
|
|
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) {
|
|
connect(dynamic_cast<NetworkAccessManager*>
|
|
(frame->page()->networkAccessManager()),
|
|
SIGNAL(log(QString)),
|
|
SLOT(log(QString)));
|
|
connect(frame, SIGNAL(contentsSizeChanged(const QSize&)),
|
|
SLOT(contentsSizeChanged(const QSize&)));
|
|
connect(frame, SIGNAL(iconChanged()),
|
|
SLOT(iconChanged()));
|
|
connect(frame, SIGNAL(initialLayoutCompleted()),
|
|
SLOT(initialLayoutCompleted()));
|
|
connect(frame, SIGNAL(javaScriptWindowObjectCleared()),
|
|
SLOT(javaScriptWindowObjectCleared()));
|
|
connect(frame, SIGNAL(loadFinished(bool)),
|
|
SLOT(loadFinished(bool)));
|
|
connect(frame, SIGNAL(loadStarted()),
|
|
SLOT(loadStarted()));
|
|
connect(frame, SIGNAL(titleChanged(const QString&)),
|
|
SLOT(titleChanged(const QString&)));
|
|
connect(frame, SIGNAL(urlChanged(const QUrl&)),
|
|
SLOT(urlChanged(const QUrl&)));
|
|
connect(&_timer, SIGNAL(timeout()), SLOT(timeout()));
|
|
}
|
|
void removeSignals(QWebFrame* frame) {
|
|
disconnect(dynamic_cast<NetworkAccessManager*>
|
|
(frame->page()->networkAccessManager()),
|
|
SIGNAL(log(QString)),
|
|
this, SLOT(log(QString)));
|
|
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(timeout()));
|
|
}
|
|
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);
|
|
for (QChar& c: text) if (c<32) c='?';
|
|
if (cmd)
|
|
prefix += QString("%2:%3%1 ")
|
|
.arg(QString(cmd->indent(), QChar(' ')))
|
|
.arg(cmd->file(), 20, QChar(' '))
|
|
.arg(cmd->line(), -4, 10, QChar(' '));
|
|
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:
|
|
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 contentsSizeChanged(const QSize&) {
|
|
}
|
|
void iconChanged() {
|
|
}
|
|
void initialLayoutCompleted() {
|
|
}
|
|
void javaScriptWindowObjectCleared() {
|
|
}
|
|
void loadFinished(bool ok) {
|
|
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 (_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() {
|
|
}
|
|
void titleChanged(const QString&) {
|
|
//_signals.push(std::make_pair("titleChanged", QStringList(title)));
|
|
}
|
|
void urlChanged(const QUrl& url) {
|
|
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 timeout() {
|
|
throw TimeOut();
|
|
}
|
|
private:
|
|
typedef std::map<QString, std::shared_ptr<Command>> Prototypes;
|
|
typedef std::vector<std::shared_ptr<Command>> Commands;
|
|
Prototypes _prototypes;
|
|
Commands _script;
|
|
std::queue<Signal> _signals;
|
|
QTimer _timer;
|
|
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;
|
|
ClickType _clicktype;
|
|
QString _targetdir;
|
|
std::shared_ptr<xml::Node> _testsuites; ///< only valid within run
|
|
QString _testclass;
|
|
Command* _command;
|
|
QString _path;
|
|
};
|
|
|
|
class Do: public Command {
|
|
public:
|
|
QString tag() const {
|
|
return "do";
|
|
}
|
|
QString description() const {
|
|
return
|
|
tag()+" [<selector>]\n <javascript-line1>\n <javascript-line2>"
|
|
"\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));
|
|
if (element.isNull())
|
|
throw 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"
|
|
"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"
|
|
"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\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);
|
|
QList<Signal> sigs;
|
|
if (_signal._signal=="load") { // special signal load
|
|
sigs += Signal("loadStarted");
|
|
sigs += Signal("urlChanged", _signal._args);
|
|
sigs += Signal("loadFinished", "true");
|
|
} else {
|
|
sigs += _signal;
|
|
}
|
|
Q_FOREACH(Signal signal, sigs) {
|
|
QStringList args;
|
|
Q_FOREACH(QString arg, signal._args)
|
|
args.push_back(script->replacevars(arg));
|
|
Script::Signal lastsignal(script->getSignal());
|
|
QStringList lastargs;
|
|
Q_FOREACH(QString arg, lastsignal.second)
|
|
lastargs.push_back(script->replacevars(arg));
|
|
if (lastsignal.first!=signal._signal || (args.size() && args!=lastargs))
|
|
throw WrongSignal(signal._signal, args, lastsignal);
|
|
}
|
|
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"
|
|
"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"
|
|
"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)
|
|
throw 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"
|
|
"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"
|
|
"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"
|
|
"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"
|
|
"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(frame, script->replacevars(_selector));
|
|
if (page->uploadPrepared())
|
|
throw 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"
|
|
"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;
|
|
Q_FOREACH(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();
|
|
}
|
|
QWebElement element(find(frame, selector));
|
|
if (text.isEmpty())
|
|
throw AssertionFailed("element not found: "+selector);
|
|
else if (element.isNull())
|
|
throw AssertionFailed("expected \""+text+"\" in non existing element "
|
|
+selector);
|
|
else
|
|
throw AssertionFailed("not found \""+text+"\" in \""
|
|
+notfound.join("\", \"")+"\" on "+selector);
|
|
return true; // never reached due to throw 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"
|
|
"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));
|
|
Q_FOREACH(QWebElement element, frame->findAllElements(selector)) {
|
|
if (text.isEmpty())
|
|
throw AssertionFailed("element must not exists: "+selector);
|
|
if (element.toOuterXml().indexOf(text)!=-1)
|
|
throw 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"
|
|
"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")));
|
|
Q_FOREACH(QString arg, _args) args.push_back(script->replacevars(arg));
|
|
QProcess exec;
|
|
exec.setProcessChannelMode(QProcess::MergedChannels);
|
|
exec.start(command, args);
|
|
if (!exec.waitForStarted())
|
|
throw CannotStartScript(command, args, scripttxt);
|
|
if (scripttxt.size()) {
|
|
if (exec.write(scripttxt.toUtf8())!=scripttxt.toUtf8().size() ||
|
|
!exec.waitForBytesWritten(60000))
|
|
throw CannotLoadScript(command, args, scripttxt);
|
|
}
|
|
exec.closeWriteChannel();
|
|
if (!exec.waitForFinished(60000) && exec.state()!=QProcess::NotRunning)
|
|
throw ScriptNotFinished(command, args, scripttxt);
|
|
QString stdout(exec.readAllStandardOutput());
|
|
QString stderr(exec.readAllStandardError());
|
|
_result = stdout;
|
|
log("result: "+(_result.size()?_result:"(void)"));
|
|
script->log(stdout);
|
|
if (exec.exitCode()!=0 || exec.exitStatus()!=QProcess::NormalExit)
|
|
throw ScriptExecutionFailed(command, args, scripttxt,
|
|
exec.exitCode(), stdout, stderr);
|
|
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"
|
|
"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);
|
|
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) throw DownloadFailed(_realfilename);
|
|
if (!_filesuccess) throw 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;
|
|
}
|
|
}
|
|
}
|
|
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"
|
|
"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(frame, clicktarget);
|
|
break;
|
|
}
|
|
case Script::JAVASCRIPT_CLICK:
|
|
default: {
|
|
QWebElement element(find(frame, clicktarget));
|
|
if (element.isNull())
|
|
throw 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"+
|
|
tag()+" <variable>\n"
|
|
" <command>"
|
|
"\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.";
|
|
}
|
|
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"
|
|
"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"
|
|
"Set the timeout in seconds (defaults to 10).";
|
|
}
|
|
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) throw BadArgument(script->replacevars(_timeout)
|
|
+" should be a number of seconds");
|
|
script->timeout(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"
|
|
"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))
|
|
throw FileNotFound(filename);
|
|
QSslCertificate cacert(&cacertfile);
|
|
if (cacert.isNull()) throw 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"
|
|
"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))
|
|
throw FileNotFound(filename);
|
|
QSslCertificate cert(&certfile);
|
|
if (cert.isNull()) throw 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))
|
|
throw FileNotFound(filename);
|
|
keyfile.open(QIODevice::ReadOnly);
|
|
QSslKey k(&keyfile, QSsl::Rsa, QSsl::Pem,
|
|
QSsl::PrivateKey, script->replacevars(_password).toUtf8());
|
|
if (k.isNull()) throw 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"
|
|
"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"+
|
|
tag()+" <selector> -> '<value1>', '<value2>', <...>"
|
|
"\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)));
|
|
if (element.isNull())
|
|
throw 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));
|
|
Q_FOREACH(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 Command {
|
|
public:
|
|
QString tag() const {
|
|
return "function";
|
|
}
|
|
QString description() const {
|
|
return
|
|
tag()+" <name> [<var1>, <var2>, <...>]\n"
|
|
" <command1>\n"
|
|
" <command2>\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 = std::shared_ptr<Script>(new Script);
|
|
cmd->_script->parse(subCommandBlock(in), file, line+1, indent+1);
|
|
return cmd;
|
|
}
|
|
bool execute(Script* script, QWebFrame*) {
|
|
Logger log(this, script, false);
|
|
return true;
|
|
}
|
|
bool call(Command* parentCommand, QStringList args,
|
|
Script* script, QWebFrame* frame) {
|
|
try {
|
|
return runScript(parentCommand, _script, script, frame, _vars, args);
|
|
} catch (const std::exception& x) {
|
|
throw FunctionCallFailed(_name, _vars, args, x);
|
|
}
|
|
}
|
|
private:
|
|
QString _name;
|
|
QStringList _vars;
|
|
std::shared_ptr<Script> _script;
|
|
};
|
|
|
|
class Call: public Command {
|
|
public:
|
|
QString tag() const {
|
|
return "call";
|
|
}
|
|
QString description() const {
|
|
return
|
|
tag()+" <name> ['<arg1>', '<arg2>', ...]"
|
|
"\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(this, _args, script, frame);
|
|
}
|
|
public:
|
|
QString _name;
|
|
QStringList _args;
|
|
};
|
|
|
|
class If: public Command {
|
|
public:
|
|
QString tag() const {
|
|
return "if";
|
|
}
|
|
QString description() const {
|
|
return
|
|
tag()+" <variable> <cmp> <value>\n"
|
|
" <command1>\n"
|
|
" <command2>\n"
|
|
" <...>\n"
|
|
"else\n"
|
|
" <command3>\n"
|
|
" <command4>\n"
|
|
" <...>\n"
|
|
"\n\n"+
|
|
tag()+" <selector> -> <text>\n"
|
|
" <command1>\n"
|
|
" <command2>\n"
|
|
" <...>\n"
|
|
"else\n"
|
|
" <command3>\n"
|
|
" <command4>\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.";
|
|
}
|
|
QString command() const {
|
|
return tag()+" "+_variable+" "+_cmp+" "+_value
|
|
+(_script.get()?"\n "+_script->print().join("\n "):"");
|
|
}
|
|
std::shared_ptr<Command> parse(Script*, QString args,
|
|
QStringList& in, QString file, int line,
|
|
int indent) {
|
|
std::shared_ptr<If> cmd(new If());
|
|
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 = std::shared_ptr<Script>(new Script);
|
|
cmd->_script->parse(subCommandBlock(in), file, line+1, indent+1);
|
|
if (in.size() && in.first().contains(QRegularExpression("^ *else *$"))) {
|
|
in.removeFirst();
|
|
cmd->_else = std::shared_ptr<Script>(new Script);
|
|
cmd->_else->parse(subCommandBlock(in), file, line+1, indent+1);
|
|
}
|
|
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=="->") {
|
|
Q_FOREACH(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+" "+_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:;
|
|
}
|
|
log(QString("evaluated expression to ")+(check?"true":"false")+": "
|
|
+script->variable(_variable)+" "+_cmp+" "+value);
|
|
}
|
|
if (check) return runScript(this, _script, script, frame);
|
|
else if (_else) return runScript(this, _else, script, frame);
|
|
return true;
|
|
}
|
|
private:
|
|
QString _variable;
|
|
QString _cmp;
|
|
QString _value;
|
|
std::shared_ptr<Script> _script;
|
|
std::shared_ptr<Script> _else;
|
|
};
|
|
|
|
class TestSuite: public Command {
|
|
public:
|
|
QString tag() const {
|
|
return "testsuite";
|
|
}
|
|
QString description() const {
|
|
return
|
|
tag()+" <name>"
|
|
"\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* frame) {
|
|
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"
|
|
"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* frame) {
|
|
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"+
|
|
tag()+" <value1>\n"
|
|
" <command>"
|
|
"\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;
|
|
int pos(args.indexOf(QRegularExpression("[=!.^~<>]")));
|
|
if (pos<0) throw BadArgument(tag()+" needs a comparision, not: "+args);
|
|
cmd->_value1 = args.left(pos).trimmed();
|
|
cmd->_cmp = args[pos].toLatin1();
|
|
cmd->_value2 = args.mid(pos+1).trimmed();
|
|
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
|
|
}
|
|
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) throw CheckFailed(value1, _cmp, value2);
|
|
return true;
|
|
}
|
|
private:
|
|
QString _value1;
|
|
QString _value2;
|
|
char _cmp;
|
|
std::shared_ptr<Command> _next;
|
|
};
|
|
|
|
class For: public Command {
|
|
public:
|
|
QString tag() const {
|
|
return "for";
|
|
}
|
|
QString description() const {
|
|
return
|
|
tag()+" <variable> -> <val1>, <val2>, <...>\n"
|
|
" <command1>\n"
|
|
" <command2>\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*, 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 = std::shared_ptr<Script>(new Script);
|
|
cmd->_script->parse(subCommandBlock(in), file, line+1, indent+1);
|
|
return cmd;
|
|
}
|
|
bool execute(Script* script, QWebFrame* frame) {
|
|
Logger log(this, script);
|
|
Q_FOREACH(QString i, _vals.size()?_vals:commaSeparatedList(script->variable(_variable))) {
|
|
if (!runScript(this, _script, script, frame, QStringList()<<_variable, QStringList()<<i))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
private:
|
|
QString _variable;
|
|
QStringList _vals;
|
|
std::shared_ptr<Script> _script;
|
|
};
|
|
|
|
class Echo: public Command {
|
|
public:
|
|
QString tag() const {
|
|
return "echo";
|
|
}
|
|
QString description() const {
|
|
return
|
|
tag()+" <text>"
|
|
"\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* frame) {
|
|
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"
|
|
"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))
|
|
throw 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"
|
|
"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();
|
|
Q_FOREACH(QNetworkCookie cookie, cookies->cookiesForUrl(url)) {
|
|
log("delete cookie "+cookie.name());
|
|
cookies->deleteCookie(cookie);
|
|
}
|
|
return true;
|
|
}
|
|
private:
|
|
QString _url;
|
|
};
|
|
|
|
class Include: public Command {
|
|
public:
|
|
QString tag() const {
|
|
return "include";
|
|
}
|
|
QString description() const {
|
|
return
|
|
tag()+" <filename>"
|
|
"\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 = std::shared_ptr<Script>(new Script);
|
|
cmd->_script->parse(txt.split('\n'), cmd->_filename, 1, indent+1);
|
|
} catch (std::exception& e) {
|
|
throw ParseIncludeFailed(cmd->_filename, e.what());
|
|
}
|
|
return cmd;
|
|
}
|
|
bool execute(Script* script, QWebFrame* frame) try {
|
|
Logger log(this, script);
|
|
return runScript(this, _script, script, frame);
|
|
} catch (std::exception& e) {
|
|
throw ExecuteIncludeFailed(_filename, e.what());
|
|
}
|
|
private:
|
|
QString _filename;
|
|
std::shared_ptr<Script> _script;
|
|
};
|
|
|
|
class Case: public Command {
|
|
public:
|
|
QString tag() const {
|
|
return "case";
|
|
}
|
|
QString description() const {
|
|
return
|
|
tag()+" <variable>\n"
|
|
" <cmp1> <value1>\n"
|
|
" <command>\n"
|
|
" <command>\n"
|
|
" <cmp1> <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"
|
|
"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;
|
|
Q_FOREACH(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*, 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));
|
|
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> script(std::shared_ptr<Script>(new Script));
|
|
script->parse(subCommandBlock(body), file, line+1, indent+2);
|
|
cmd->_conditions.append(Condition(cmp, value, script));
|
|
}
|
|
return cmd;
|
|
}
|
|
bool execute(Script* script, QWebFrame* frame) {
|
|
Logger log(this, script, false);
|
|
QString selector(script->replacevars(_variable));
|
|
Q_FOREACH(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=="->") {
|
|
Q_FOREACH(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(this, condition.script, script, frame);
|
|
}
|
|
return true;
|
|
}
|
|
private:
|
|
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"
|
|
"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);
|
|
throw TestFailed(script->replacevars(_text));
|
|
return true; // dummy
|
|
}
|
|
private:
|
|
QString _text;
|
|
};
|
|
|
|
|
|
/* Template:
|
|
class : public Command {
|
|
public:
|
|
QString tag() const {
|
|
return "";
|
|
}
|
|
QString description() const {
|
|
return
|
|
tag()+
|
|
"\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(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) {
|
|
_previous = _script->command();
|
|
_script->command(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->log()) _script->log(" "+txt, _command);
|
|
}
|
|
inline void Logger::operator[](QString txt) {
|
|
_script->plainlog(txt);
|
|
}
|
|
inline Logger::~Logger() {
|
|
if (_command->log()) _script->log("/ "+_command->tag(), _command);
|
|
_script->command(_previous);
|
|
}
|
|
|
|
inline bool Command::runScript(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);
|
|
if (args.size()!=vars.size())
|
|
throw 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 {
|
|
connect(&scriptCopy, SIGNAL(logging(QString)),
|
|
parent, SLOT(parentlog(QString)));
|
|
parent->removeSignals(frame);
|
|
bool res(scriptCopy.run(frame));
|
|
parent->addSignals(frame);
|
|
disconnect(&scriptCopy, SIGNAL(logging(QString)),
|
|
parent, SLOT(parentlog(QString)));
|
|
parentCommand->_result = scriptCopy.result();
|
|
Q_FOREACH(QString key, scriptCopy.variables()) // copy new variables to parent
|
|
if (!vars.contains(key)) parent->set(key, scriptCopy.variable(key));
|
|
Q_FOREACH(QString key, scriptCopy.functions()) // copy new functions to parent
|
|
parent->function(key, scriptCopy.function(key));
|
|
if (parentCommand->_result.size())
|
|
parent->log("result: "+parentCommand->_result);
|
|
return res;
|
|
} catch (const std::exception& x) {
|
|
parent->addSignals(frame);
|
|
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 CaCertificate);
|
|
add(new ClientCertificate);
|
|
add(new ::ClickType);
|
|
add(new SetValue);
|
|
add(new Function);
|
|
add(new Call);
|
|
add(new If);
|
|
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);
|
|
}
|
|
|
|
#endif
|