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.
3382 lines
116 KiB
3382 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(">", ">").replace("<", "<") |
|
.replace("<br/>", "\n").replace(""", "\"") |
|
.replace(" ", " ").replace("&", "&"); |
|
} |
|
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("&", "&") |
|
.replace("<", "<") |
|
.replace(">", ">") |
|
.replace(QRegularExpression("<([-_A-Za-z0-9]+)>"), "<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/>") |
|
.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."; |
|
} |
|
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
|
|
|