More features: have a seperate setup script, where global variable replacements can be placed, new commandline parameters, command window, better recording, chose click type, ...

master
Marc Wäckerlin 9 years ago
parent 04c4c54089
commit 332db8b079
  1. 2
      configure.ac
  2. 15
      docker/Dockerfile
  3. 11
      docker/runtests.sh
  4. 344
      src/commands.hxx
  5. 257
      src/testgui.hxx
  6. 122
      src/testgui.ui
  7. 2
      src/webrunner.cxx
  8. 9
      src/webtester.cxx

@ -29,6 +29,8 @@ if test -z "${QMAKE}"; then
AC_MSG_ERROR([cannot find a qmake command])
fi
# requires mrw-c++ - to be defined ...
README=$(tail -n +3 README)
README_DEB=$(tail -n +3 README | sed -e 's/^$/./g' -e 's/^/ /g')
DESCRIPTION=$(head -1 README)

@ -0,0 +1,15 @@
FROM ubuntu:latest
MAINTAINER "Marc Wäckerlin"
RUN apt-get install -y wget software-properties-common apt-transport-https
RUN apt-add-repository https://dev.marc.waeckerlin.org/repository
RUN wget -O- https://dev.marc.waeckerlin.org/repository/PublicKey | apt-key add -
RUN apt-get update -y
RUN apt-get install -y xvfb webtester
ADD runtests.sh runtests.sh
VOLUME /tests
WORKDIR /tests
CMD /runtests.sh

@ -0,0 +1,11 @@
#!/bin/bash -e
if test -z "${WEBRUNNER_SCRIPTS}"; then
echo '*** ERROR: set at least -e WEBRUNNER_SCRIPTS="testfile.wt ..."'
echo " optional variables: XVFB_SERVER_ARGS, WEBRUNNER_ARGS"
exit 1
fi
xvfb-run -a --server-args="${XVFB_SERVER_ARGS:--screen 0 2048x2048x24}" \
webrunner ${WEBRUNNER_ARGS:--W 2048 -H 2048 -sx testoutput.xml} \
${WEBRUNNER_SCRIPTS}

@ -21,6 +21,7 @@
#include <QTimer>
#include <QProcess>
#include <QMouseEvent>
#include <QRegularExpression>
#include <vector>
#include <queue>
#include <map>
@ -35,8 +36,8 @@ class Command;
class Logger {
public:
Logger(Command* command, Script* script);
void plainlog(QString txt);
void log(QString txt);
void operator[](QString txt);
void operator()(QString txt);
~Logger();
private:
Command* _command;
@ -170,7 +171,7 @@ class Empty: public Command {
"Empty lines are allowed";
}
QString command() const {
return "";
return tag();
}
std::shared_ptr<Command> parse(Script*, QString, QStringList&, int) {
std::shared_ptr<Empty> cmd(new Empty());
@ -258,14 +259,14 @@ class Screenshot: public Command {
}
QString description() const {
return
"screenshot <filename-base>"
tag()+" <filename-base>"
"\n\n"
"Create a PNG screenshot of the actual web page and store it in the "
"file <filename-base>.png. If not already opened, a browser window "
"will pop up to take the screenshot.";
}
QString command() const {
return "screenshot "+_filename;
return tag()+" "+_filename;
}
std::shared_ptr<Command> parse(Script*, QString args, QStringList&, int) {
std::shared_ptr<Screenshot> cmd(new Screenshot());
@ -325,6 +326,25 @@ class Script: public QObject {
void logging(QString);
public:
typedef std::pair<QString, QStringList> Signal;
enum ClickType {
REAL_MOUSE_CLICK,
JAVASCRIPT_CLICK
};
enum Formatting {
PLAIN,
HTML
};
/// QString that sorts first by string length, then by content
class LenString: public QString {
public:
LenString() {}
LenString(const QString& o): QString(o) {}
virtual bool operator<(const LenString& o) const {
return
size()<o.size() ? true : o.size()<size() ? false
: QString::operator<(o.toLatin1());
}
};
public:
static QString xmlattr(QString attr, bool br = false) {
attr.replace("&", "&amp;")//.replace(" ", "&nbsp;")
@ -343,7 +363,7 @@ class Script: public QObject {
.replace("&nbsp;", " ").replace("&amp;", "&");
}
public:
Script() {
Script(): _clicktype(JAVASCRIPT_CLICK) {
initPrototypes();
}
QString syntax() const {
@ -355,10 +375,34 @@ class Script: public QObject {
"Note: When a selector is required as parameter, then the selector "
"is a CSS selector that must not contain spaces.";
}
QString commands() const {
QString commands(Formatting f = PLAIN) const {
QString cmds;
for (auto it(_prototypes.begin()); it!=_prototypes.end(); ++it)
cmds+="\n\n"+it->second->description();
switch (f) {
case PLAIN: {
cmds+="\n\n\nCOMMAND: "+it->first+"\n\n"+it->second->description();
} break;
case HTML: {
cmds+="<h1>"+it->first+"</h1><p>"
+it->second->description()
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace(QRegularExpression("&lt;([^ ]+)&gt;"),
"<i>\\1</i>")
.replace(QRegularExpression("(\n[^ ][^\n]*)(\n +-)"),
"\\1<ul>\\2")
.replace(QRegularExpression("(\n +-[^\n]*\n)([^ ])"),
"\\1</ul>\\2")
.replace(QRegularExpression("\n +- ([^\n]*)(</ul>)?"),
"<li>\\1</li>\\2")
.replace("</li>\n", "</li>")
.replace("\n ", "\n&nbsp;&nbsp;")
.replace("\n\n", "</p><p>")
.replace("\n", "<br/>")
+"</p>";
} break;
}
return cmds.trimmed();
}
void reset() {
@ -368,6 +412,14 @@ class Script: public QObject {
_ignores.clear();
_cout.clear();
_cerr.clear();
_ignoreSignalsUntil.clear();
}
void cleanup() {
reset();
_variables.clear();
_rvariables.clear();
_timeout = 20;
_clicktype = JAVASCRIPT_CLICK;
}
std::shared_ptr<Command> parse(QStringList& in, int linenr) try {
QString line(in.takeFirst().trimmed());
@ -555,19 +607,36 @@ class Script: public QObject {
}
void set(QString name, QString value) {
_variables[name] = value;
_rvariables[value] = name;
}
void unset(QString name) {
_rvariables.remove(_variables[name]);
_variables.remove(name);
}
void timeout(int t) {
_timeout = t;
}
void clicktype(ClickType c) {
_clicktype = c;
}
ClickType clicktype() {
return _clicktype;
}
QString replacevars(QString txt) {
for(QMap<QString, QString>::iterator it(_variables.begin());
it!=_variables.end(); ++it)
txt.replace(it.key(), it.value());
return txt;
}
QString insertvars(QString txt) {
QMapIterator<LenString, LenString> it(_rvariables);
it.toBack();
while (it.hasPrevious()) {
it.previous();
txt.replace(it.key(), it.value());
}
return txt;
}
public Q_SLOTS:
void log(QString text) {
text = QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss ")+text;
@ -703,8 +772,10 @@ class Script: public QObject {
QString _cerr;
bool _screenshots;
QString _ignoreSignalsUntil;
QMap<QString, QString> _variables;
QMap<QString, QString> _variables; ///< variable mapping
QMap<LenString, LenString> _rvariables; ///< reverse variable mapping
int _timeout;
ClickType _clicktype;
};
class Do: public Command {
@ -714,7 +785,7 @@ class Do: public Command {
}
QString description() const {
return
"do <selector>\n <javascript-line1>\n <javascript-line2>"
tag()+" <selector>\n <javascript-line1>\n <javascript-line2>"
"\n\n"
"Execute JavaScript on a CSS selected object. The object is the first "
"object in the DOM tree that matches the given CSS selector. You can "
@ -723,7 +794,7 @@ class Do: public Command {
"one space";
}
QString command() const {
return "do "+_selector+_javascript;
return tag()+" "+_selector+_javascript;
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList& in, int) {
@ -735,8 +806,9 @@ class Do: public Command {
}
bool execute(Script* script, QWebFrame* frame) {
Logger log(this, script);
QWebElement element(find(frame, _selector));
if (element.isNull()) throw ElementNotFound(_selector);
QWebElement element(find(frame, script->replacevars(_selector)));
if (element.isNull())
throw ElementNotFound(script->replacevars(_selector));
_result =
element.evaluateJavaScript(script->replacevars(_javascript)).toString();
return true;
@ -753,12 +825,12 @@ class Load: public Command {
}
QString description() const {
return
"load <url>"
tag()+" <url>"
"\n\n"
"Load an URL, the URL is given as parameter in full syntax.";
}
QString command() const {
return "load "+_url;
return tag()+" "+_url;
}
std::shared_ptr<Command> parse(Script*, QString args, QStringList&, int) {
std::shared_ptr<Load> cmd(new Load());
@ -781,7 +853,7 @@ class Expect: public Command {
}
QString description() const {
return
"expect <signal> [<parameter>]"
tag()+" <signal> [<parameter>]"
"\n\n"
"Expect a signal. Signals are emitted by webkit and may contain "
"parameter. If a parameter is given in the script, then the parameter "
@ -800,7 +872,7 @@ class Expect: public Command {
" - loadFinished true";
}
QString command() const {
return "expect "+_signal._signal
return tag()+" "+_signal._signal
+(_signal._args.size()?" "+_signal._args.join(' '):QString());
}
std::shared_ptr<Command> parse(Script*, QString args, QStringList&, int) {
@ -851,12 +923,12 @@ class Open: public Command {
}
QString description() const {
return
"open"
tag()+
"\n\n"
"Open the browser window, so you can follow the test steps visually.";
}
QString command() const {
return "open";
return tag();
}
std::shared_ptr<Command> parse(Script*, QString, QStringList&, int) {
std::shared_ptr<Open> cmd(new Open());
@ -876,14 +948,14 @@ class Sleep: public Command {
}
QString description() const {
return
"sleep <seconds>"
tag()+" <seconds>"
"\n\n"
"Sleep for a certain amount of seconds. This helps, if you must wait "
"for some javascript actions, i.e. AJAX or slow pages, and the "
"excpeted signals are not sufficient.";
}
QString command() const {
return "sleep "+_time;
return tag()+" "+_time;
}
std::shared_ptr<Command> parse(Script*, QString time, QStringList&, int) {
std::shared_ptr<Sleep> cmd(new Sleep());
@ -913,14 +985,14 @@ class Exit: public Command {
}
QString description() const {
return
"exit"
tag()+
"\n\n"
"Successfully terminate script immediately. The following commands "
"are not executed. This helps when you debug your scripts and you "
"want the script stop at a certain point for investigations.";
}
QString command() const {
return "exit";
return tag();
}
std::shared_ptr<Command> parse(Script*, QString, QStringList&, int) {
std::shared_ptr<Exit> cmd(new Exit());
@ -939,7 +1011,7 @@ class IgnoreTo: public Command {
}
QString description() const {
return
"ignoreto <label>"
tag()+" <label>"
"\n\n"
"Ignore all following commands up to a given label. The following "
"commands are not executed until the given label appears in the "
@ -947,7 +1019,7 @@ class IgnoreTo: public Command {
"want to skip some lines of script code.";
}
QString command() const {
return "ignoreto "+_label;
return tag()+" "+_label;
}
std::shared_ptr<Command> parse(Script*, QString args, QStringList&, int) {
std::shared_ptr<IgnoreTo> cmd(new IgnoreTo());
@ -971,12 +1043,12 @@ class Label: public Command {
}
QString description() const {
return
"label <label>"
tag()+" <label>"
"\n\n"
"This marks the label refered by command \"ignoreto\".";
}
QString command() const {
return "label "+_label;
return tag()+" "+_label;
}
std::shared_ptr<Command> parse(Script*, QString args, QStringList&, int) {
std::shared_ptr<Label> cmd(new Label());
@ -1000,23 +1072,23 @@ class Upload: public Command {
}
QString description() const {
return
"upload <selector> -> <filename>"
tag()+" <selector> -> <filename>"
"\n\n"
"Presses the specified file upload button and passes a given file "
"name. The command requires a CSS selector followed by a filename. "
"The first object that matches the selector is used.";
}
QString command() const {
return "upload "+_selector+" -> "+_filename;
return tag()+" "+_selector+" -> "+_filename;
}
std::shared_ptr<Command> parse(Script*, QString args, QStringList&, int) {
std::shared_ptr<Upload> cmd(new Upload());
QStringList allargs(args.split("->"));
if (allargs.size()<2)
throw BadArgument("upload needs a selector folowed by a filename, "
throw BadArgument(tag()+"requires <selector> -> <filename>, "
"instead of: \""+args+"\"");
cmd->_selector = allargs.takeFirst().trimmed();
cmd->_filename = allargs.join(" ").trimmed();
cmd->_filename = allargs.join("->").trimmed();
return cmd;
}
bool execute(Script* script, QWebFrame* frame) {
@ -1047,7 +1119,7 @@ class Exists: public Command {
}
QString description() const {
return
"exists <selector> -> <text>"
tag()+" <selector> -> <text>"
"\n\n"
"Assert that a certain text exists in the selected object, or if no "
"text is given, assert that the specified object exists. The object "
@ -1055,7 +1127,7 @@ class Exists: public Command {
"text.";
}
QString command() const {
return "exists "+_selector+(_text.size()?" -> "+_text:QString());
return tag()+" "+_selector+(_text.size()?" -> "+_text:QString());
}
std::shared_ptr<Command> parse(Script*, QString args, QStringList&, int) {
std::shared_ptr<Exists> cmd(new Exists());
@ -1064,7 +1136,7 @@ class Exists: public Command {
cmd->_selector = args;
} else {
cmd->_selector = allargs.takeFirst().trimmed();
cmd->_text = allargs.join(" ").trimmed();
cmd->_text = allargs.join("->").trimmed();
}
return cmd;
}
@ -1099,7 +1171,7 @@ class Not: public Command {
}
QString description() const {
return
"not <selector> -> <text>"
tag()+" <selector> -> <text>"
"\n\n"
"Assert that a certain text does not exists in the selected object, "
"or if no text is given, assert that the specified object does not "
@ -1107,7 +1179,7 @@ class Not: public Command {
"are search for the text.";
}
QString command() const {
return "not "+_selector+(_text.size()?" -> "+_text:QString());
return tag()+" "+_selector+(_text.size()?" -> "+_text:QString());
}
std::shared_ptr<Command> parse(Script*, QString args, QStringList&, int) {
std::shared_ptr<Not> cmd(new Not());
@ -1116,7 +1188,7 @@ class Not: public Command {
cmd->_selector = args;
} else {
cmd->_selector = allargs.takeFirst().trimmed();
cmd->_text = allargs.join(" ").trimmed();
cmd->_text = allargs.join("->").trimmed();
}
return cmd;
}
@ -1145,7 +1217,7 @@ class Execute: public Command {
}
QString description() const {
return
"execute <command>\n <line1>\n <line2>\n <...>"
tag()+" <command>\n <line1>\n <line2>\n <...>"
"\n\n"
"Execute <command>. The command can have space separated arguments. "
"Following lines that are intended by at least "
@ -1154,8 +1226,8 @@ class Execute: public Command {
}
QString command() const {
QStringList script(_script);
script.replaceInStrings(QRegExp("^"), " ");
return "execute "+_command
script.replaceInStrings(QRegularExpression("^"), " ");
return tag()+" "+_command
+(_args.size()?" "+_args.join(' '):QString())
+(script.size()?"\n"+script.join("\n"):QString());
}
@ -1214,15 +1286,16 @@ class Download: public Command {
}
QString description() const {
return
"download <filename>"
tag()+" <filename>"
"<command-to-start-download>"
"\n\n"
"Set download file before loading a download link or clicking on a "
"download button. The next line must be exactly one command that "
"initiates the download.";
"initiates the download.\n\n"
"Please note that variables are not substituted in the filename.";
}
QString command() const {
return "download"+(_filename.size()?" "+_filename:QString())+"\n"
return tag()+(_filename.size()?" "+_filename:QString())+"\n"
+_next->command();
}
std::shared_ptr<Command> parse(Script* script, QString args,
@ -1243,11 +1316,11 @@ class Download: public Command {
script->timer().stop(); // no timeout during download
for (_done = false; !_done;) // wait for download finish
QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
log.log("download terminated "+
log("download terminated "+
QString(_netsuccess&&_filesuccess?"successfully":"with error"));
if (!_netsuccess) throw DownloadFailed(_realfilename);
if (!_filesuccess) throw WriteFileFailed(_realfilename);
log.plainlog("[[ATTACHMENT|"+QDir(_realfilename).absolutePath()+"]]");
log["[[ATTACHMENT|"+QDir(_realfilename).absolutePath()+"]]"];
disconnect(frame->page(), SIGNAL(unsupportedContent(QNetworkReply*)),
this, SLOT(unsupportedContent(QNetworkReply*)));
return res;
@ -1271,8 +1344,8 @@ class Download: public Command {
.isValid()) {
QString part(reply->header(QNetworkRequest::ContentDispositionHeader)
.toString());
if (part.contains(QRegExp("attachment; *filename="))) {
part.replace(QRegExp(".*attachment; *filename="), "");
if (part.contains(QRegularExpression("attachment; *filename="))) {
part.replace(QRegularExpression(".*attachment; *filename="), "");
if (part.size()) _realfilename = part;
}
}
@ -1293,12 +1366,12 @@ class Click: public Command {
}
QString description() const {
return
"click <selector>"
tag()+" <selector>"
"\n\n"
"Click on the specified element";
}
QString command() const {
return "click "+_selector;
return tag()+" "+_selector;
}
std::shared_ptr<Command> parse(Script*, QString args, QStringList&, int) {
std::shared_ptr<Click> cmd(new Click());
@ -1307,7 +1380,22 @@ class Click: public Command {
}
bool execute(Script* script, QWebFrame* frame) {
Logger log(this, script);
switch (script->clicktype()) {
case Script::REAL_MOUSE_CLICK: {
log("Script::REAL_MOUSE_CLICK");
realMouseClick(frame, script->replacevars(_selector));
break;
}
case Script::JAVASCRIPT_CLICK:
default: {
log("Script::JAVASCRIPT_CLICK");
QWebElement element(find(frame, script->replacevars(_selector)));
if (element.isNull())
throw ElementNotFound(script->replacevars(_selector));
_result = element.evaluateJavaScript("this.click();").toString();
break;
}
}
return true;
}
private:
@ -1321,8 +1409,8 @@ class Set: public Command {
}
QString description() const {
return
"set <variable>=<value>\n"
"set <variable>\n"
tag()+" <variable>=<value>\n"+
tag()+" <variable>\n"
" <command>"
"\n\n"
"Sets the value of a variable either to a constant, or to the output"
@ -1332,9 +1420,9 @@ class Set: public Command {
}
QString command() const {
if (_next)
return "set "+_name+"\n "+_next->command();
return tag()+" "+_name+"\n "+_next->command();
else
return "set "+_name+" = "+_value;
return tag()+" "+_name+" = "+_value;
}
std::shared_ptr<Command> parse(Script* script, QString args,
QStringList& in, int line) {
@ -1374,12 +1462,12 @@ class UnSet: public Command {
}
QString description() const {
return
"unset <variable>"
tag()+" <variable>"
"\n\n"
"Undo the setting of a variable. The opposite of «set».";
}
QString command() const {
return "unset "+_name;
return tag()+" "+_name;
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList&, int) {
@ -1403,12 +1491,12 @@ class Timeout: public Command {
}
QString description() const {
return
"timeout <seconds>"
tag()+" <seconds>"
"\n\n"
"Set the timeout in seconds (defaults to 10).";
}
QString command() const {
return "timeout "+_timeout;
return tag()+" "+_timeout;
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList&, int) {
@ -1436,12 +1524,12 @@ class CaCertificate: public Command {
}
QString description() const {
return
"ca-certificate <filename.pem>"
tag()+" <filename.pem>"
"\n\n"
"Load a CA certificate that will be accepted on SSL connections.";
}
QString command() const {
return "ca-certificate "+_filename;
return tag()+" "+_filename;
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList&, int) {
@ -1471,7 +1559,7 @@ class ClientCertificate: public Command {
}
QString description() const {
return
"client-certificate <certfile.pem> <keyfile.key> <keypassword>"
tag()+" <certfile.pem> <keyfile.key> <keypassword>"
"\n\n"
"Load a client certificate to authenticate on SSL connections. "
"The password for the keyfile should not contain spaces. "
@ -1480,7 +1568,7 @@ class ClientCertificate: public Command {
" openssl pkcs12 -in certfile.p12 -out keyfile.pem -nocerts\n";
}
QString command() const {
return "client-certificate "+_certfile+" "+_keyfile+" "+_password;
return tag()+" "+_certfile+" "+_keyfile+" "+_password;
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList&, int) {
@ -1510,7 +1598,7 @@ class ClientCertificate: public Command {
throw FileNotFound(filename);
keyfile.open(QIODevice::ReadOnly);
QSslKey k(&keyfile, QSsl::Rsa, QSsl::Pem,
QSsl::PrivateKey, _password.toUtf8());
QSsl::PrivateKey, script->replacevars(_password).toUtf8());
if (k.isNull()) throw KeyNotReadable(filename);
sslConfig.setPrivateKey(k);
QSslConfiguration::setDefaultConfiguration(sslConfig);
@ -1522,6 +1610,126 @@ class ClientCertificate: public Command {
QString _password;
};
class ClickType: public Command {
public:
QString tag() const {
return "clicktype";
}
QString description() const {
return
tag()+" <type>"
"\n\n"
"Set how mouseclicks should be mapped. The best solution depends on"
" your problem. Normally it is good to call \"click()\" on the element"
" using javascript. But with complex javascript infected websites, it"
" might be necessary to simulate a real mouse click.\n\n"
"<type> is one of:\n"
" - realmouse\n"
" - javascript (default)";
}
QString command() const {
switch (_clicktype) {
case Script::REAL_MOUSE_CLICK: return tag()+" realmouse";
case Script::JAVASCRIPT_CLICK: default:
return tag()+" javascript";
}
}
std::shared_ptr<Command> parse(Script* script, QString args,
QStringList&, int) {
std::shared_ptr<ClickType> cmd(new ClickType());
QString choice(script->replacevars(args).trimmed());
if (choice=="realmouse")
cmd->_clicktype = Script::REAL_MOUSE_CLICK;
else if (choice=="javascript")
cmd->_clicktype = Script::JAVASCRIPT_CLICK;
else
throw BadArgument("unknown clicktype: "+choice);
return cmd;
}
bool execute(Script* script, QWebFrame* ) {
Logger log(this, script);
script->clicktype(_clicktype);
return true;
}
private:
Script::ClickType _clicktype;
};
class SetValue: public Command {
public:
QString tag() const {
return "setvalue";
}
QString description() const {
return
tag()+" <selector> -> '<value>'\n"+
tag()+" <selector> -> '<value1>', '<value2>', ..."
"\n\n"
"Set the selected element to a given value. It is mostly the same as"
" the following constuct, except that options in a select are evaluated"
" correctly:\n\n"
" do <selector>\n"
" this.value='<value>';\n\n"
"The second syntax with comma separated list of valus applies for"
" options in a select element\n\n"
"If you quote the values, then quote all values with the same"
" quotes. If you need a comma within a value, you must quote.";
}
QString command() const {
return tag()+" "+_selector+" -> "+_value;
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList&, int) {
std::shared_ptr<SetValue> cmd(new SetValue());
QStringList allargs(args.split("->"));
if (allargs.size()<2)
throw BadArgument(tag()+" requires <selector> -> <value>, "
"instead of: \""+args+"\"");
cmd->_selector = allargs.takeFirst().trimmed();
cmd->_value = allargs.join("->").trimmed();
return cmd;
}
bool execute(Script* script, QWebFrame* frame) {
Logger log(this, script);
QWebElement element(find(frame, script->replacevars(_selector)));
if (element.isNull())
throw ElementNotFound(script->replacevars(_selector));
QString value(script->replacevars(_value));
if (element.tagName()=="SELECT") {
// value is a comma seperated list of option values
QStringList values;
switch (value.size()>1&&value.at(0)==value.at(value.size()-1)
?value.at(0).toLatin1():'\0') {
case '"': case '\'': {
values = value.mid(1, value.size()-2)
.split(QRegularExpression(QString(value[0])+", *"
+QString(value[0])));
} break;
default: {
values = value.split(QRegularExpression(", *"));
}
}
Q_FOREACH(QWebElement option, element.findAll("option")) {
QString name(option.evaluateJavaScript("this.value").toString());
option.evaluateJavaScript
("this.selected="+QString(values.contains(name)?"true;":"false;"));
}
} else {
if (value.size()>1 && value[0]==value[value.size()-1] &&
(value[0]=='"' || value[0]=='\''))
element.evaluateJavaScript("this.value="+value+";");
else
element.evaluateJavaScript("this.value='"
+value.replace("'", "\\'")
+"';");
}
return true;
}
private:
QString _selector;
QString _value;
};
/* Template:
class : public Command {
public:
@ -1530,12 +1738,12 @@ class : public Command {
}
QString description() const {
return
""
tag()+
"\n\n"
"";
}
QString command() const {
return "";
return tag();
}
std::shared_ptr<Command> parse(Script*, QString args,
QStringList& in, int) {
@ -1555,7 +1763,7 @@ inline bool Screenshot::execute(Script* script, QWebFrame* frame) {
QString filename(screenshot(line(), targetdir(),
QFileInfo(testsuite()).baseName(),
_filename, frame));
log.plainlog("[[ATTACHMENT|"+filename+"]]");
log["[[ATTACHMENT|"+filename+"]]"];
return true;
}
@ -1566,11 +1774,11 @@ inline Logger::Logger(Command* command, Script* script):
_script->log(_command->command());
}
}
inline void Logger::log(QString txt) {
inline void Logger::operator()(QString txt) {
if (_command->log())
_script->log(txt);
}
inline void Logger::plainlog(QString txt) {
inline void Logger::operator[](QString txt) {
_script->plainlog(txt);
}
inline Logger::~Logger() {
@ -1599,6 +1807,8 @@ inline void Script::initPrototypes() {
add(new Timeout);
add(new CaCertificate);
add(new ClientCertificate);
add(new ::ClickType);
add(new SetValue);
}
#endif

@ -21,11 +21,16 @@
#include <stdexcept>
#include <QNetworkReply>
#include <QEvent>
#include <QTextDocumentFragment>
#include <mrw/stdext.hxx>
class TestGUI: public QMainWindow, protected Ui::TestGUI {
Q_OBJECT;
public:
explicit TestGUI(QWidget *parent = 0, QString url = QString()):
explicit TestGUI(QWidget *parent = 0,
QString url = QString(),
QString setupScript = QString(),
QString scriptFile = QString()):
QMainWindow(parent),
_typing(false),
_inEventFilter(false) {
@ -40,18 +45,21 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
_web->setPage(page);
_web->installEventFilter(this); // track mouse and keyboard
page->setForwardUnsupportedContent(true);
_commands->setText(Script().commands(Script::HTML));
connect(page, SIGNAL(uploadFile(QString)), SLOT(uploadFile(QString)));
connect(page, SIGNAL(unsupportedContent(QNetworkReply*)),
SLOT(unsupportedContent(QNetworkReply*)));
connect(page, SIGNAL(downloadRequested(const QNetworkRequest&)),
SLOT(downloadRequested(const QNetworkRequest&)));
if (setupScript.size()) loadSetup(setupScript);
if (scriptFile.size()) loadFile(scriptFile);
}
virtual ~TestGUI() {}
public Q_SLOTS:
void on__load_clicked() {
enterText(true);
if (_record->isChecked())
appendCommand("load "+_url->text());
appendCommand("load "+map(_url->text()));
_web->load(_url->text());
}
void on__abort_clicked() {
@ -61,28 +69,15 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
void on__actionOpen_triggered() {
QString name(QFileDialog::getOpenFileName(this, tr("Open Test Script")));
if (name.isEmpty()) return;
on__actionRevertToSaved_triggered(name);
loadFile(name);
}
void on__actionRevertToSaved_triggered() {
on__actionRevertToSaved_triggered(_filename);
}
void on__actionRevertToSaved_triggered(QString name) {
QFile file(name);
try {
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
throw std::runtime_error("file open failed");
_testscript->setPlainText(QString::fromUtf8(file.readAll()));
if (file.error()!=QFileDevice::NoError)
throw std::runtime_error("file read failed");
_filename = name;
_actionSave->setEnabled(true);
_actionRevertToSaved->setEnabled(true);
} catch(const std::exception& x) {
QMessageBox::critical(this, tr("Open Failed"),
tr("Reading test script failed, %2. "
"Cannot read test script from file %1.")
.arg(name).arg(x.what()));
void on__actionOpenSetupScript_triggered() {
QString name(QFileDialog::getOpenFileName(this, tr("Open Setup Script")));
if (name.isEmpty()) return;
loadSetup(name);
}
void on__actionRevertToSaved_triggered() {
loadFile(_filename);
}
void on__actionSaveAs_triggered() {
QString name(QFileDialog::getSaveFileName(this, tr("Save Test Script")));
@ -120,18 +115,24 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
bool oldRecordState(_record->isChecked());
_run->setEnabled(false);
try {
xml::Node testsuites("testsuites");
Script script;
connect(&script, SIGNAL(logging(QString)), SLOT(logging(QString)));
xml::Node testsuite("testsuite");
testsuite.attr("name") = "on-the-fly";
if (_setupscriptactive->isEnabled()
&& _setupscriptactive->isChecked()) {
script.parse(_setupscript->toPlainText().split('\n'));
testsuite.attr("name") = "setup-script";
testsuite.attr("timestamp") =
QDateTime::currentDateTime().toString(Qt::ISODate).toStdString();
xml::Node testcase("testcase");
testcase.attr("classname") = "testsuite-preparation";
QString text(_testscript->textCursor().selectedText());
script.run(_web->page()->mainFrame(), testsuite, QString(), false);
script.reset();
}
QString text(_testscript->textCursor().selection().toPlainText());
if (text.isEmpty()) text = _testscript->toPlainText();
Script script;
connect(&script, SIGNAL(logging(QString)), SLOT(logging(QString)));
script.parse(text.split('\n'));
testsuite.attr("name") = "setup-script";
testsuite.attr("timestamp") =
QDateTime::currentDateTime().toString(Qt::ISODate).toStdString();
script.run(_web->page()->mainFrame(), testsuite, QString(), false);
} catch (std::exception &x) {
QMessageBox::critical(this, tr("Script Failed"),
@ -153,28 +154,14 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
highlight(_web->page()->mainFrame()->documentElement()
.findFirst(_selector->text()));
}
void on__jsClick_clicked() {
enterText(true);
execute(selector(),
"this.click();");
// "var evObj = document.createEvent('MouseEvents');\n"
// "evObj.initEvent( 'click', true, true );\n"
// "this.dispatchEvent(evObj);");
}
void on__jsValue_clicked() {
enterText(true);
QWebElement element(selected());
execute(selector(element),
"this.value='"+value(element).replace("\n", "\\n")+"';");
}
void on__jsExecute_clicked() {
enterText(true);
execute(selector(), _javascriptCode->toPlainText());
_jsResult->setText(execute(selector(), _javascriptCode->toPlainText()));
}
void on__web_linkClicked(const QUrl& url) {
enterText(true);
if (_record->isChecked())
appendCommand("load "+url.url());
appendCommand("load "+map(url.url()));
}
void on__web_loadProgress(int progress) {
enterText(true);
@ -183,7 +170,7 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
void on__web_loadStarted() {
enterText(true);
if (_record->isChecked())
appendCommand("expect loadStarted");
appendCommand("expect "+map("loadStarted"));
_progress->setValue(0);
_urlStack->setCurrentIndex(PROGRESS_VIEW);
}
@ -196,7 +183,7 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
void on__web_urlChanged(const QUrl& url) {
enterText(true);
if (_record->isChecked())
appendCommand("expect urlChanged "+url.url());
appendCommand("expect "+map("urlChanged "+url.url()));
}
void on__web_selectionChanged() {
_source->setPlainText(_web->hasSelection()
@ -205,15 +192,55 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
}
void on__web_loadFinished(bool ok) {
enterText(true);
if (_record->isChecked())
appendCommand("expect loadFinished "
+QString(ok?"true":"false"));
if (_record->isChecked()) {
QString text(_testscript->toPlainText());
QStringList lines(text.split("\n"));
if (ok && lines.size()>1 &&
lines.last().startsWith("expect urlChanged") &&
lines.at(lines.size()-2)=="expect loadStarted") {
// replace three expect lines by one single line
QString url(lines.last().replace("expect urlChanged", "").trimmed());
lines.removeLast(); lines.removeLast();
_testscript->setPlainText(lines.join("\n"));
_testscript->moveCursor(QTextCursor::End);
_testscript->ensureCursorVisible();
appendCommand("expect "+map("load "+url));
} else {
appendCommand("expect "+map("loadFinished "
+QString(ok?"true":"false")));
}
}
_urlStack->setCurrentIndex(URL_VIEW);
on__web_selectionChanged();
setLinks();
setForms();
setDom();
}
void on__setupscript_textChanged() {
bool oldRecordState(_record->isChecked());
_run->setEnabled(false);
_setupscriptactive->setEnabled(false);
try {
_setupscriptstatus->setText(trUtf8("?"));
xml::Node testsuite("testsuite");
testsuite.attr("name") = "setup-script";
testsuite.attr("timestamp") =
QDateTime::currentDateTime().toString(Qt::ISODate).toStdString();
Script script;
TestWebPage page(0, true);
script.parse(_setupscript->toPlainText().split('\n'));
script.run(page.mainFrame(), testsuite, QString(), false);
_setupScript.cleanup();
_setupScript.parse(_setupscript->toPlainText().split('\n'));
_setupScript.run(page.mainFrame(), testsuite, QString(), false);;
_setupscriptstatus->setText(trUtf8(""));
_setupscriptactive->setEnabled(true);
} catch (std::exception &x) {
_setupscriptstatus->setText(trUtf8(""));
}
_run->setEnabled(true);
_record->setChecked(oldRecordState);
}
void on__forms_currentItemChanged(QTreeWidgetItem* item, QTreeWidgetItem*) {
if (!item) return;
_source->setPlainText(item->data(0, Qt::UserRole).toString());
@ -225,7 +252,7 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
void uploadFile(QString filename) {
enterText(true);
if (_record->isChecked())
appendCommand("upload "+filename);
appendCommand("upload "+map(filename));
}
void unsupportedContent(QNetworkReply* reply) {
if (!_record->isChecked()) return;
@ -241,7 +268,8 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
QString text(_testscript->toPlainText());
int pos1(text.lastIndexOf(QRegularExpression("^do ")));
int pos2(text.lastIndexOf(QRegularExpression("^load ")));
text.insert(pos1>pos2?pos1:pos2, "download "+filename);
int pos3(text.lastIndexOf(QRegularExpression("^click ")));
text.insert(mrw::max(pos1, pos2, pos3), "download "+filename);
_testscript->setPlainText(text);
_testscript->moveCursor(QTextCursor::End);
_testscript->ensureCursorVisible();
@ -259,6 +287,8 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
void appendCommand(const QString& txt) {
_testscript->appendPlainText(txt);
QScrollBar *vb(_testscript->verticalScrollBar());
_testscript->moveCursor(QTextCursor::End);
_testscript->ensureCursorVisible();
if (!vb) return;
vb->setValue(vb->maximum());
}
@ -315,16 +345,31 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
if (mooCombo.hasMatch()) {
// special treatment for moo tools combobox (e.g. used
// in joomla)
appendCommand("click "+mooCombo.captured(1)+">a");
appendCommand("sleep 1");
appendCommand("click "+map(mooCombo.captured(1)+">a"));
appendCommand("sleep "+map("1"));
} else if (mooComboItem.hasMatch()) {
// special treatment for item in moo tools combobox
appendCommand
("click li.active-result[data-option-array-index=\""
+element.attribute("data-option-array-index")+"\"]");
appendCommand("sleep 1");
("click "+map("li.active-result[data-option-array-index=\""
+element.attribute("data-option-array-index")
+"\"]"));
appendCommand("sleep "+map("1"));
} else if (_lastFocused.tagName()=="SELECT") {
// click on a select results in a value change
// find all selected options ...
QStringList v;
Q_FOREACH(QWebElement option,
_lastFocused.findAll("option")) {
//! @bug QT does not support selected
if (option.evaluateJavaScript("this.selected").toBool())
v += value(option);
}
setValue(selected, v);
} else {
appendCommand("click "+selected);
appendCommand("click "+map(selected));
}
if (_lastFocused.tagName()=="INPUT") {
_typing = true;
}
} else {
appendCommand("# click, but where?");
@ -332,13 +377,11 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
}
} break;
case QEvent::MouseButtonPress: {
enterText(true);
} break;
case QEvent::ChildRemoved: { // select option value changed
enterText(true);
_typing = true;
_lastFocused=element;
_keyStrokes = "dummy";
} break;
case QEvent::InputMethodQuery:
case QEvent::ToolTipChange:
@ -353,12 +396,44 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
return false;
}
private:
void loadFile(QString name) {
QFile file(name);
try {
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
throw std::runtime_error("file open failed");
_testscript->setPlainText(QString::fromUtf8(file.readAll()));
if (file.error()!=QFileDevice::NoError)
throw std::runtime_error("file read failed");
_filename = name;
_actionSave->setEnabled(true);
_actionRevertToSaved->setEnabled(true);
} catch(const std::exception& x) {
QMessageBox::critical(this, tr("Open Failed"),
tr("Reading test script failed, %2. "
"Cannot read test script from file %1.")
.arg(name).arg(x.what()));
}
}
void loadSetup(QString name) {
QFile file(name);
try {
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
throw std::runtime_error("file open failed");
_setupscript->setPlainText(QString::fromUtf8(file.readAll()));
if (file.error()!=QFileDevice::NoError)
throw std::runtime_error("file read failed");
on__setupscript_textChanged();
} catch(const std::exception& x) {
QMessageBox::critical(this, tr("Open Failed"),
tr("Reading test script failed, %2. "
"Cannot read test script from file %1.")
.arg(name).arg(x.what()));
}
}
void enterText(bool force=false) {
if (!force && (!_typing || _lastFocused==focused())) return;
if (_keyStrokes.size() && !_lastFocused.isNull()) {
store(selector(_lastFocused), "this.value='"
+value(_lastFocused).replace("\n", "\\n")+"';");
}
if (_typing && !_lastFocused.isNull())
setValue(selector(_lastFocused), value(_lastFocused));
_lastFocused = QWebElement();
_keyStrokes.clear();
_typing = false;
@ -451,15 +526,54 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
// else
// return element.toPlainText();
}
void store(const QString& selector, QString code) {
QString map(QString in) {
if (_setupscriptactive->isEnabled()
&& _setupscriptactive->isChecked()) {
return _setupScript.insertvars(in);
}
return in;
}
void javascript(const QString& selector, QString code) {
if (_record->isChecked())
appendCommand("do "+selector+"\n "
+code.replace("\n", "\\n"));
appendCommand("do "+map(selector)+"\n "
+map(code).replace("\n", "\\n"));
}
void cleanup(const QString& selector) {
QString text(_testscript->toPlainText());
QStringList lines(text.split("\n"));
bool changed(false);
while (lines.size() &&
(lines.last()=="click "+selector ||
lines.last().startsWith("setvalue "+selector+" -> "))) {
lines.removeLast();
changed = true;
}
if (changed) {
_testscript->setPlainText(lines.join("\n"));
_testscript->moveCursor(QTextCursor::End);
_testscript->ensureCursorVisible();
}
}
void setValue(const QString& selector, QString code) {
if (_record->isChecked()) {
cleanup(selector);
appendCommand("setvalue "+map(selector)+" -> '"
+map(code).replace("'", "\\'").replace("\n", "\\n")+"'");
}
}
void setValue(const QString& selector, QStringList code) {
if (_record->isChecked()) {
cleanup(selector);
appendCommand("setvalue "+map(selector)+" -> '"+
map(code.replaceInStrings("'", "\\'")
.replaceInStrings("\n", "\\n")
.join("', '")+"'"));
}
}
void execute(const QString& selector, const QString& code) {
store(selector, code);
_web->page()->mainFrame()->documentElement().findFirst(selector)
.evaluateJavaScript(code);
QString execute(const QString& selector, const QString& code) {
javascript(selector, code);
return _web->page()->mainFrame()->documentElement().findFirst(selector)
.evaluateJavaScript(code).toString();
}
void setLinks() {
QWebElementCollection links(_web->page()->mainFrame()->documentElement()
@ -675,6 +789,7 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
QString _keyStrokes; // collect key strokes
bool _typing; // user is typing
bool _inEventFilter; // actually handling event filter
Script _setupScript;
};
#endif // TESTGUI_HXX

@ -91,7 +91,7 @@
<x>0</x>
<y>0</y>
<width>888</width>
<height>23</height>
<height>22</height>
</rect>
</property>
<widget class="QMenu" name="menuViews">
@ -111,6 +111,7 @@
<string>File</string>
</property>
<addaction name="_actionOpen"/>
<addaction name="_actionOpenSetupScript"/>
<addaction name="_actionSave"/>
<addaction name="_actionSaveAs"/>
<addaction name="separator"/>
@ -121,8 +122,15 @@
<addaction name="_actionClear"/>
<addaction name="_actionQuit"/>
</widget>
<widget class="QMenu" name="menuHelp">
<property name="title">
<string>Help</string>
</property>
<addaction name="_actionCommands"/>
</widget>
<addaction name="menuFile"/>
<addaction name="menuViews"/>
<addaction name="menuHelp"/>
</widget>
<widget class="QStatusBar" name="statusbar"/>
<widget class="QDockWidget" name="_domDock">
@ -241,31 +249,14 @@
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QPushButton" name="_jsClick">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Click</string>
<string>Result:</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="_jsValue">
<property name="text">
<string>Set Value</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
<widget class="QLineEdit" name="_jsResult"/>
</item>
</layout>
</item>
@ -397,7 +388,6 @@ this.dispatchEvent(evObj);</string>
</layout>
</item>
</layout>
<zorder></zorder>
</widget>
</widget>
<widget class="QDockWidget" name="_scriptDock">
@ -463,6 +453,84 @@ this.dispatchEvent(evObj);</string>
</layout>
</widget>
</widget>
<widget class="QDockWidget" name="dockWidget">
<property name="windowTitle">
<string>Setup Script</string>
</property>
<attribute name="dockWidgetArea">
<number>4</number>
</attribute>
<widget class="QWidget" name="dockWidgetContents">
<layout class="QVBoxLayout" name="verticalLayout_7">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QCheckBox" name="_setupscriptactive">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>active</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Status:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="_setupscriptstatus">
<property name="text">
<string>?</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QPlainTextEdit" name="_setupscript"/>
</item>
</layout>
</widget>
</widget>
<widget class="QDockWidget" name="dockWidget_2">
<property name="windowTitle">
<string>Script Commands</string>
</property>
<attribute name="dockWidgetArea">
<number>4</number>
</attribute>
<widget class="QWidget" name="dockWidgetContents_3">
<layout class="QGridLayout" name="gridLayout_7">
<item row="0" column="0">
<widget class="QTextBrowser" name="_commands">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<action name="_actionDOMTree">
<property name="checkable">
<bool>true</bool>
@ -583,6 +651,16 @@ this.dispatchEvent(evObj);</string>
<string>Revert to saved</string>
</property>
</action>
<action name="_actionOpenSetupScript">
<property name="text">
<string>Open Setup Script ...</string>
</property>
</action>
<action name="_actionCommands">
<property name="text">
<string>Commands ...</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>

@ -13,7 +13,7 @@
#include <xml-cxx/xml.hxx>
#include <mrw/string.hxx>
std::string VERSION("0.9.4");
std::string VERSION("0.9.5");
QString format(QString txt, int indent = 2, int cpl = 60) {
QStringList res;

@ -6,9 +6,14 @@ int main(int argc, char *argv[]) try {
QApplication a(argc, argv);
QCommandLineParser parser;
parser.addHelpOption();
parser.addOption(QCommandLineOption
(QStringList()<<"u"<<"url",
"set initial URL to <url>", "url"));
parser.process(a);
QStringList urls(parser.positionalArguments());
TestGUI w(0, urls.size()?urls[0]:"");
QStringList scripts(parser.positionalArguments());
TestGUI w(0, parser.value("url"),
scripts.size()>1?scripts[0]:"",
scripts.size()>1?scripts[1]:scripts.size()?scripts[0]:"");
w.show();
return a.exec();
} catch (std::exception &x) {

Loading…
Cancel
Save