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 10 years ago
parent 04c4c54089
commit 332db8b079
  1. 2
      configure.ac
  2. 15
      docker/Dockerfile
  3. 11
      docker/runtests.sh
  4. 348
      src/commands.hxx
  5. 261
      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 "+
QString(_netsuccess&&_filesuccess?"successfully":"with error"));
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);
realMouseClick(frame, script->replacevars(_selector));
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__actionOpenSetupScript_triggered() {
QString name(QFileDialog::getOpenFileName(this, tr("Open Setup Script")));
if (name.isEmpty()) return;
loadSetup(name);
}
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__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");
xml::Node testsuite("testsuite");
testsuite.attr("name") = "on-the-fly";
testsuite.attr("timestamp") =
QDateTime::currentDateTime().toString(Qt::ISODate).toStdString();
xml::Node testcase("testcase");
testcase.attr("classname") = "testsuite-preparation";
QString text(_testscript->textCursor().selectedText());
if (text.isEmpty()) text = _testscript->toPlainText();
Script script;
connect(&script, SIGNAL(logging(QString)), SLOT(logging(QString)));
xml::Node testsuite("testsuite");
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();
script.run(_web->page()->mainFrame(), testsuite, QString(), false);
script.reset();
}
QString text(_testscript->textCursor().selection().toPlainText());
if (text.isEmpty()) text = _testscript->toPlainText();
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">
<property name="text">
<string>Click</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="_jsValue">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Set Value</string>
<string>Result:</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