new option for timeout, longer timeout in shell, new untested command auth to set basic authentication credentials

master
Marc Wäckerlin 7 years ago
parent aa2fc7a98b
commit 6e8e665da7
  1. 2
      COPYING
  2. 2
      INSTALL
  3. 33
      README
  4. 1
      configure.ac
  5. 233
      src/commands.hxx
  6. 6
      src/networkaccessmanager.hxx
  7. 1
      src/testgui.hxx
  8. 6
      src/webrunner.cxx

@ -1 +1 @@
/usr/share/automake-1.15/COPYING
/usr/share/automake-1.14/COPYING

@ -1 +1 @@
/usr/share/automake-1.15/INSTALL
/usr/share/automake-1.14/INSTALL

@ -1,18 +1,25 @@
Test Your Web Application: GUI Web Testing Environment + Script Runner
Webtester consists of two binaries: webtester to interactively create your web application tests and webrunner to run your test scripts.
# Framework for Automated Web Application Testing
There is a test GUI including browser to record user input while he surfs on the web and a test runner to run (recorded) test scripts. The tests can be integrated e.g. in a jenkins build job. It has been tested on Wordpress, Dokuwiki and Joomla pages. Joomla is difficult due to Javascript-Moo-Tools pollution. There's some specific support, that may help a bit, but to test Joomla sites, you need a lot of experience. Concluson: Avoid Joomla.
Sample Script to search my old homepage on Google, klick on the link, there click on tab «Computer» and check the title for the text «Marcs Computerblog»:
load https://google.com
expect load
setvalue input[name="q"] -> 'Marc Wäckerlin';
click input[name="btnG"]
expect load
click a[href^="/url?q=https://marc.waeckerlin.org/&"]
expect load https://marc.waeckerlin.org/doku.php
click a[href="/computer/index"]
expect load https://marc.waeckerlin.org/computer/index
exists h1.sectionedit1 -> Marcs Computerblog
load https://google.com
expect loadStarted
expect urlChanged
expect loadFinished true
do input[name="q"]
this.value='Marc Wäckerlin';
click input[name="btnG"]
expect loadStarted
expect urlChanged
expect loadFinished true
click a[href^="/url?q=https://marc.waeckerlin.org/&"]
expect loadStarted
expect urlChanged https://marc.waeckerlin.org/doku.php
expect loadFinished true
click a[href="/computer/index"]
expect loadStarted
expect urlChanged https://marc.waeckerlin.org/computer/index
expect loadFinished true
exists h1.sectionedit1 -> Marcs Computerblog

@ -41,6 +41,7 @@ AM_CPPFLAGS="${AM_CPPFLAGS} -DQT_NO_KEYWORDS"
# libraries used
AX_PKG_REQUIRE([mrwcxx], [mrw-c++])
AX_PKG_REQUIRE([xmlcxx], [libxml-cxx])
AC_CHECK_HEADERS([cxxabi.h])
# create output
AC_OUTPUT

@ -10,6 +10,7 @@
#include <exceptions.hxx>
#include <webpage.hxx>
#include <QNetworkReply>
#include <QAuthenticator>
#include <QCoreApplication>
#include <QStringList>
#include <QWebFrame>
@ -32,6 +33,20 @@
#include <cassert>
#include <xml-cxx/xml.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
namespace std {
template<typename T> class optional {
private:
@ -99,7 +114,7 @@ class Command: public QObject {
QStringList&, QString, int, int) = 0;
virtual bool execute(Script*, QWebFrame*) = 0;
static void error(Logger& log, const Exception& e) {
log(QString(" FAILED: ")+e.what());
log(QString(" FAILED[")+demangle(typeid(e).name())+"]: "+e.what());
throw e;
}
void line(int linenr) {
@ -187,20 +202,38 @@ class Command: public QObject {
}
return commands;
}
QStringList commaSeparatedList(QString value) {
value=value.trimmed();
if (!value.size()) return QStringList();
switch (value.size()>1&&value.at(0)==value.at(value.size()-1)
?value.at(0).toLatin1():'\0') {
case '"': case '\'': {
return value.mid(1, value.size()-2)
.split(QRegularExpression(QString(value[0])+", *"
+QString(value[0])));
} break;
default: {
return value.split(QRegularExpression(", *"));
QStringList quotedStrings(QString value,
QString delimiter = " ",
bool keepDelimiters = false) {
QStringList res;
QString quot("'\"");
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.capturedLength()) res+=m.captured().mid(start).trimmed();
std::cout<<"REMOVE: \""<<m.captured()<<"\" 0 - "<<(m.capturedEnd()+start)
<<" start="<<start<<" pos="<<pos<<std::endl
<<"REMAINING: \""<<value<<"\""<<std::endl;
}
std::cout<<"FOUND"<<std::endl;
Q_FOREACH(QString tag, res) {
std::cout<<" - \""<<tag<<"\""<<std::endl;
}
return res;
}
QStringList commaSeparatedList(QString value) {
return quotedStrings(value, ",");
}
static QWebElement find(QWebFrame* frame, QString selector,
int repeat = 2, int sleepsec = 1) {
@ -376,15 +409,15 @@ class RunDownload: public QObject {
public:
RunDownload(QNetworkReply* reply, QString filename):
_reply(reply), _file(filename) {
connect(_reply, SIGNAL(finished()), SLOT(finished()));
connect(_reply, SIGNAL(downloadProgress(qint64, qint64)),
SLOT(downloadProgress(qint64, qint64)));
assert(connect(_reply, SIGNAL(finished()), SLOT(finished())));
assert(connect(_reply, SIGNAL(downloadProgress(qint64, qint64)),
SLOT(downloadProgress(qint64, qint64))));
_file.open(QIODevice::WriteOnly);
}
~RunDownload() {
disconnect(_reply, SIGNAL(finished()), this, SLOT(finished()));
disconnect(_reply, SIGNAL(downloadProgress(qint64, qint64)),
this, SLOT(downloadProgress(qint64, qint64)));
assert(disconnect(_reply, SIGNAL(finished()), this, SLOT(finished())));
assert(disconnect(_reply, SIGNAL(downloadProgress(qint64, qint64)),
this, SLOT(downloadProgress(qint64, qint64))));
delete _reply;
}
Q_SIGNALS:
@ -454,7 +487,8 @@ class Script: public QObject {
.replace("&nbsp;", " ").replace("&amp;", "&");
}
public:
Script(): _clicktype(JAVASCRIPT_CLICK), _command(0), _screenshots(true) {
Script(): _clicktype(JAVASCRIPT_CLICK), _command(0), _screenshots(true),
_defaultTimeout(20) {
initPrototypes();
}
Script(const Script& o):
@ -535,7 +569,7 @@ class Script: public QObject {
_variables.clear();
_rvariables.clear();
_functions.clear();
_timeout = 20;
_timeout = _defaultTimeout;
_clicktype = JAVASCRIPT_CLICK;
}
std::shared_ptr<Command> parseLine(QStringList& in,
@ -586,7 +620,7 @@ class Script: public QObject {
int maxretries = 0) {
bool res(true);
_testsuites = testsuites;
_timeout = 20; // defaults to 20s
_timeout = _defaultTimeout; // defaults to 20s
_ignoreSignalsUntil.clear();
addSignals(frame);
_screenshots = screenshots;
@ -815,6 +849,7 @@ class Script: public QObject {
_variables = o._variables;
_rvariables = o._rvariables;
_timeout = o._timeout;
_defaultTimeout = o._timeout;
_clicktype = o._clicktype;
_testsuites = o._testsuites;
_testclass = o._testclass;
@ -845,6 +880,15 @@ class Script: public QObject {
void timeout(int t) {
_timeout = t;
}
void defaultTimeout(int t) {
_defaultTimeout = 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;
}
@ -876,33 +920,39 @@ class Script: public QObject {
return QString();
}
void addSignals(QWebFrame* frame) {
connect(dynamic_cast<NetworkAccessManager*>
assert(connect(dynamic_cast<NetworkAccessManager*>
(frame->page()->networkAccessManager()),
SIGNAL(log(QString)),
SLOT(log(QString)));
connect(frame, SIGNAL(contentsSizeChanged(const QSize&)),
SLOT(contentsSizeChanged(const QSize&)));
connect(frame, SIGNAL(iconChanged()),
SLOT(iconChanged()));
connect(frame, SIGNAL(initialLayoutCompleted()),
SLOT(initialLayoutCompleted()));
connect(frame, SIGNAL(javaScriptWindowObjectCleared()),
SLOT(javaScriptWindowObjectCleared()));
connect(frame, SIGNAL(loadFinished(bool)),
SLOT(loadFinished(bool)));
connect(frame, SIGNAL(loadStarted()),
SLOT(loadStarted()));
connect(frame, SIGNAL(titleChanged(const QString&)),
SLOT(titleChanged(const QString&)));
connect(frame, SIGNAL(urlChanged(const QUrl&)),
SLOT(urlChanged(const QUrl&)));
connect(&_timer, SIGNAL(timeout()), SLOT(timeout()));
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(timeout())));
}
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()),
@ -931,9 +981,11 @@ class Script: public QObject {
for (QChar& c: text) if (c<32&&c!='\n') c='?';
if (cmd)
prefix += QString("%2:%3%1 ")
.arg(QString(cmd->indent(), QChar(' ')))
.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;
@ -950,7 +1002,7 @@ class Script: public QObject {
}
private:
void error(const Exception& e) {
log(QString(" FAILED: ")+e.what());
log(QString(" FAILED[")+demangle(typeid(e).name())+"]: "+e.what());
throw e;
}
std::shared_ptr<Command> unknown(QString command) {
@ -965,6 +1017,15 @@ class Script: public QObject {
_prototypes[c->tag()] = std::shared_ptr<Command>(c);
}
private Q_SLOTS:
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() {
@ -1012,6 +1073,10 @@ class Script: public QObject {
error(TimeOut());
}
private:
struct AuthRealm {
QString username;
QString password;
};
typedef std::map<QString, std::shared_ptr<Command>> Prototypes;
typedef std::vector<std::shared_ptr<Command>> Commands;
Prototypes _prototypes;
@ -1028,12 +1093,14 @@ class Script: public QObject {
QMap<LenString, LenString> _rvariables; ///< reverse variable mapping
QMap<QString, std::shared_ptr<Function> > _functions;
int _timeout;
int _defaultTimeout;
ClickType _clicktype;
QString _targetdir;
std::shared_ptr<xml::Node> _testsuites; ///< only valid within run
QString _testclass;
Command* _command;
QString _path;
QMap<QString, AuthRealm> _auth;
};
class Do: public Command {
@ -1587,8 +1654,8 @@ class Download: public Command {
_realfilename = script->replacevars(_filename);
log("REALFILENAME="+_realfilename);
frame->page()->setForwardUnsupportedContent(true);
connect(frame->page(), SIGNAL(unsupportedContent(QNetworkReply*)),
this, SLOT(unsupportedContent(QNetworkReply*)));
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
@ -1626,8 +1693,8 @@ class Download: public Command {
}
}
}
connect(new RunDownload(reply, _realfilename),
SIGNAL(completed(bool, bool)), SLOT(completed(bool, bool)));
assert(connect(new RunDownload(reply, _realfilename),
SIGNAL(completed(bool, bool)), SLOT(completed(bool, bool))));
}
private:
QString _filename;
@ -2312,14 +2379,22 @@ class Check: public Command {
int indent) {
std::shared_ptr<Check> cmd(new Check());
cmd->_next = 0;
int pos(args.indexOf(QRegularExpression("[=!.^~<>]")));
if (pos<0) throw BadArgument(tag()+" needs a comparision, not: "+args);
cmd->_value1 = args.left(pos).trimmed();
cmd->_cmp = args[pos].toLatin1();
cmd->_value2 = args.mid(pos+1).trimmed();
if (in.size() && in.first().contains(QRegularExpression("^ "))) {
cmd->_next = script->parseLine(in, file, line+1, indent+1);
cmd->_next->log(false); // suppress logging of subcommand
QString comp("[=!.^~<>]");
QStringList allargs = quotedStrings(args, comp, true);
if (allargs.size()<2 || allargs[1].size()!=1 ||
!QRegularExpression("^"+comp+"$").match(allargs[1]).hasMatch())
throw BadArgument(tag()+" needs a comparision, not: "+args);
if (allargs.size()>3)
throw BadArgument(tag()+" has at most three arguments");
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");
}
return cmd;
}
@ -2704,6 +2779,49 @@ class Fail: public Command {
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"
"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* frame) {
Logger log(this, script);
script->auth(_realm, _username, _password);
return true;
}
private:
QString _realm;
QString _username;
QString _password;
};
/* Template:
class : public Command {
@ -2786,8 +2904,8 @@ inline bool Command::runScript(Logger& log, Command* parentCommand,
scriptCopy.set(*var, parent->replacevars(*arg));
}
try {
connect(&scriptCopy, SIGNAL(logging(QString)),
parent, SLOT(parentlog(QString)));
assert(connect(&scriptCopy, SIGNAL(logging(QString)),
parent, SLOT(parentlog(QString))));
parent->removeSignals(frame);
bool res(scriptCopy.run(frame));
parent->addSignals(frame);
@ -2845,6 +2963,7 @@ inline void Script::initPrototypes() {
add(new Include);
add(new Case);
add(new Fail);
add(new Auth);
}
#endif

@ -60,13 +60,13 @@ class NetworkAccessManager: public QNetworkAccessManager {
return QNetworkAccessManager::configuration();
}
void connectToHost(const QString& hostName, quint16 port = 80) {
//log(__PRETTY_FUNCTION__);
//log(__PRETTY_FUNCTION__ + QString(" -> ") + hostName);
QNetworkAccessManager::connectToHost(hostName, port);
}
void connectToHostEncrypted(const QString& hostName, quint16 port = 443,
const QSslConfiguration& sslConfiguration
= QSslConfiguration::defaultConfiguration()) {
//log(__PRETTY_FUNCTION__);
//log(__PRETTY_FUNCTION__ + QString(" -> ") + hostName);
QNetworkAccessManager::connectToHostEncrypted(hostName, port,
sslConfiguration);
}
@ -171,7 +171,7 @@ class NetworkAccessManager: public QNetworkAccessManager {
virtual QNetworkReply* createRequest(Operation op,
const QNetworkRequest& req,
QIODevice* outgoingData = 0) {
//log(__PRETTY_FUNCTION__);
//log(__PRETTY_FUNCTION__ + QString(" -> ") + req.url().url());
switch (op) {
case QNetworkAccessManager::HeadOperation: break;
case QNetworkAccessManager::GetOperation: break;

@ -195,6 +195,7 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
//std::cout<<"titleChanged: "<<title.toStdString()<<std::endl;
}
void on__web_urlChanged(const QUrl& url) {
_url->setText(url.url());
enterText(true);
if (_record->isChecked())
appendCommand("expect "+map("urlChanged "+url.url()));

@ -115,6 +115,10 @@ int main(int argc, char *argv[]) try {
(QStringList()<<"r"<<"retries",
"on error retry up to <maxretries> times",
"maxretries", "0"));
parser.addOption(QCommandLineOption
(QStringList()<<"timeout",
"set default timeout in seconds",
"timeout", "40"));
parser.addOption(QCommandLineOption
(QStringList()<<"W"<<"width",
"set screenshot size to <width> pixel", "width", "2048"));
@ -141,8 +145,10 @@ int main(int argc, char *argv[]) try {
}
if (parser.isSet("path")) script.path(parser.value("path"));
int retries(parser.value("retries").toInt());
int timeout(parser.value("timeout").toInt());
int width(parser.value("width").toInt());
int height(parser.value("height").toInt());
script.defaultTimeout(parser.value("timeout").toInt());
QString target(parser.value("target-path"));
p.resize(width, height);
std::shared_ptr<xml::Node> testsuites(new xml::Node("testsuites"));

Loading…
Cancel
Save