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

@ -21,11 +21,16 @@
#include <stdexcept> #include <stdexcept>
#include <QNetworkReply> #include <QNetworkReply>
#include <QEvent> #include <QEvent>
#include <QTextDocumentFragment>
#include <mrw/stdext.hxx>
class TestGUI: public QMainWindow, protected Ui::TestGUI { class TestGUI: public QMainWindow, protected Ui::TestGUI {
Q_OBJECT; Q_OBJECT;
public: 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), QMainWindow(parent),
_typing(false), _typing(false),
_inEventFilter(false) { _inEventFilter(false) {
@ -40,18 +45,21 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
_web->setPage(page); _web->setPage(page);
_web->installEventFilter(this); // track mouse and keyboard _web->installEventFilter(this); // track mouse and keyboard
page->setForwardUnsupportedContent(true); page->setForwardUnsupportedContent(true);
_commands->setText(Script().commands(Script::HTML));
connect(page, SIGNAL(uploadFile(QString)), SLOT(uploadFile(QString))); connect(page, SIGNAL(uploadFile(QString)), SLOT(uploadFile(QString)));
connect(page, SIGNAL(unsupportedContent(QNetworkReply*)), connect(page, SIGNAL(unsupportedContent(QNetworkReply*)),
SLOT(unsupportedContent(QNetworkReply*))); SLOT(unsupportedContent(QNetworkReply*)));
connect(page, SIGNAL(downloadRequested(const QNetworkRequest&)), connect(page, SIGNAL(downloadRequested(const QNetworkRequest&)),
SLOT(downloadRequested(const QNetworkRequest&))); SLOT(downloadRequested(const QNetworkRequest&)));
if (setupScript.size()) loadSetup(setupScript);
if (scriptFile.size()) loadFile(scriptFile);
} }
virtual ~TestGUI() {} virtual ~TestGUI() {}
public Q_SLOTS: public Q_SLOTS:
void on__load_clicked() { void on__load_clicked() {
enterText(true); enterText(true);
if (_record->isChecked()) if (_record->isChecked())
appendCommand("load "+_url->text()); appendCommand("load "+map(_url->text()));
_web->load(_url->text()); _web->load(_url->text());
} }
void on__abort_clicked() { void on__abort_clicked() {
@ -61,28 +69,15 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
void on__actionOpen_triggered() { void on__actionOpen_triggered() {
QString name(QFileDialog::getOpenFileName(this, tr("Open Test Script"))); QString name(QFileDialog::getOpenFileName(this, tr("Open Test Script")));
if (name.isEmpty()) return; if (name.isEmpty()) return;
on__actionRevertToSaved_triggered(name); loadFile(name);
} }
void on__actionRevertToSaved_triggered() { void on__actionOpenSetupScript_triggered() {
on__actionRevertToSaved_triggered(_filename); QString name(QFileDialog::getOpenFileName(this, tr("Open Setup Script")));
if (name.isEmpty()) return;
loadSetup(name);
} }
void on__actionRevertToSaved_triggered(QString name) { void on__actionRevertToSaved_triggered() {
QFile file(name); loadFile(_filename);
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__actionSaveAs_triggered() { void on__actionSaveAs_triggered() {
QString name(QFileDialog::getSaveFileName(this, tr("Save Test Script"))); QString name(QFileDialog::getSaveFileName(this, tr("Save Test Script")));
@ -120,18 +115,24 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
bool oldRecordState(_record->isChecked()); bool oldRecordState(_record->isChecked());
_run->setEnabled(false); _run->setEnabled(false);
try { 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; Script script;
connect(&script, SIGNAL(logging(QString)), SLOT(logging(QString))); 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')); 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); script.run(_web->page()->mainFrame(), testsuite, QString(), false);
} catch (std::exception &x) { } catch (std::exception &x) {
QMessageBox::critical(this, tr("Script Failed"), QMessageBox::critical(this, tr("Script Failed"),
@ -153,28 +154,14 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
highlight(_web->page()->mainFrame()->documentElement() highlight(_web->page()->mainFrame()->documentElement()
.findFirst(_selector->text())); .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() { void on__jsExecute_clicked() {
enterText(true); enterText(true);
execute(selector(), _javascriptCode->toPlainText()); _jsResult->setText(execute(selector(), _javascriptCode->toPlainText()));
} }
void on__web_linkClicked(const QUrl& url) { void on__web_linkClicked(const QUrl& url) {
enterText(true); enterText(true);
if (_record->isChecked()) if (_record->isChecked())
appendCommand("load "+url.url()); appendCommand("load "+map(url.url()));
} }
void on__web_loadProgress(int progress) { void on__web_loadProgress(int progress) {
enterText(true); enterText(true);
@ -183,7 +170,7 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
void on__web_loadStarted() { void on__web_loadStarted() {
enterText(true); enterText(true);
if (_record->isChecked()) if (_record->isChecked())
appendCommand("expect loadStarted"); appendCommand("expect "+map("loadStarted"));
_progress->setValue(0); _progress->setValue(0);
_urlStack->setCurrentIndex(PROGRESS_VIEW); _urlStack->setCurrentIndex(PROGRESS_VIEW);
} }
@ -196,7 +183,7 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
void on__web_urlChanged(const QUrl& url) { void on__web_urlChanged(const QUrl& url) {
enterText(true); enterText(true);
if (_record->isChecked()) if (_record->isChecked())
appendCommand("expect urlChanged "+url.url()); appendCommand("expect "+map("urlChanged "+url.url()));
} }
void on__web_selectionChanged() { void on__web_selectionChanged() {
_source->setPlainText(_web->hasSelection() _source->setPlainText(_web->hasSelection()
@ -205,15 +192,55 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
} }
void on__web_loadFinished(bool ok) { void on__web_loadFinished(bool ok) {
enterText(true); enterText(true);
if (_record->isChecked()) if (_record->isChecked()) {
appendCommand("expect loadFinished " QString text(_testscript->toPlainText());
+QString(ok?"true":"false")); 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); _urlStack->setCurrentIndex(URL_VIEW);
on__web_selectionChanged(); on__web_selectionChanged();
setLinks(); setLinks();
setForms(); setForms();
setDom(); 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*) { void on__forms_currentItemChanged(QTreeWidgetItem* item, QTreeWidgetItem*) {
if (!item) return; if (!item) return;
_source->setPlainText(item->data(0, Qt::UserRole).toString()); _source->setPlainText(item->data(0, Qt::UserRole).toString());
@ -225,7 +252,7 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
void uploadFile(QString filename) { void uploadFile(QString filename) {
enterText(true); enterText(true);
if (_record->isChecked()) if (_record->isChecked())
appendCommand("upload "+filename); appendCommand("upload "+map(filename));
} }
void unsupportedContent(QNetworkReply* reply) { void unsupportedContent(QNetworkReply* reply) {
if (!_record->isChecked()) return; if (!_record->isChecked()) return;
@ -241,7 +268,8 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
QString text(_testscript->toPlainText()); QString text(_testscript->toPlainText());
int pos1(text.lastIndexOf(QRegularExpression("^do "))); int pos1(text.lastIndexOf(QRegularExpression("^do ")));
int pos2(text.lastIndexOf(QRegularExpression("^load "))); 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->setPlainText(text);
_testscript->moveCursor(QTextCursor::End); _testscript->moveCursor(QTextCursor::End);
_testscript->ensureCursorVisible(); _testscript->ensureCursorVisible();
@ -259,6 +287,8 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
void appendCommand(const QString& txt) { void appendCommand(const QString& txt) {
_testscript->appendPlainText(txt); _testscript->appendPlainText(txt);
QScrollBar *vb(_testscript->verticalScrollBar()); QScrollBar *vb(_testscript->verticalScrollBar());
_testscript->moveCursor(QTextCursor::End);
_testscript->ensureCursorVisible();
if (!vb) return; if (!vb) return;
vb->setValue(vb->maximum()); vb->setValue(vb->maximum());
} }
@ -315,16 +345,31 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
if (mooCombo.hasMatch()) { if (mooCombo.hasMatch()) {
// special treatment for moo tools combobox (e.g. used // special treatment for moo tools combobox (e.g. used
// in joomla) // in joomla)
appendCommand("click "+mooCombo.captured(1)+">a"); appendCommand("click "+map(mooCombo.captured(1)+">a"));
appendCommand("sleep 1"); appendCommand("sleep "+map("1"));
} else if (mooComboItem.hasMatch()) { } else if (mooComboItem.hasMatch()) {
// special treatment for item in moo tools combobox // special treatment for item in moo tools combobox
appendCommand appendCommand
("click li.active-result[data-option-array-index=\"" ("click "+map("li.active-result[data-option-array-index=\""
+element.attribute("data-option-array-index")+"\"]"); +element.attribute("data-option-array-index")
appendCommand("sleep 1"); +"\"]"));
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 { } else {
appendCommand("click "+selected); appendCommand("click "+map(selected));
}
if (_lastFocused.tagName()=="INPUT") {
_typing = true;
} }
} else { } else {
appendCommand("# click, but where?"); appendCommand("# click, but where?");
@ -332,13 +377,11 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
} }
} break; } break;
case QEvent::MouseButtonPress: { case QEvent::MouseButtonPress: {
enterText(true);
} break; } break;
case QEvent::ChildRemoved: { // select option value changed case QEvent::ChildRemoved: { // select option value changed
enterText(true); enterText(true);
_typing = true; _typing = true;
_lastFocused=element; _lastFocused=element;
_keyStrokes = "dummy";
} break; } break;
case QEvent::InputMethodQuery: case QEvent::InputMethodQuery:
case QEvent::ToolTipChange: case QEvent::ToolTipChange:
@ -353,12 +396,44 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
return false; return false;
} }
private: 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) { void enterText(bool force=false) {
if (!force && (!_typing || _lastFocused==focused())) return; if (!force && (!_typing || _lastFocused==focused())) return;
if (_keyStrokes.size() && !_lastFocused.isNull()) { if (_typing && !_lastFocused.isNull())
store(selector(_lastFocused), "this.value='" setValue(selector(_lastFocused), value(_lastFocused));
+value(_lastFocused).replace("\n", "\\n")+"';");
}
_lastFocused = QWebElement(); _lastFocused = QWebElement();
_keyStrokes.clear(); _keyStrokes.clear();
_typing = false; _typing = false;
@ -451,15 +526,54 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
// else // else
// return element.toPlainText(); // 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()) if (_record->isChecked())
appendCommand("do "+selector+"\n " appendCommand("do "+map(selector)+"\n "
+code.replace("\n", "\\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) { QString execute(const QString& selector, const QString& code) {
store(selector, code); javascript(selector, code);
_web->page()->mainFrame()->documentElement().findFirst(selector) return _web->page()->mainFrame()->documentElement().findFirst(selector)
.evaluateJavaScript(code); .evaluateJavaScript(code).toString();
} }
void setLinks() { void setLinks() {
QWebElementCollection links(_web->page()->mainFrame()->documentElement() QWebElementCollection links(_web->page()->mainFrame()->documentElement()
@ -675,6 +789,7 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
QString _keyStrokes; // collect key strokes QString _keyStrokes; // collect key strokes
bool _typing; // user is typing bool _typing; // user is typing
bool _inEventFilter; // actually handling event filter bool _inEventFilter; // actually handling event filter
Script _setupScript;
}; };
#endif // TESTGUI_HXX #endif // TESTGUI_HXX

@ -91,7 +91,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>888</width> <width>888</width>
<height>23</height> <height>22</height>
</rect> </rect>
</property> </property>
<widget class="QMenu" name="menuViews"> <widget class="QMenu" name="menuViews">
@ -111,6 +111,7 @@
<string>File</string> <string>File</string>
</property> </property>
<addaction name="_actionOpen"/> <addaction name="_actionOpen"/>
<addaction name="_actionOpenSetupScript"/>
<addaction name="_actionSave"/> <addaction name="_actionSave"/>
<addaction name="_actionSaveAs"/> <addaction name="_actionSaveAs"/>
<addaction name="separator"/> <addaction name="separator"/>
@ -121,8 +122,15 @@
<addaction name="_actionClear"/> <addaction name="_actionClear"/>
<addaction name="_actionQuit"/> <addaction name="_actionQuit"/>
</widget> </widget>
<widget class="QMenu" name="menuHelp">
<property name="title">
<string>Help</string>
</property>
<addaction name="_actionCommands"/>
</widget>
<addaction name="menuFile"/> <addaction name="menuFile"/>
<addaction name="menuViews"/> <addaction name="menuViews"/>
<addaction name="menuHelp"/>
</widget> </widget>
<widget class="QStatusBar" name="statusbar"/> <widget class="QStatusBar" name="statusbar"/>
<widget class="QDockWidget" name="_domDock"> <widget class="QDockWidget" name="_domDock">
@ -241,31 +249,14 @@
<item> <item>
<layout class="QHBoxLayout" name="horizontalLayout_2"> <layout class="QHBoxLayout" name="horizontalLayout_2">
<item> <item>
<widget class="QPushButton" name="_jsClick"> <widget class="QLabel" name="label_3">
<property name="text">
<string>Click</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="_jsValue">
<property name="text"> <property name="text">
<string>Set Value</string> <string>Result:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<spacer name="horizontalSpacer"> <widget class="QLineEdit" name="_jsResult"/>
<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>
</layout> </layout>
</item> </item>
@ -397,7 +388,6 @@ this.dispatchEvent(evObj);</string>
</layout> </layout>
</item> </item>
</layout> </layout>
<zorder></zorder>
</widget> </widget>
</widget> </widget>
<widget class="QDockWidget" name="_scriptDock"> <widget class="QDockWidget" name="_scriptDock">
@ -463,6 +453,84 @@ this.dispatchEvent(evObj);</string>
</layout> </layout>
</widget> </widget>
</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"> <action name="_actionDOMTree">
<property name="checkable"> <property name="checkable">
<bool>true</bool> <bool>true</bool>
@ -583,6 +651,16 @@ this.dispatchEvent(evObj);</string>
<string>Revert to saved</string> <string>Revert to saved</string>
</property> </property>
</action> </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> </widget>
<customwidgets> <customwidgets>
<customwidget> <customwidget>

@ -13,7 +13,7 @@
#include <xml-cxx/xml.hxx> #include <xml-cxx/xml.hxx>
#include <mrw/string.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) { QString format(QString txt, int indent = 2, int cpl = 60) {
QStringList res; QStringList res;

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

Loading…
Cancel
Save