More features: have a seperate setup script, where global variable replacements can be placed, new commandline parameters, command window, better recording, chose click type, ...
This commit is contained in:
@@ -29,6 +29,8 @@ if test -z "${QMAKE}"; then
|
||||
AC_MSG_ERROR([cannot find a qmake command])
|
||||
fi
|
||||
|
||||
# requires mrw-c++ - to be defined ...
|
||||
|
||||
README=$(tail -n +3 README)
|
||||
README_DEB=$(tail -n +3 README | sed -e 's/^$/./g' -e 's/^/ /g')
|
||||
DESCRIPTION=$(head -1 README)
|
||||
|
15
docker/Dockerfile
Normal file
15
docker/Dockerfile
Normal file
@@ -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
|
11
docker/runtests.sh
Executable file
11
docker/runtests.sh
Executable file
@@ -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}
|
344
src/commands.hxx
344
src/commands.hxx
@@ -21,6 +21,7 @@
|
||||
#include <QTimer>
|
||||
#include <QProcess>
|
||||
#include <QMouseEvent>
|
||||
#include <QRegularExpression>
|
||||
#include <vector>
|
||||
#include <queue>
|
||||
#include <map>
|
||||
@@ -35,8 +36,8 @@ class Command;
|
||||
class Logger {
|
||||
public:
|
||||
Logger(Command* command, Script* script);
|
||||
void plainlog(QString txt);
|
||||
void log(QString txt);
|
||||
void operator[](QString txt);
|
||||
void operator()(QString txt);
|
||||
~Logger();
|
||||
private:
|
||||
Command* _command;
|
||||
@@ -170,7 +171,7 @@ class Empty: public Command {
|
||||
"Empty lines are allowed";
|
||||
}
|
||||
QString command() const {
|
||||
return "";
|
||||
return tag();
|
||||
}
|
||||
std::shared_ptr<Command> parse(Script*, QString, QStringList&, int) {
|
||||
std::shared_ptr<Empty> cmd(new Empty());
|
||||
@@ -258,14 +259,14 @@ class Screenshot: public Command {
|
||||
}
|
||||
QString description() const {
|
||||
return
|
||||
"screenshot <filename-base>"
|
||||
tag()+" <filename-base>"
|
||||
"\n\n"
|
||||
"Create a PNG screenshot of the actual web page and store it in the "
|
||||
"file <filename-base>.png. If not already opened, a browser window "
|
||||
"will pop up to take the screenshot.";
|
||||
}
|
||||
QString command() const {
|
||||
return "screenshot "+_filename;
|
||||
return tag()+" "+_filename;
|
||||
}
|
||||
std::shared_ptr<Command> parse(Script*, QString args, QStringList&, int) {
|
||||
std::shared_ptr<Screenshot> cmd(new Screenshot());
|
||||
@@ -325,6 +326,25 @@ class Script: public QObject {
|
||||
void logging(QString);
|
||||
public:
|
||||
typedef std::pair<QString, QStringList> Signal;
|
||||
enum ClickType {
|
||||
REAL_MOUSE_CLICK,
|
||||
JAVASCRIPT_CLICK
|
||||
};
|
||||
enum Formatting {
|
||||
PLAIN,
|
||||
HTML
|
||||
};
|
||||
/// QString that sorts first by string length, then by content
|
||||
class LenString: public QString {
|
||||
public:
|
||||
LenString() {}
|
||||
LenString(const QString& o): QString(o) {}
|
||||
virtual bool operator<(const LenString& o) const {
|
||||
return
|
||||
size()<o.size() ? true : o.size()<size() ? false
|
||||
: QString::operator<(o.toLatin1());
|
||||
}
|
||||
};
|
||||
public:
|
||||
static QString xmlattr(QString attr, bool br = false) {
|
||||
attr.replace("&", "&")//.replace(" ", " ")
|
||||
@@ -343,7 +363,7 @@ class Script: public QObject {
|
||||
.replace(" ", " ").replace("&", "&");
|
||||
}
|
||||
public:
|
||||
Script() {
|
||||
Script(): _clicktype(JAVASCRIPT_CLICK) {
|
||||
initPrototypes();
|
||||
}
|
||||
QString syntax() const {
|
||||
@@ -355,10 +375,34 @@ class Script: public QObject {
|
||||
"Note: When a selector is required as parameter, then the selector "
|
||||
"is a CSS selector that must not contain spaces.";
|
||||
}
|
||||
QString commands() const {
|
||||
QString commands(Formatting f = PLAIN) const {
|
||||
QString cmds;
|
||||
for (auto it(_prototypes.begin()); it!=_prototypes.end(); ++it)
|
||||
cmds+="\n\n"+it->second->description();
|
||||
switch (f) {
|
||||
case PLAIN: {
|
||||
cmds+="\n\n\nCOMMAND: "+it->first+"\n\n"+it->second->description();
|
||||
} break;
|
||||
case HTML: {
|
||||
cmds+="<h1>"+it->first+"</h1><p>"
|
||||
+it->second->description()
|
||||
.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace(QRegularExpression("<([^ ]+)>"),
|
||||
"<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 ")
|
||||
.replace("\n\n", "</p><p>")
|
||||
.replace("\n", "<br/>")
|
||||
+"</p>";
|
||||
} break;
|
||||
}
|
||||
return cmds.trimmed();
|
||||
}
|
||||
void reset() {
|
||||
@@ -368,6 +412,14 @@ class Script: public QObject {
|
||||
_ignores.clear();
|
||||
_cout.clear();
|
||||
_cerr.clear();
|
||||
_ignoreSignalsUntil.clear();
|
||||
}
|
||||
void cleanup() {
|
||||
reset();
|
||||
_variables.clear();
|
||||
_rvariables.clear();
|
||||
_timeout = 20;
|
||||
_clicktype = JAVASCRIPT_CLICK;
|
||||
}
|
||||
std::shared_ptr<Command> parse(QStringList& in, int linenr) try {
|
||||
QString line(in.takeFirst().trimmed());
|
||||
@@ -555,19 +607,36 @@ class Script: public QObject {
|
||||
}
|
||||
void set(QString name, QString value) {
|
||||
_variables[name] = value;
|
||||
_rvariables[value] = name;
|
||||
}
|
||||
void unset(QString name) {
|
||||
_rvariables.remove(_variables[name]);
|
||||
_variables.remove(name);
|
||||
}
|
||||
void timeout(int t) {
|
||||
_timeout = t;
|
||||
}
|
||||
void clicktype(ClickType c) {
|
||||
_clicktype = c;
|
||||
}
|
||||
ClickType clicktype() {
|
||||
return _clicktype;
|
||||
}
|
||||
QString replacevars(QString txt) {
|
||||
for(QMap<QString, QString>::iterator it(_variables.begin());
|
||||
it!=_variables.end(); ++it)
|
||||
txt.replace(it.key(), it.value());
|
||||
return txt;
|
||||
}
|
||||
QString insertvars(QString txt) {
|
||||
QMapIterator<LenString, LenString> it(_rvariables);
|
||||
it.toBack();
|
||||
while (it.hasPrevious()) {
|
||||
it.previous();
|
||||
txt.replace(it.key(), it.value());
|
||||
}
|
||||
return txt;
|
||||
}
|
||||
public Q_SLOTS:
|
||||
void log(QString text) {
|
||||
text = QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss ")+text;
|
||||
@@ -703,8 +772,10 @@ class Script: public QObject {
|
||||
QString _cerr;
|
||||
bool _screenshots;
|
||||
QString _ignoreSignalsUntil;
|
||||
QMap<QString, QString> _variables;
|
||||
QMap<QString, QString> _variables; ///< variable mapping
|
||||
QMap<LenString, LenString> _rvariables; ///< reverse variable mapping
|
||||
int _timeout;
|
||||
ClickType _clicktype;
|
||||
};
|
||||
|
||||
class Do: public Command {
|
||||
@@ -714,7 +785,7 @@ class Do: public Command {
|
||||
}
|
||||
QString description() const {
|
||||
return
|
||||
"do <selector>\n <javascript-line1>\n <javascript-line2>"
|
||||
tag()+" <selector>\n <javascript-line1>\n <javascript-line2>"
|
||||
"\n\n"
|
||||
"Execute JavaScript on a CSS selected object. The object is the first "
|
||||
"object in the DOM tree that matches the given CSS selector. You can "
|
||||
@@ -723,7 +794,7 @@ class Do: public Command {
|
||||
"one space";
|
||||
}
|
||||
QString command() const {
|
||||
return "do "+_selector+_javascript;
|
||||
return tag()+" "+_selector+_javascript;
|
||||
}
|
||||
std::shared_ptr<Command> parse(Script*, QString args,
|
||||
QStringList& in, int) {
|
||||
@@ -735,8 +806,9 @@ class Do: public Command {
|
||||
}
|
||||
bool execute(Script* script, QWebFrame* frame) {
|
||||
Logger log(this, script);
|
||||
QWebElement element(find(frame, _selector));
|
||||
if (element.isNull()) throw ElementNotFound(_selector);
|
||||
QWebElement element(find(frame, script->replacevars(_selector)));
|
||||
if (element.isNull())
|
||||
throw ElementNotFound(script->replacevars(_selector));
|
||||
_result =
|
||||
element.evaluateJavaScript(script->replacevars(_javascript)).toString();
|
||||
return true;
|
||||
@@ -753,12 +825,12 @@ class Load: public Command {
|
||||
}
|
||||
QString description() const {
|
||||
return
|
||||
"load <url>"
|
||||
tag()+" <url>"
|
||||
"\n\n"
|
||||
"Load an URL, the URL is given as parameter in full syntax.";
|
||||
}
|
||||
QString command() const {
|
||||
return "load "+_url;
|
||||
return tag()+" "+_url;
|
||||
}
|
||||
std::shared_ptr<Command> parse(Script*, QString args, QStringList&, int) {
|
||||
std::shared_ptr<Load> cmd(new Load());
|
||||
@@ -781,7 +853,7 @@ class Expect: public Command {
|
||||
}
|
||||
QString description() const {
|
||||
return
|
||||
"expect <signal> [<parameter>]"
|
||||
tag()+" <signal> [<parameter>]"
|
||||
"\n\n"
|
||||
"Expect a signal. Signals are emitted by webkit and may contain "
|
||||
"parameter. If a parameter is given in the script, then the parameter "
|
||||
@@ -800,7 +872,7 @@ class Expect: public Command {
|
||||
" - loadFinished true";
|
||||
}
|
||||
QString command() const {
|
||||
return "expect "+_signal._signal
|
||||
return tag()+" "+_signal._signal
|
||||
+(_signal._args.size()?" "+_signal._args.join(' '):QString());
|
||||
}
|
||||
std::shared_ptr<Command> parse(Script*, QString args, QStringList&, int) {
|
||||
@@ -851,12 +923,12 @@ class Open: public Command {
|
||||
}
|
||||
QString description() const {
|
||||
return
|
||||
"open"
|
||||
tag()+
|
||||
"\n\n"
|
||||
"Open the browser window, so you can follow the test steps visually.";
|
||||
}
|
||||
QString command() const {
|
||||
return "open";
|
||||
return tag();
|
||||
}
|
||||
std::shared_ptr<Command> parse(Script*, QString, QStringList&, int) {
|
||||
std::shared_ptr<Open> cmd(new Open());
|
||||
@@ -876,14 +948,14 @@ class Sleep: public Command {
|
||||
}
|
||||
QString description() const {
|
||||
return
|
||||
"sleep <seconds>"
|
||||
tag()+" <seconds>"
|
||||
"\n\n"
|
||||
"Sleep for a certain amount of seconds. This helps, if you must wait "
|
||||
"for some javascript actions, i.e. AJAX or slow pages, and the "
|
||||
"excpeted signals are not sufficient.";
|
||||
}
|
||||
QString command() const {
|
||||
return "sleep "+_time;
|
||||
return tag()+" "+_time;
|
||||
}
|
||||
std::shared_ptr<Command> parse(Script*, QString time, QStringList&, int) {
|
||||
std::shared_ptr<Sleep> cmd(new Sleep());
|
||||
@@ -913,14 +985,14 @@ class Exit: public Command {
|
||||
}
|
||||
QString description() const {
|
||||
return
|
||||
"exit"
|
||||
tag()+
|
||||
"\n\n"
|
||||
"Successfully terminate script immediately. The following commands "
|
||||
"are not executed. This helps when you debug your scripts and you "
|
||||
"want the script stop at a certain point for investigations.";
|
||||
}
|
||||
QString command() const {
|
||||
return "exit";
|
||||
return tag();
|
||||
}
|
||||
std::shared_ptr<Command> parse(Script*, QString, QStringList&, int) {
|
||||
std::shared_ptr<Exit> cmd(new Exit());
|
||||
@@ -939,7 +1011,7 @@ class IgnoreTo: public Command {
|
||||
}
|
||||
QString description() const {
|
||||
return
|
||||
"ignoreto <label>"
|
||||
tag()+" <label>"
|
||||
"\n\n"
|
||||
"Ignore all following commands up to a given label. The following "
|
||||
"commands are not executed until the given label appears in the "
|
||||
@@ -947,7 +1019,7 @@ class IgnoreTo: public Command {
|
||||
"want to skip some lines of script code.";
|
||||
}
|
||||
QString command() const {
|
||||
return "ignoreto "+_label;
|
||||
return tag()+" "+_label;
|
||||
}
|
||||
std::shared_ptr<Command> parse(Script*, QString args, QStringList&, int) {
|
||||
std::shared_ptr<IgnoreTo> cmd(new IgnoreTo());
|
||||
@@ -971,12 +1043,12 @@ class Label: public Command {
|
||||
}
|
||||
QString description() const {
|
||||
return
|
||||
"label <label>"
|
||||
tag()+" <label>"
|
||||
"\n\n"
|
||||
"This marks the label refered by command \"ignoreto\".";
|
||||
}
|
||||
QString command() const {
|
||||
return "label "+_label;
|
||||
return tag()+" "+_label;
|
||||
}
|
||||
std::shared_ptr<Command> parse(Script*, QString args, QStringList&, int) {
|
||||
std::shared_ptr<Label> cmd(new Label());
|
||||
@@ -1000,23 +1072,23 @@ class Upload: public Command {
|
||||
}
|
||||
QString description() const {
|
||||
return
|
||||
"upload <selector> -> <filename>"
|
||||
tag()+" <selector> -> <filename>"
|
||||
"\n\n"
|
||||
"Presses the specified file upload button and passes a given file "
|
||||
"name. The command requires a CSS selector followed by a filename. "
|
||||
"The first object that matches the selector is used.";
|
||||
}
|
||||
QString command() const {
|
||||
return "upload "+_selector+" -> "+_filename;
|
||||
return tag()+" "+_selector+" -> "+_filename;
|
||||
}
|
||||
std::shared_ptr<Command> parse(Script*, QString args, QStringList&, int) {
|
||||
std::shared_ptr<Upload> cmd(new Upload());
|
||||
QStringList allargs(args.split("->"));
|
||||
if (allargs.size()<2)
|
||||
throw BadArgument("upload needs a selector folowed by a filename, "
|
||||
throw BadArgument(tag()+"requires <selector> -> <filename>, "
|
||||
"instead of: \""+args+"\"");
|
||||
cmd->_selector = allargs.takeFirst().trimmed();
|
||||
cmd->_filename = allargs.join(" ").trimmed();
|
||||
cmd->_filename = allargs.join("->").trimmed();
|
||||
return cmd;
|
||||
}
|
||||
bool execute(Script* script, QWebFrame* frame) {
|
||||
@@ -1047,7 +1119,7 @@ class Exists: public Command {
|
||||
}
|
||||
QString description() const {
|
||||
return
|
||||
"exists <selector> -> <text>"
|
||||
tag()+" <selector> -> <text>"
|
||||
"\n\n"
|
||||
"Assert that a certain text exists in the selected object, or if no "
|
||||
"text is given, assert that the specified object exists. The object "
|
||||
@@ -1055,7 +1127,7 @@ class Exists: public Command {
|
||||
"text.";
|
||||
}
|
||||
QString command() const {
|
||||
return "exists "+_selector+(_text.size()?" -> "+_text:QString());
|
||||
return tag()+" "+_selector+(_text.size()?" -> "+_text:QString());
|
||||
}
|
||||
std::shared_ptr<Command> parse(Script*, QString args, QStringList&, int) {
|
||||
std::shared_ptr<Exists> cmd(new Exists());
|
||||
@@ -1064,7 +1136,7 @@ class Exists: public Command {
|
||||
cmd->_selector = args;
|
||||
} else {
|
||||
cmd->_selector = allargs.takeFirst().trimmed();
|
||||
cmd->_text = allargs.join(" ").trimmed();
|
||||
cmd->_text = allargs.join("->").trimmed();
|
||||
}
|
||||
return cmd;
|
||||
}
|
||||
@@ -1099,7 +1171,7 @@ class Not: public Command {
|
||||
}
|
||||
QString description() const {
|
||||
return
|
||||
"not <selector> -> <text>"
|
||||
tag()+" <selector> -> <text>"
|
||||
"\n\n"
|
||||
"Assert that a certain text does not exists in the selected object, "
|
||||
"or if no text is given, assert that the specified object does not "
|
||||
@@ -1107,7 +1179,7 @@ class Not: public Command {
|
||||
"are search for the text.";
|
||||
}
|
||||
QString command() const {
|
||||
return "not "+_selector+(_text.size()?" -> "+_text:QString());
|
||||
return tag()+" "+_selector+(_text.size()?" -> "+_text:QString());
|
||||
}
|
||||
std::shared_ptr<Command> parse(Script*, QString args, QStringList&, int) {
|
||||
std::shared_ptr<Not> cmd(new Not());
|
||||
@@ -1116,7 +1188,7 @@ class Not: public Command {
|
||||
cmd->_selector = args;
|
||||
} else {
|
||||
cmd->_selector = allargs.takeFirst().trimmed();
|
||||
cmd->_text = allargs.join(" ").trimmed();
|
||||
cmd->_text = allargs.join("->").trimmed();
|
||||
}
|
||||
return cmd;
|
||||
}
|
||||
@@ -1145,7 +1217,7 @@ class Execute: public Command {
|
||||
}
|
||||
QString description() const {
|
||||
return
|
||||
"execute <command>\n <line1>\n <line2>\n <...>"
|
||||
tag()+" <command>\n <line1>\n <line2>\n <...>"
|
||||
"\n\n"
|
||||
"Execute <command>. The command can have space separated arguments. "
|
||||
"Following lines that are intended by at least "
|
||||
@@ -1154,8 +1226,8 @@ class Execute: public Command {
|
||||
}
|
||||
QString command() const {
|
||||
QStringList script(_script);
|
||||
script.replaceInStrings(QRegExp("^"), " ");
|
||||
return "execute "+_command
|
||||
script.replaceInStrings(QRegularExpression("^"), " ");
|
||||
return tag()+" "+_command
|
||||
+(_args.size()?" "+_args.join(' '):QString())
|
||||
+(script.size()?"\n"+script.join("\n"):QString());
|
||||
}
|
||||
@@ -1214,15 +1286,16 @@ class Download: public Command {
|
||||
}
|
||||
QString description() const {
|
||||
return
|
||||
"download <filename>"
|
||||
tag()+" <filename>"
|
||||
"<command-to-start-download>"
|
||||
"\n\n"
|
||||
"Set download file before loading a download link or clicking on a "
|
||||
"download button. The next line must be exactly one command that "
|
||||
"initiates the download.";
|
||||
"initiates the download.\n\n"
|
||||
"Please note that variables are not substituted in the filename.";
|
||||
}
|
||||
QString command() const {
|
||||
return "download"+(_filename.size()?" "+_filename:QString())+"\n"
|
||||
return tag()+(_filename.size()?" "+_filename:QString())+"\n"
|
||||
+_next->command();
|
||||
}
|
||||
std::shared_ptr<Command> parse(Script* script, QString args,
|
||||
@@ -1243,11 +1316,11 @@ class Download: public Command {
|
||||
script->timer().stop(); // no timeout during download
|
||||
for (_done = false; !_done;) // wait for download finish
|
||||
QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
|
||||
log.log("download terminated "+
|
||||
log("download terminated "+
|
||||
QString(_netsuccess&&_filesuccess?"successfully":"with error"));
|
||||
if (!_netsuccess) throw DownloadFailed(_realfilename);
|
||||
if (!_filesuccess) throw WriteFileFailed(_realfilename);
|
||||
log.plainlog("[[ATTACHMENT|"+QDir(_realfilename).absolutePath()+"]]");
|
||||
log["[[ATTACHMENT|"+QDir(_realfilename).absolutePath()+"]]"];
|
||||
disconnect(frame->page(), SIGNAL(unsupportedContent(QNetworkReply*)),
|
||||
this, SLOT(unsupportedContent(QNetworkReply*)));
|
||||
return res;
|
||||
@@ -1271,8 +1344,8 @@ class Download: public Command {
|
||||
.isValid()) {
|
||||
QString part(reply->header(QNetworkRequest::ContentDispositionHeader)
|
||||
.toString());
|
||||
if (part.contains(QRegExp("attachment; *filename="))) {
|
||||
part.replace(QRegExp(".*attachment; *filename="), "");
|
||||
if (part.contains(QRegularExpression("attachment; *filename="))) {
|
||||
part.replace(QRegularExpression(".*attachment; *filename="), "");
|
||||
if (part.size()) _realfilename = part;
|
||||
}
|
||||
}
|
||||
@@ -1293,12 +1366,12 @@ class Click: public Command {
|
||||
}
|
||||
QString description() const {
|
||||
return
|
||||
"click <selector>"
|
||||
tag()+" <selector>"
|
||||
"\n\n"
|
||||
"Click on the specified element";
|
||||
}
|
||||
QString command() const {
|
||||
return "click "+_selector;
|
||||
return tag()+" "+_selector;
|
||||
}
|
||||
std::shared_ptr<Command> parse(Script*, QString args, QStringList&, int) {
|
||||
std::shared_ptr<Click> cmd(new Click());
|
||||
@@ -1307,7 +1380,22 @@ class Click: public Command {
|
||||
}
|
||||
bool execute(Script* script, QWebFrame* frame) {
|
||||
Logger log(this, script);
|
||||
switch (script->clicktype()) {
|
||||
case Script::REAL_MOUSE_CLICK: {
|
||||
log("Script::REAL_MOUSE_CLICK");
|
||||
realMouseClick(frame, script->replacevars(_selector));
|
||||
break;
|
||||
}
|
||||
case Script::JAVASCRIPT_CLICK:
|
||||
default: {
|
||||
log("Script::JAVASCRIPT_CLICK");
|
||||
QWebElement element(find(frame, script->replacevars(_selector)));
|
||||
if (element.isNull())
|
||||
throw ElementNotFound(script->replacevars(_selector));
|
||||
_result = element.evaluateJavaScript("this.click();").toString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private:
|
||||
@@ -1321,8 +1409,8 @@ class Set: public Command {
|
||||
}
|
||||
QString description() const {
|
||||
return
|
||||
"set <variable>=<value>\n"
|
||||
"set <variable>\n"
|
||||
tag()+" <variable>=<value>\n"+
|
||||
tag()+" <variable>\n"
|
||||
" <command>"
|
||||
"\n\n"
|
||||
"Sets the value of a variable either to a constant, or to the output"
|
||||
@@ -1332,9 +1420,9 @@ class Set: public Command {
|
||||
}
|
||||
QString command() const {
|
||||
if (_next)
|
||||
return "set "+_name+"\n "+_next->command();
|
||||
return tag()+" "+_name+"\n "+_next->command();
|
||||
else
|
||||
return "set "+_name+" = "+_value;
|
||||
return tag()+" "+_name+" = "+_value;
|
||||
}
|
||||
std::shared_ptr<Command> parse(Script* script, QString args,
|
||||
QStringList& in, int line) {
|
||||
@@ -1374,12 +1462,12 @@ class UnSet: public Command {
|
||||
}
|
||||
QString description() const {
|
||||
return
|
||||
"unset <variable>"
|
||||
tag()+" <variable>"
|
||||
"\n\n"
|
||||
"Undo the setting of a variable. The opposite of «set».";
|
||||
}
|
||||
QString command() const {
|
||||
return "unset "+_name;
|
||||
return tag()+" "+_name;
|
||||
}
|
||||
std::shared_ptr<Command> parse(Script*, QString args,
|
||||
QStringList&, int) {
|
||||
@@ -1403,12 +1491,12 @@ class Timeout: public Command {
|
||||
}
|
||||
QString description() const {
|
||||
return
|
||||
"timeout <seconds>"
|
||||
tag()+" <seconds>"
|
||||
"\n\n"
|
||||
"Set the timeout in seconds (defaults to 10).";
|
||||
}
|
||||
QString command() const {
|
||||
return "timeout "+_timeout;
|
||||
return tag()+" "+_timeout;
|
||||
}
|
||||
std::shared_ptr<Command> parse(Script*, QString args,
|
||||
QStringList&, int) {
|
||||
@@ -1436,12 +1524,12 @@ class CaCertificate: public Command {
|
||||
}
|
||||
QString description() const {
|
||||
return
|
||||
"ca-certificate <filename.pem>"
|
||||
tag()+" <filename.pem>"
|
||||
"\n\n"
|
||||
"Load a CA certificate that will be accepted on SSL connections.";
|
||||
}
|
||||
QString command() const {
|
||||
return "ca-certificate "+_filename;
|
||||
return tag()+" "+_filename;
|
||||
}
|
||||
std::shared_ptr<Command> parse(Script*, QString args,
|
||||
QStringList&, int) {
|
||||
@@ -1471,7 +1559,7 @@ class ClientCertificate: public Command {
|
||||
}
|
||||
QString description() const {
|
||||
return
|
||||
"client-certificate <certfile.pem> <keyfile.key> <keypassword>"
|
||||
tag()+" <certfile.pem> <keyfile.key> <keypassword>"
|
||||
"\n\n"
|
||||
"Load a client certificate to authenticate on SSL connections. "
|
||||
"The password for the keyfile should not contain spaces. "
|
||||
@@ -1480,7 +1568,7 @@ class ClientCertificate: public Command {
|
||||
" openssl pkcs12 -in certfile.p12 -out keyfile.pem -nocerts\n";
|
||||
}
|
||||
QString command() const {
|
||||
return "client-certificate "+_certfile+" "+_keyfile+" "+_password;
|
||||
return tag()+" "+_certfile+" "+_keyfile+" "+_password;
|
||||
}
|
||||
std::shared_ptr<Command> parse(Script*, QString args,
|
||||
QStringList&, int) {
|
||||
@@ -1510,7 +1598,7 @@ class ClientCertificate: public Command {
|
||||
throw FileNotFound(filename);
|
||||
keyfile.open(QIODevice::ReadOnly);
|
||||
QSslKey k(&keyfile, QSsl::Rsa, QSsl::Pem,
|
||||
QSsl::PrivateKey, _password.toUtf8());
|
||||
QSsl::PrivateKey, script->replacevars(_password).toUtf8());
|
||||
if (k.isNull()) throw KeyNotReadable(filename);
|
||||
sslConfig.setPrivateKey(k);
|
||||
QSslConfiguration::setDefaultConfiguration(sslConfig);
|
||||
@@ -1522,6 +1610,126 @@ class ClientCertificate: public Command {
|
||||
QString _password;
|
||||
};
|
||||
|
||||
class ClickType: public Command {
|
||||
public:
|
||||
QString tag() const {
|
||||
return "clicktype";
|
||||
}
|
||||
QString description() const {
|
||||
return
|
||||
tag()+" <type>"
|
||||
"\n\n"
|
||||
"Set how mouseclicks should be mapped. The best solution depends on"
|
||||
" your problem. Normally it is good to call \"click()\" on the element"
|
||||
" using javascript. But with complex javascript infected websites, it"
|
||||
" might be necessary to simulate a real mouse click.\n\n"
|
||||
"<type> is one of:\n"
|
||||
" - realmouse\n"
|
||||
" - javascript (default)";
|
||||
}
|
||||
QString command() const {
|
||||
switch (_clicktype) {
|
||||
case Script::REAL_MOUSE_CLICK: return tag()+" realmouse";
|
||||
case Script::JAVASCRIPT_CLICK: default:
|
||||
return tag()+" javascript";
|
||||
}
|
||||
}
|
||||
std::shared_ptr<Command> parse(Script* script, QString args,
|
||||
QStringList&, int) {
|
||||
std::shared_ptr<ClickType> cmd(new ClickType());
|
||||
QString choice(script->replacevars(args).trimmed());
|
||||
if (choice=="realmouse")
|
||||
cmd->_clicktype = Script::REAL_MOUSE_CLICK;
|
||||
else if (choice=="javascript")
|
||||
cmd->_clicktype = Script::JAVASCRIPT_CLICK;
|
||||
else
|
||||
throw BadArgument("unknown clicktype: "+choice);
|
||||
return cmd;
|
||||
}
|
||||
bool execute(Script* script, QWebFrame* ) {
|
||||
Logger log(this, script);
|
||||
script->clicktype(_clicktype);
|
||||
return true;
|
||||
}
|
||||
private:
|
||||
Script::ClickType _clicktype;
|
||||
};
|
||||
|
||||
class SetValue: public Command {
|
||||
public:
|
||||
QString tag() const {
|
||||
return "setvalue";
|
||||
}
|
||||
QString description() const {
|
||||
return
|
||||
tag()+" <selector> -> '<value>'\n"+
|
||||
tag()+" <selector> -> '<value1>', '<value2>', ..."
|
||||
"\n\n"
|
||||
"Set the selected element to a given value. It is mostly the same as"
|
||||
" the following constuct, except that options in a select are evaluated"
|
||||
" correctly:\n\n"
|
||||
" do <selector>\n"
|
||||
" this.value='<value>';\n\n"
|
||||
"The second syntax with comma separated list of valus applies for"
|
||||
" options in a select element\n\n"
|
||||
"If you quote the values, then quote all values with the same"
|
||||
" quotes. If you need a comma within a value, you must quote.";
|
||||
}
|
||||
QString command() const {
|
||||
return tag()+" "+_selector+" -> "+_value;
|
||||
}
|
||||
std::shared_ptr<Command> parse(Script*, QString args,
|
||||
QStringList&, int) {
|
||||
std::shared_ptr<SetValue> cmd(new SetValue());
|
||||
QStringList allargs(args.split("->"));
|
||||
if (allargs.size()<2)
|
||||
throw BadArgument(tag()+" requires <selector> -> <value>, "
|
||||
"instead of: \""+args+"\"");
|
||||
cmd->_selector = allargs.takeFirst().trimmed();
|
||||
cmd->_value = allargs.join("->").trimmed();
|
||||
return cmd;
|
||||
}
|
||||
bool execute(Script* script, QWebFrame* frame) {
|
||||
Logger log(this, script);
|
||||
QWebElement element(find(frame, script->replacevars(_selector)));
|
||||
if (element.isNull())
|
||||
throw ElementNotFound(script->replacevars(_selector));
|
||||
QString value(script->replacevars(_value));
|
||||
if (element.tagName()=="SELECT") {
|
||||
// value is a comma seperated list of option values
|
||||
QStringList values;
|
||||
switch (value.size()>1&&value.at(0)==value.at(value.size()-1)
|
||||
?value.at(0).toLatin1():'\0') {
|
||||
case '"': case '\'': {
|
||||
values = value.mid(1, value.size()-2)
|
||||
.split(QRegularExpression(QString(value[0])+", *"
|
||||
+QString(value[0])));
|
||||
} break;
|
||||
default: {
|
||||
values = value.split(QRegularExpression(", *"));
|
||||
}
|
||||
}
|
||||
Q_FOREACH(QWebElement option, element.findAll("option")) {
|
||||
QString name(option.evaluateJavaScript("this.value").toString());
|
||||
option.evaluateJavaScript
|
||||
("this.selected="+QString(values.contains(name)?"true;":"false;"));
|
||||
}
|
||||
} else {
|
||||
if (value.size()>1 && value[0]==value[value.size()-1] &&
|
||||
(value[0]=='"' || value[0]=='\''))
|
||||
element.evaluateJavaScript("this.value="+value+";");
|
||||
else
|
||||
element.evaluateJavaScript("this.value='"
|
||||
+value.replace("'", "\\'")
|
||||
+"';");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private:
|
||||
QString _selector;
|
||||
QString _value;
|
||||
};
|
||||
|
||||
/* Template:
|
||||
class : public Command {
|
||||
public:
|
||||
@@ -1530,12 +1738,12 @@ class : public Command {
|
||||
}
|
||||
QString description() const {
|
||||
return
|
||||
""
|
||||
tag()+
|
||||
"\n\n"
|
||||
"";
|
||||
}
|
||||
QString command() const {
|
||||
return "";
|
||||
return tag();
|
||||
}
|
||||
std::shared_ptr<Command> parse(Script*, QString args,
|
||||
QStringList& in, int) {
|
||||
@@ -1555,7 +1763,7 @@ inline bool Screenshot::execute(Script* script, QWebFrame* frame) {
|
||||
QString filename(screenshot(line(), targetdir(),
|
||||
QFileInfo(testsuite()).baseName(),
|
||||
_filename, frame));
|
||||
log.plainlog("[[ATTACHMENT|"+filename+"]]");
|
||||
log["[[ATTACHMENT|"+filename+"]]"];
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1566,11 +1774,11 @@ inline Logger::Logger(Command* command, Script* script):
|
||||
_script->log(_command->command());
|
||||
}
|
||||
}
|
||||
inline void Logger::log(QString txt) {
|
||||
inline void Logger::operator()(QString txt) {
|
||||
if (_command->log())
|
||||
_script->log(txt);
|
||||
}
|
||||
inline void Logger::plainlog(QString txt) {
|
||||
inline void Logger::operator[](QString txt) {
|
||||
_script->plainlog(txt);
|
||||
}
|
||||
inline Logger::~Logger() {
|
||||
@@ -1599,6 +1807,8 @@ inline void Script::initPrototypes() {
|
||||
add(new Timeout);
|
||||
add(new CaCertificate);
|
||||
add(new ClientCertificate);
|
||||
add(new ::ClickType);
|
||||
add(new SetValue);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
263
src/testgui.hxx
263
src/testgui.hxx
@@ -21,11 +21,16 @@
|
||||
#include <stdexcept>
|
||||
#include <QNetworkReply>
|
||||
#include <QEvent>
|
||||
#include <QTextDocumentFragment>
|
||||
#include <mrw/stdext.hxx>
|
||||
|
||||
class TestGUI: public QMainWindow, protected Ui::TestGUI {
|
||||
Q_OBJECT;
|
||||
public:
|
||||
explicit TestGUI(QWidget *parent = 0, QString url = QString()):
|
||||
explicit TestGUI(QWidget *parent = 0,
|
||||
QString url = QString(),
|
||||
QString setupScript = QString(),
|
||||
QString scriptFile = QString()):
|
||||
QMainWindow(parent),
|
||||
_typing(false),
|
||||
_inEventFilter(false) {
|
||||
@@ -40,18 +45,21 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
|
||||
_web->setPage(page);
|
||||
_web->installEventFilter(this); // track mouse and keyboard
|
||||
page->setForwardUnsupportedContent(true);
|
||||
_commands->setText(Script().commands(Script::HTML));
|
||||
connect(page, SIGNAL(uploadFile(QString)), SLOT(uploadFile(QString)));
|
||||
connect(page, SIGNAL(unsupportedContent(QNetworkReply*)),
|
||||
SLOT(unsupportedContent(QNetworkReply*)));
|
||||
connect(page, SIGNAL(downloadRequested(const QNetworkRequest&)),
|
||||
SLOT(downloadRequested(const QNetworkRequest&)));
|
||||
if (setupScript.size()) loadSetup(setupScript);
|
||||
if (scriptFile.size()) loadFile(scriptFile);
|
||||
}
|
||||
virtual ~TestGUI() {}
|
||||
public Q_SLOTS:
|
||||
void on__load_clicked() {
|
||||
enterText(true);
|
||||
if (_record->isChecked())
|
||||
appendCommand("load "+_url->text());
|
||||
appendCommand("load "+map(_url->text()));
|
||||
_web->load(_url->text());
|
||||
}
|
||||
void on__abort_clicked() {
|
||||
@@ -61,28 +69,15 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
|
||||
void on__actionOpen_triggered() {
|
||||
QString name(QFileDialog::getOpenFileName(this, tr("Open Test Script")));
|
||||
if (name.isEmpty()) return;
|
||||
on__actionRevertToSaved_triggered(name);
|
||||
loadFile(name);
|
||||
}
|
||||
void on__actionOpenSetupScript_triggered() {
|
||||
QString name(QFileDialog::getOpenFileName(this, tr("Open Setup Script")));
|
||||
if (name.isEmpty()) return;
|
||||
loadSetup(name);
|
||||
}
|
||||
void on__actionRevertToSaved_triggered() {
|
||||
on__actionRevertToSaved_triggered(_filename);
|
||||
}
|
||||
void on__actionRevertToSaved_triggered(QString name) {
|
||||
QFile file(name);
|
||||
try {
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||
throw std::runtime_error("file open failed");
|
||||
_testscript->setPlainText(QString::fromUtf8(file.readAll()));
|
||||
if (file.error()!=QFileDevice::NoError)
|
||||
throw std::runtime_error("file read failed");
|
||||
_filename = name;
|
||||
_actionSave->setEnabled(true);
|
||||
_actionRevertToSaved->setEnabled(true);
|
||||
} catch(const std::exception& x) {
|
||||
QMessageBox::critical(this, tr("Open Failed"),
|
||||
tr("Reading test script failed, %2. "
|
||||
"Cannot read test script from file %1.")
|
||||
.arg(name).arg(x.what()));
|
||||
}
|
||||
loadFile(_filename);
|
||||
}
|
||||
void on__actionSaveAs_triggered() {
|
||||
QString name(QFileDialog::getSaveFileName(this, tr("Save Test Script")));
|
||||
@@ -120,18 +115,24 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
|
||||
bool oldRecordState(_record->isChecked());
|
||||
_run->setEnabled(false);
|
||||
try {
|
||||
xml::Node testsuites("testsuites");
|
||||
xml::Node testsuite("testsuite");
|
||||
testsuite.attr("name") = "on-the-fly";
|
||||
testsuite.attr("timestamp") =
|
||||
QDateTime::currentDateTime().toString(Qt::ISODate).toStdString();
|
||||
xml::Node testcase("testcase");
|
||||
testcase.attr("classname") = "testsuite-preparation";
|
||||
QString text(_testscript->textCursor().selectedText());
|
||||
if (text.isEmpty()) text = _testscript->toPlainText();
|
||||
Script script;
|
||||
connect(&script, SIGNAL(logging(QString)), SLOT(logging(QString)));
|
||||
xml::Node testsuite("testsuite");
|
||||
if (_setupscriptactive->isEnabled()
|
||||
&& _setupscriptactive->isChecked()) {
|
||||
script.parse(_setupscript->toPlainText().split('\n'));
|
||||
testsuite.attr("name") = "setup-script";
|
||||
testsuite.attr("timestamp") =
|
||||
QDateTime::currentDateTime().toString(Qt::ISODate).toStdString();
|
||||
script.run(_web->page()->mainFrame(), testsuite, QString(), false);
|
||||
script.reset();
|
||||
}
|
||||
QString text(_testscript->textCursor().selection().toPlainText());
|
||||
if (text.isEmpty()) text = _testscript->toPlainText();
|
||||
script.parse(text.split('\n'));
|
||||
testsuite.attr("name") = "setup-script";
|
||||
testsuite.attr("timestamp") =
|
||||
QDateTime::currentDateTime().toString(Qt::ISODate).toStdString();
|
||||
script.run(_web->page()->mainFrame(), testsuite, QString(), false);
|
||||
} catch (std::exception &x) {
|
||||
QMessageBox::critical(this, tr("Script Failed"),
|
||||
@@ -153,28 +154,14 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
|
||||
highlight(_web->page()->mainFrame()->documentElement()
|
||||
.findFirst(_selector->text()));
|
||||
}
|
||||
void on__jsClick_clicked() {
|
||||
enterText(true);
|
||||
execute(selector(),
|
||||
"this.click();");
|
||||
// "var evObj = document.createEvent('MouseEvents');\n"
|
||||
// "evObj.initEvent( 'click', true, true );\n"
|
||||
// "this.dispatchEvent(evObj);");
|
||||
}
|
||||
void on__jsValue_clicked() {
|
||||
enterText(true);
|
||||
QWebElement element(selected());
|
||||
execute(selector(element),
|
||||
"this.value='"+value(element).replace("\n", "\\n")+"';");
|
||||
}
|
||||
void on__jsExecute_clicked() {
|
||||
enterText(true);
|
||||
execute(selector(), _javascriptCode->toPlainText());
|
||||
_jsResult->setText(execute(selector(), _javascriptCode->toPlainText()));
|
||||
}
|
||||
void on__web_linkClicked(const QUrl& url) {
|
||||
enterText(true);
|
||||
if (_record->isChecked())
|
||||
appendCommand("load "+url.url());
|
||||
appendCommand("load "+map(url.url()));
|
||||
}
|
||||
void on__web_loadProgress(int progress) {
|
||||
enterText(true);
|
||||
@@ -183,7 +170,7 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
|
||||
void on__web_loadStarted() {
|
||||
enterText(true);
|
||||
if (_record->isChecked())
|
||||
appendCommand("expect loadStarted");
|
||||
appendCommand("expect "+map("loadStarted"));
|
||||
_progress->setValue(0);
|
||||
_urlStack->setCurrentIndex(PROGRESS_VIEW);
|
||||
}
|
||||
@@ -196,7 +183,7 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
|
||||
void on__web_urlChanged(const QUrl& url) {
|
||||
enterText(true);
|
||||
if (_record->isChecked())
|
||||
appendCommand("expect urlChanged "+url.url());
|
||||
appendCommand("expect "+map("urlChanged "+url.url()));
|
||||
}
|
||||
void on__web_selectionChanged() {
|
||||
_source->setPlainText(_web->hasSelection()
|
||||
@@ -205,15 +192,55 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
|
||||
}
|
||||
void on__web_loadFinished(bool ok) {
|
||||
enterText(true);
|
||||
if (_record->isChecked())
|
||||
appendCommand("expect loadFinished "
|
||||
+QString(ok?"true":"false"));
|
||||
if (_record->isChecked()) {
|
||||
QString text(_testscript->toPlainText());
|
||||
QStringList lines(text.split("\n"));
|
||||
if (ok && lines.size()>1 &&
|
||||
lines.last().startsWith("expect urlChanged") &&
|
||||
lines.at(lines.size()-2)=="expect loadStarted") {
|
||||
// replace three expect lines by one single line
|
||||
QString url(lines.last().replace("expect urlChanged", "").trimmed());
|
||||
lines.removeLast(); lines.removeLast();
|
||||
_testscript->setPlainText(lines.join("\n"));
|
||||
_testscript->moveCursor(QTextCursor::End);
|
||||
_testscript->ensureCursorVisible();
|
||||
appendCommand("expect "+map("load "+url));
|
||||
} else {
|
||||
appendCommand("expect "+map("loadFinished "
|
||||
+QString(ok?"true":"false")));
|
||||
}
|
||||
}
|
||||
_urlStack->setCurrentIndex(URL_VIEW);
|
||||
on__web_selectionChanged();
|
||||
setLinks();
|
||||
setForms();
|
||||
setDom();
|
||||
}
|
||||
void on__setupscript_textChanged() {
|
||||
bool oldRecordState(_record->isChecked());
|
||||
_run->setEnabled(false);
|
||||
_setupscriptactive->setEnabled(false);
|
||||
try {
|
||||
_setupscriptstatus->setText(trUtf8("?"));
|
||||
xml::Node testsuite("testsuite");
|
||||
testsuite.attr("name") = "setup-script";
|
||||
testsuite.attr("timestamp") =
|
||||
QDateTime::currentDateTime().toString(Qt::ISODate).toStdString();
|
||||
Script script;
|
||||
TestWebPage page(0, true);
|
||||
script.parse(_setupscript->toPlainText().split('\n'));
|
||||
script.run(page.mainFrame(), testsuite, QString(), false);
|
||||
_setupScript.cleanup();
|
||||
_setupScript.parse(_setupscript->toPlainText().split('\n'));
|
||||
_setupScript.run(page.mainFrame(), testsuite, QString(), false);;
|
||||
_setupscriptstatus->setText(trUtf8("✔"));
|
||||
_setupscriptactive->setEnabled(true);
|
||||
} catch (std::exception &x) {
|
||||
_setupscriptstatus->setText(trUtf8("✘"));
|
||||
}
|
||||
_run->setEnabled(true);
|
||||
_record->setChecked(oldRecordState);
|
||||
}
|
||||
void on__forms_currentItemChanged(QTreeWidgetItem* item, QTreeWidgetItem*) {
|
||||
if (!item) return;
|
||||
_source->setPlainText(item->data(0, Qt::UserRole).toString());
|
||||
@@ -225,7 +252,7 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
|
||||
void uploadFile(QString filename) {
|
||||
enterText(true);
|
||||
if (_record->isChecked())
|
||||
appendCommand("upload "+filename);
|
||||
appendCommand("upload "+map(filename));
|
||||
}
|
||||
void unsupportedContent(QNetworkReply* reply) {
|
||||
if (!_record->isChecked()) return;
|
||||
@@ -241,7 +268,8 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
|
||||
QString text(_testscript->toPlainText());
|
||||
int pos1(text.lastIndexOf(QRegularExpression("^do ")));
|
||||
int pos2(text.lastIndexOf(QRegularExpression("^load ")));
|
||||
text.insert(pos1>pos2?pos1:pos2, "download "+filename);
|
||||
int pos3(text.lastIndexOf(QRegularExpression("^click ")));
|
||||
text.insert(mrw::max(pos1, pos2, pos3), "download "+filename);
|
||||
_testscript->setPlainText(text);
|
||||
_testscript->moveCursor(QTextCursor::End);
|
||||
_testscript->ensureCursorVisible();
|
||||
@@ -259,6 +287,8 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
|
||||
void appendCommand(const QString& txt) {
|
||||
_testscript->appendPlainText(txt);
|
||||
QScrollBar *vb(_testscript->verticalScrollBar());
|
||||
_testscript->moveCursor(QTextCursor::End);
|
||||
_testscript->ensureCursorVisible();
|
||||
if (!vb) return;
|
||||
vb->setValue(vb->maximum());
|
||||
}
|
||||
@@ -315,16 +345,31 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
|
||||
if (mooCombo.hasMatch()) {
|
||||
// special treatment for moo tools combobox (e.g. used
|
||||
// in joomla)
|
||||
appendCommand("click "+mooCombo.captured(1)+">a");
|
||||
appendCommand("sleep 1");
|
||||
appendCommand("click "+map(mooCombo.captured(1)+">a"));
|
||||
appendCommand("sleep "+map("1"));
|
||||
} else if (mooComboItem.hasMatch()) {
|
||||
// special treatment for item in moo tools combobox
|
||||
appendCommand
|
||||
("click li.active-result[data-option-array-index=\""
|
||||
+element.attribute("data-option-array-index")+"\"]");
|
||||
appendCommand("sleep 1");
|
||||
("click "+map("li.active-result[data-option-array-index=\""
|
||||
+element.attribute("data-option-array-index")
|
||||
+"\"]"));
|
||||
appendCommand("sleep "+map("1"));
|
||||
} else if (_lastFocused.tagName()=="SELECT") {
|
||||
// click on a select results in a value change
|
||||
// find all selected options ...
|
||||
QStringList v;
|
||||
Q_FOREACH(QWebElement option,
|
||||
_lastFocused.findAll("option")) {
|
||||
//! @bug QT does not support selected
|
||||
if (option.evaluateJavaScript("this.selected").toBool())
|
||||
v += value(option);
|
||||
}
|
||||
setValue(selected, v);
|
||||
} else {
|
||||
appendCommand("click "+selected);
|
||||
appendCommand("click "+map(selected));
|
||||
}
|
||||
if (_lastFocused.tagName()=="INPUT") {
|
||||
_typing = true;
|
||||
}
|
||||
} else {
|
||||
appendCommand("# click, but where?");
|
||||
@@ -332,13 +377,11 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
|
||||
}
|
||||
} break;
|
||||
case QEvent::MouseButtonPress: {
|
||||
enterText(true);
|
||||
} break;
|
||||
case QEvent::ChildRemoved: { // select option value changed
|
||||
enterText(true);
|
||||
_typing = true;
|
||||
_lastFocused=element;
|
||||
_keyStrokes = "dummy";
|
||||
} break;
|
||||
case QEvent::InputMethodQuery:
|
||||
case QEvent::ToolTipChange:
|
||||
@@ -353,12 +396,44 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
|
||||
return false;
|
||||
}
|
||||
private:
|
||||
void loadFile(QString name) {
|
||||
QFile file(name);
|
||||
try {
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||
throw std::runtime_error("file open failed");
|
||||
_testscript->setPlainText(QString::fromUtf8(file.readAll()));
|
||||
if (file.error()!=QFileDevice::NoError)
|
||||
throw std::runtime_error("file read failed");
|
||||
_filename = name;
|
||||
_actionSave->setEnabled(true);
|
||||
_actionRevertToSaved->setEnabled(true);
|
||||
} catch(const std::exception& x) {
|
||||
QMessageBox::critical(this, tr("Open Failed"),
|
||||
tr("Reading test script failed, %2. "
|
||||
"Cannot read test script from file %1.")
|
||||
.arg(name).arg(x.what()));
|
||||
}
|
||||
}
|
||||
void loadSetup(QString name) {
|
||||
QFile file(name);
|
||||
try {
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||
throw std::runtime_error("file open failed");
|
||||
_setupscript->setPlainText(QString::fromUtf8(file.readAll()));
|
||||
if (file.error()!=QFileDevice::NoError)
|
||||
throw std::runtime_error("file read failed");
|
||||
on__setupscript_textChanged();
|
||||
} catch(const std::exception& x) {
|
||||
QMessageBox::critical(this, tr("Open Failed"),
|
||||
tr("Reading test script failed, %2. "
|
||||
"Cannot read test script from file %1.")
|
||||
.arg(name).arg(x.what()));
|
||||
}
|
||||
}
|
||||
void enterText(bool force=false) {
|
||||
if (!force && (!_typing || _lastFocused==focused())) return;
|
||||
if (_keyStrokes.size() && !_lastFocused.isNull()) {
|
||||
store(selector(_lastFocused), "this.value='"
|
||||
+value(_lastFocused).replace("\n", "\\n")+"';");
|
||||
}
|
||||
if (_typing && !_lastFocused.isNull())
|
||||
setValue(selector(_lastFocused), value(_lastFocused));
|
||||
_lastFocused = QWebElement();
|
||||
_keyStrokes.clear();
|
||||
_typing = false;
|
||||
@@ -451,15 +526,54 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
|
||||
// else
|
||||
// return element.toPlainText();
|
||||
}
|
||||
void store(const QString& selector, QString code) {
|
||||
if (_record->isChecked())
|
||||
appendCommand("do "+selector+"\n "
|
||||
+code.replace("\n", "\\n"));
|
||||
QString map(QString in) {
|
||||
if (_setupscriptactive->isEnabled()
|
||||
&& _setupscriptactive->isChecked()) {
|
||||
return _setupScript.insertvars(in);
|
||||
}
|
||||
void execute(const QString& selector, const QString& code) {
|
||||
store(selector, code);
|
||||
_web->page()->mainFrame()->documentElement().findFirst(selector)
|
||||
.evaluateJavaScript(code);
|
||||
return in;
|
||||
}
|
||||
void javascript(const QString& selector, QString code) {
|
||||
if (_record->isChecked())
|
||||
appendCommand("do "+map(selector)+"\n "
|
||||
+map(code).replace("\n", "\\n"));
|
||||
}
|
||||
void cleanup(const QString& selector) {
|
||||
QString text(_testscript->toPlainText());
|
||||
QStringList lines(text.split("\n"));
|
||||
bool changed(false);
|
||||
while (lines.size() &&
|
||||
(lines.last()=="click "+selector ||
|
||||
lines.last().startsWith("setvalue "+selector+" -> "))) {
|
||||
lines.removeLast();
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
_testscript->setPlainText(lines.join("\n"));
|
||||
_testscript->moveCursor(QTextCursor::End);
|
||||
_testscript->ensureCursorVisible();
|
||||
}
|
||||
}
|
||||
void setValue(const QString& selector, QString code) {
|
||||
if (_record->isChecked()) {
|
||||
cleanup(selector);
|
||||
appendCommand("setvalue "+map(selector)+" -> '"
|
||||
+map(code).replace("'", "\\'").replace("\n", "\\n")+"'");
|
||||
}
|
||||
}
|
||||
void setValue(const QString& selector, QStringList code) {
|
||||
if (_record->isChecked()) {
|
||||
cleanup(selector);
|
||||
appendCommand("setvalue "+map(selector)+" -> '"+
|
||||
map(code.replaceInStrings("'", "\\'")
|
||||
.replaceInStrings("\n", "\\n")
|
||||
.join("', '")+"'"));
|
||||
}
|
||||
}
|
||||
QString execute(const QString& selector, const QString& code) {
|
||||
javascript(selector, code);
|
||||
return _web->page()->mainFrame()->documentElement().findFirst(selector)
|
||||
.evaluateJavaScript(code).toString();
|
||||
}
|
||||
void setLinks() {
|
||||
QWebElementCollection links(_web->page()->mainFrame()->documentElement()
|
||||
@@ -675,6 +789,7 @@ class TestGUI: public QMainWindow, protected Ui::TestGUI {
|
||||
QString _keyStrokes; // collect key strokes
|
||||
bool _typing; // user is typing
|
||||
bool _inEventFilter; // actually handling event filter
|
||||
Script _setupScript;
|
||||
};
|
||||
|
||||
#endif // TESTGUI_HXX
|
||||
|
122
src/testgui.ui
122
src/testgui.ui
@@ -91,7 +91,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>888</width>
|
||||
<height>23</height>
|
||||
<height>22</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QMenu" name="menuViews">
|
||||
@@ -111,6 +111,7 @@
|
||||
<string>File</string>
|
||||
</property>
|
||||
<addaction name="_actionOpen"/>
|
||||
<addaction name="_actionOpenSetupScript"/>
|
||||
<addaction name="_actionSave"/>
|
||||
<addaction name="_actionSaveAs"/>
|
||||
<addaction name="separator"/>
|
||||
@@ -121,8 +122,15 @@
|
||||
<addaction name="_actionClear"/>
|
||||
<addaction name="_actionQuit"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuHelp">
|
||||
<property name="title">
|
||||
<string>Help</string>
|
||||
</property>
|
||||
<addaction name="_actionCommands"/>
|
||||
</widget>
|
||||
<addaction name="menuFile"/>
|
||||
<addaction name="menuViews"/>
|
||||
<addaction name="menuHelp"/>
|
||||
</widget>
|
||||
<widget class="QStatusBar" name="statusbar"/>
|
||||
<widget class="QDockWidget" name="_domDock">
|
||||
@@ -241,31 +249,14 @@
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QPushButton" name="_jsClick">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Click</string>
|
||||
<string>Result:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="_jsValue">
|
||||
<property name="text">
|
||||
<string>Set Value</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
<widget class="QLineEdit" name="_jsResult"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
@@ -397,7 +388,6 @@ this.dispatchEvent(evObj);</string>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
<zorder></zorder>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QDockWidget" name="_scriptDock">
|
||||
@@ -463,6 +453,84 @@ this.dispatchEvent(evObj);</string>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QDockWidget" name="dockWidget">
|
||||
<property name="windowTitle">
|
||||
<string>Setup Script</string>
|
||||
</property>
|
||||
<attribute name="dockWidgetArea">
|
||||
<number>4</number>
|
||||
</attribute>
|
||||
<widget class="QWidget" name="dockWidgetContents">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_7">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="_setupscriptactive">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>active</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Status:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="_setupscriptstatus">
|
||||
<property name="text">
|
||||
<string>?</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPlainTextEdit" name="_setupscript"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QDockWidget" name="dockWidget_2">
|
||||
<property name="windowTitle">
|
||||
<string>Script Commands</string>
|
||||
</property>
|
||||
<attribute name="dockWidgetArea">
|
||||
<number>4</number>
|
||||
</attribute>
|
||||
<widget class="QWidget" name="dockWidgetContents_3">
|
||||
<layout class="QGridLayout" name="gridLayout_7">
|
||||
<item row="0" column="0">
|
||||
<widget class="QTextBrowser" name="_commands">
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<action name="_actionDOMTree">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
@@ -583,6 +651,16 @@ this.dispatchEvent(evObj);</string>
|
||||
<string>Revert to saved</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="_actionOpenSetupScript">
|
||||
<property name="text">
|
||||
<string>Open Setup Script ...</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="_actionCommands">
|
||||
<property name="text">
|
||||
<string>Commands ...</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
|
@@ -13,7 +13,7 @@
|
||||
#include <xml-cxx/xml.hxx>
|
||||
#include <mrw/string.hxx>
|
||||
|
||||
std::string VERSION("0.9.4");
|
||||
std::string VERSION("0.9.5");
|
||||
|
||||
QString format(QString txt, int indent = 2, int cpl = 60) {
|
||||
QStringList res;
|
||||
|
@@ -6,9 +6,14 @@ int main(int argc, char *argv[]) try {
|
||||
QApplication a(argc, argv);
|
||||
QCommandLineParser parser;
|
||||
parser.addHelpOption();
|
||||
parser.addOption(QCommandLineOption
|
||||
(QStringList()<<"u"<<"url",
|
||||
"set initial URL to <url>", "url"));
|
||||
parser.process(a);
|
||||
QStringList urls(parser.positionalArguments());
|
||||
TestGUI w(0, urls.size()?urls[0]:"");
|
||||
QStringList scripts(parser.positionalArguments());
|
||||
TestGUI w(0, parser.value("url"),
|
||||
scripts.size()>1?scripts[0]:"",
|
||||
scripts.size()>1?scripts[1]:scripts.size()?scripts[0]:"");
|
||||
w.show();
|
||||
return a.exec();
|
||||
} catch (std::exception &x) {
|
||||
|
Reference in New Issue
Block a user