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]) 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