/*! @file @id $Id$ */ // 1 2 3 4 5 6 7 8 // 45678901234567890123456789012345678901234567890123456789012345678901234567890 #ifndef COMMANDS_HXX #define COMMANDS_HXX #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace std { template class optional { private: T* _opt; bool _set; public: optional(): _opt(0), _set(false) { } optional(const T& other): _opt(new T(other)), _set(true) { } ~optional() { if (_set) delete _opt; } optional& operator=(const T& other) { if (_set) delete _opt; _set = true; _opt = new T(other); return *this; } T* operator->() { return _opt; } T& operator*() { return *_opt; } const T* operator->() const { return _opt; } const T& operator*() const { return *_opt; } operator bool() const { return _set; } }; } class Script; class Command; class Function; class Logger { public: Logger(Command* command, Script* script, bool showLines = true); void operator[](QString txt); void operator()(QString txt); ~Logger(); private: Command* _command; Command* _previous; Script* _script; }; class Command: public QObject { Q_OBJECT; public: Command(): _log(true), _line(-1), _indent(0) {} virtual ~Command() {} virtual QString tag() const = 0; virtual QString description() const = 0; virtual QString command() const = 0; virtual std::shared_ptr parse(Script*, QString, QStringList&, QString, int, int) = 0; virtual bool execute(Script*, QWebFrame*) = 0; void line(int linenr) { _line = linenr; } int line() const { return _line; } void file(QString filename) { _file = filename; } QString file() const { return _file; } void indent(int i) { _indent = i; } int indent() const { return _indent; } bool log() { return _log; } void log(bool l) { _log = l; } QString result() { return _result; } virtual bool isTestcase() { return true; } static void realMouseClick(QWebFrame* frame, QString selector) { QWebElement element(find(frame, selector)); if (element.isNull()) throw ElementNotFound(selector); realMouseClick(element); } static void realMouseClick(const QWebElement& element) { QWidget* web(element.webFrame()->page()->view()); QRect elGeom=element.geometry(); QPoint elPoint=elGeom.center(); int elX=elPoint.x(); int elY=elPoint.y(); int webWidth=web->width(); int webHeight=web->height(); int pixelsToScrolRight=0; int pixelsToScrolDown=0; if (elX>webWidth) pixelsToScrolRight = //the +10 scrolls a bit further elX-webWidth+elGeom.width()/2+10; if (elY>webHeight) pixelsToScrolDown = //the +10 scrolls a bit further elY-webHeight+elGeom.height()/2+10; element.webFrame()->setScrollBarValue(Qt::Horizontal, pixelsToScrolRight); element.webFrame()->setScrollBarValue(Qt::Vertical, pixelsToScrolDown); QPoint pointToClick(elX-pixelsToScrolRight, elY-pixelsToScrolDown); QMouseEvent pressEvent(QMouseEvent::MouseButtonPress, pointToClick, Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); QCoreApplication::sendEvent(web, &pressEvent); QMouseEvent releaseEvent(QMouseEvent::MouseButtonRelease, pointToClick, Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); QCoreApplication::sendEvent(web, &releaseEvent); QCoreApplication::processEvents(); } static void sleep(int s) { QTime dieTime= QTime::currentTime().addSecs(s); while (QTime::currentTime() script, Script* parent, QWebFrame* frame, QStringList vars = QStringList(), QStringList args = QStringList()); QStringList subCommandBlock(QStringList& in) { QStringList commands; int pos(-1); while (in.size() && in[0].size() && in[0][0]==' ') { if (pos<0) pos=in[0].toStdString().find_first_not_of(' '); commands += in.takeFirst().mid(pos); } return commands; } QStringList commaSeparatedList(QString value) { value=value.trimmed(); if (!value.size()) return QStringList(); switch (value.size()>1&&value.at(0)==value.at(value.size()-1) ?value.at(0).toLatin1():'\0') { case '"': case '\'': { return value.mid(1, value.size()-2) .split(QRegularExpression(QString(value[0])+", *" +QString(value[0]))); } break; default: { return value.split(QRegularExpression(", *")); } } } static QWebElement find(QWebFrame* frame, QString selector, int repeat = 2, int sleepsec = 1) { QWebElement element; element = find1(frame, selector, repeat, sleepsec); if (!element.isNull()) return element; element = find1(frame->page()->currentFrame(), selector, repeat, sleepsec); if (!element.isNull()) return element; element = find1(frame->page()->mainFrame(), selector, repeat, sleepsec); if (!element.isNull()) return element; return element; } static QWebElement find1(QWebFrame* frame, QString selector, int repeat = 5, int sleepsec = 1) { QWebElement element; for (int i=0; ifindFirstElement(selector); if (!element.isNull()) return element; Q_FOREACH(QWebFrame* childFrame, frame->childFrames()) { element = find1(childFrame, selector, 1, 0); if (!element.isNull()) return element; } if (sleepsec) sleep(sleepsec); } return element; } void log(Script*); bool _log; protected: QString _result; private: int _line; QString _file; int _indent; }; class Empty: public Command { public: QString tag() const { return ""; } QString description() const { return "" "\n\n" "Empty lines are allowed"; } QString command() const { return tag(); } std::shared_ptr parse(Script*, QString, QStringList&, QString, int, int) { std::shared_ptr cmd(new Empty()); return cmd; } bool execute(Script*, QWebFrame*) { return true; } virtual bool isTestcase() { return false; } }; class Comment: public Command { public: Comment(QString comment): _comment(comment) {} QString tag() const { return "#"; } QString description() const { return "# comment" "\n\n" "Comments are lines that start with #"; } QString command() const { return _comment; } std::shared_ptr parse(Script*, QString args, QStringList&, QString, int, int) { std::shared_ptr cmd(new Comment(args)); return cmd; } bool execute(Script* script, QWebFrame*) { this->log(false); Logger log(this, script); this->log(true); log(_comment); this->log(false); return true; } virtual bool isTestcase() { return false; } private: QString _comment; }; class Screenshot: public Command { public: static QString sourceHtml(int line, QString target, QString base, QString name, QWebFrame* frame) { if (!QDir(target).exists() && !QDir().mkpath(target)) throw DirectoryCannotBeCreated(target); QCoreApplication::processEvents(); QString filename(target+QDir::separator()+ QString("%4-%1-%2-%3.html") .arg(base) .arg(line, 4, 10, QChar('0')) .arg(name) .arg(QDateTime::currentDateTime() .toString("yyyyMMddHHmmss"))); QFile file(filename); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) throw CannotWriteSouceHTML(filename); QTextStream out(&file); out<toHtml(); if (out.status()!=QTextStream::Ok) throw CannotWriteSouceHTML(filename); return QDir(filename).absolutePath(); } static QString screenshot(int line, QString target, QString base, QString name, QWebFrame* frame) { bool wasShown(frame->page()->view()->isVisible()); frame->page()->view()->show(); QCoreApplication::processEvents(); QImage image(frame->page()->view()->size(), QImage::Format_RGB32); QPainter painter(&image); frame->render(&painter); painter.end(); if (!wasShown) frame->page()->view()->hide(); if (!QDir(target).exists() && !QDir().mkpath(target)) throw DirectoryCannotBeCreated(target); QCoreApplication::processEvents(); QString filename(target+QDir::separator()+ QString("%4-%1-%2-%3.png") .arg(base) .arg(line, 4, 10, QChar('0')) .arg(name) .arg(QDateTime::currentDateTime() .toString("yyyyMMddHHmmss"))); if (!image.save(filename)) throw CannotWriteScreenshot(filename); return QDir(filename).absolutePath(); } QString tag() const { return "screenshot"; } QString description() const { return tag()+" " "\n\n" "Create a PNG screenshot of the actual web page and store it in the " "file .png. If not already opened, a browser window " "will pop up to take the screenshot."; } QString command() const { return tag()+" "+_filename; } std::shared_ptr parse(Script*, QString args, QStringList&, QString, int, int) { std::shared_ptr cmd(new Screenshot()); cmd->_filename = args; return cmd; } bool execute(Script* script, QWebFrame* frame); private: QString _filename; }; class RunDownload: public QObject { Q_OBJECT; public: RunDownload(QNetworkReply* reply, QString filename): _reply(reply), _file(filename) { connect(_reply, SIGNAL(finished()), SLOT(finished())); connect(_reply, SIGNAL(downloadProgress(qint64, qint64)), SLOT(downloadProgress(qint64, qint64))); _file.open(QIODevice::WriteOnly); } ~RunDownload() { disconnect(_reply, SIGNAL(finished()), this, SLOT(finished())); disconnect(_reply, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(downloadProgress(qint64, qint64))); delete _reply; } Q_SIGNALS: void completed(bool netsuccess, bool filesuccess); private Q_SLOTS: void finished() { _file.write(_reply->readAll()); _file.close(); completed(_reply->error()==QNetworkReply::NoError, _file.error()==QFileDevice::NoError); delete this; } void downloadProgress(qint64 bytes, qint64) { _file.write(_reply->read(bytes)); } private: QNetworkReply* _reply; QFile _file; }; /** @mainpage The WebTester web application testing framework consists of three parts: -# a gui to create testcases, see @ref webtester -# a command line tool to run testcases, see @ref webrunner -# test scripts, see @ref testscript. */ class Script: public QObject { Q_OBJECT; Q_SIGNALS: void logging(QString); public: typedef std::pair 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()"); attr.replace("<", "<").replace(">", ">"); return attr; } static QString xmlstr(const std::string& attr) { return xmlstr(QString::fromStdString(attr)); } static QString xmlstr(QString attr) { return attr .replace(">", ">").replace("<", "<") .replace("
", "\n").replace(""", "\"") .replace(" ", " ").replace("&", "&"); } public: Script(): _clicktype(JAVASCRIPT_CLICK), _command(0), _screenshots(true) { initPrototypes(); } Script(const Script& o): QObject(), _prototypes(o._prototypes), _script(o._script), _command(0), _screenshots(true) { set(o); } QString syntax() const { return "Script syntax is a text file that consists of list of commands. Each " "command starts at the begin of a new line. Empty lines are allowed. " "Lines that start with \"#\" are treated as comments." "\n\n" "Subcommands are indented. The first indented line defines the level of " "indentation. All following lines must be indented by the same level." "\n\n" "Note: When a selector is required as parameter, then the selector " "is a CSS selector." "\n\n" "Thanks to the filter script doxygen-webtester.sed, you cab use the " "comments for producing doxygen documenation. Just start comments with " "\"##\" to import them to doxygen. This script is automatically configured, " "when you use the autotools bootstrap from:\n" "https://dev.marc.waeckerlin.org/redmine/projects/bootstrap-build-environment"; } /// set workdir void path(QString path) { _path = (path.size()?path:".")+QDir::separator(); } /// get workdir QString path() { return _path; } QString commands(Formatting f = PLAIN) const { QString cmds; for (auto it(_prototypes.begin()); it!=_prototypes.end(); ++it) switch (f) { case PLAIN: { cmds+="\n\n\nCOMMAND: "+it->first+"\n\n"+it->second->description(); } break; case HTML: { cmds+="

"+it->first+"

" +it->second->description() .replace("&", "&") .replace("<", "<") .replace(">", ">") .replace(QRegularExpression("<([^ ]+)>"), "\\1") .replace(QRegularExpression("(\n[^ ][^\n]*)(\n +-)"), "\\1

    \\2") .replace(QRegularExpression("(\n +-[^\n]*\n)([^ ])"), "\\1
\\2") .replace(QRegularExpression("\n +- ([^\n]*)()?"), "
  • \\1
  • \\2") .replace("\n", "") .replace("\n ", "\n  ") .replace("\n\n", "

    ") .replace("\n", "
    ") +"

    "; } break; } return cmds.trimmed(); } void reset() { _script.clear(); while (!_signals.empty()) _signals.pop(); _timer.stop(); _ignores.clear(); _cout.clear(); _cerr.clear(); _ignoreSignalsUntil.clear(); } void cleanup() { reset(); _variables.clear(); _rvariables.clear(); _functions.clear(); _timeout = 20; _clicktype = JAVASCRIPT_CLICK; } std::shared_ptr parseLine(QStringList& in, QString filename, int linenr, int indent) try { std::shared_ptr command; if (!in.size()) throw MissingLine(); QString line(in.takeFirst().trimmed()); QString cmd(line), args; int space(line.indexOf(' ')); if (space>0) { cmd = line.left(space); args = line.right(line.size()-space-1); } Prototypes::const_iterator it(_prototypes.find(cmd)); if (it!=_prototypes.end()) { command = it->second->parse(this, args, in, filename, linenr, indent); } else { command = unknown(line); } command->file(filename); command->line(linenr); command->indent(indent); return command; } catch (Exception& e) { e.line(linenr); e.file(filename); throw; } void parse(QStringList in, QString filename, int line = 1, int indent = 0) { for (int linenr(0), oldsize(0); oldsize=in.size(), in.size(); linenr+=oldsize-in.size()) _script.push_back(parseLine(in, filename, line+linenr, indent)); } QStringList print() { QStringList result; for (auto cmd(_script.begin()); cmd!=_script.end(); ++cmd) { result += (*cmd)->command(); } return result; } bool run(QWebFrame* frame) { return run(frame, _testsuites, targetdir(), _screenshots, _maxretries); } bool run(QWebFrame* frame, std::shared_ptr testsuites, QString td = QString(), bool screenshots = true, int maxretries = 0) { bool res(true); _testsuites = testsuites; _timeout = 20; // defaults to 20s _ignoreSignalsUntil.clear(); addSignals(frame); _screenshots = screenshots; _maxretries = maxretries; _timer.setSingleShot(true); targetdir(!td.isEmpty() ? td : _testsuites->children() ? xmlstr(_testsuites->last().attr("name")) : "attachments"); if (!_testsuites->children()) { xml::Node testsuite("testsuite"); testsuite.attr("name") = "Unnamed Test Suite"; (*_testsuites)<command(), true).toStdString(); if (!_ignores.size() || (*cmd)->tag()=="label") { // not ignored _timer.start(_timeout*1000); try { if (!(res=(*cmd)->execute(this, frame))) { _timer.stop(); if (!back) retries = 0; else --back; testcase<<(xml::String("system-out") = xmlattr(_cout).toStdString()); testcase<<(xml::String("system-err") = xmlattr(_cerr).toStdString()); _cout.clear(); _cerr.clear(); _testsuites->last()<line(), targetdir(), _testclass, QString("retry-%1") .arg((ulong)retries, 2, 10, QLatin1Char('0')), frame)); plainlog("[[ATTACHMENT|"+filename+"]]"); } catch (... ) {} // ignore exception in screenshot if (++retries<=maxretries) { // retry in that case QUrl url(frame->url()); if ((*cmd)->command()=="expect loadFinished true") { ------cmd; back += 3; _ignoreSignalsUntil = "loadStarted"; frame->load(url); } else if ((*cmd)->command()=="expect loadStarted") { ----cmd; back += 2; _ignoreSignalsUntil = "loadStarted"; frame->page()->triggerAction(QWebPage::Stop); } else if ((*cmd)->command().startsWith("expect urlChanged")) { QString url2((*cmd)->command()); url2.remove("expect urlChanged"); if (url2.size()) url=url2.trimmed(); ----cmd; back += 2; _ignoreSignalsUntil = "loadStarted"; frame->load(url); } else if ((*cmd)->command().startsWith("expect load")) { QString url2((*cmd)->command()); url2.remove("expect load"); if (url2.size()) url=url2.trimmed(); ----cmd; back += 2; _ignoreSignalsUntil = "loadStarted"; frame->load(url); } else { throw; } } else { throw; } log(QString("WARNING: retry#%1, redo last %2 steps; error: %3") .arg(retries).arg(back).arg(e.what())); } _timer.stop(); if (!back) retries = 0; else --back; testcase<<(xml::String("system-out") = xmlattr(_cout).toStdString()); testcase<<(xml::String("system-err") = xmlattr(_cerr).toStdString()); _cout.clear(); _cerr.clear(); if ((*cmd)->isTestcase()) _testsuites->last()<isTestcase()) _testsuites->last()<line()); e.file((*cmd)->file()); if (screenshots) try { // write html source and take a last screenshot on error { QString filename(Screenshot::sourceHtml ((*cmd)->line(), targetdir(), _testclass, "error", frame)); plainlog("[[ATTACHMENT|"+filename+"]]"); } { QString filename(Screenshot::screenshot ((*cmd)->line(), targetdir(), _testclass, "error", frame)); plainlog("[[ATTACHMENT|"+filename+"]]"); } } catch (... ) {} // ignore exception in screenshot throw; } } removeSignals(frame); if (!_signals.empty()) throw UnhandledSignals(_signals); return res; } Command* command() { return _command; } void command(Command* cmd) { _command = cmd; } QString& cout() { return _cout; } QString& cerr() { return _cerr; } int steps() { return _script.size(); } bool screenshots() { return _screenshots; } void targetdir(QString name) { _targetdir = name; } QString targetdir() { return _targetdir; } void testclass(QString tc) { _testclass = tc; } QString testclass() { return _testclass; } void testsuite(QString name) { xml::Node testsuite("testsuite"); testsuite.attr("name") = xmlattr(name, true).toStdString(); testsuite.attr("timestamp") = QDateTime::currentDateTime().toString(Qt::ISODate).toStdString(); _testsuites->last().attr("failures") = "0"; (*_testsuites)<::iterator it(_variables.find(name)); if (it==_variables.end()) throw VariableNotFound(name); return *it; } /// Copy context from other script void set(const Script& o) { _variables = o._variables; _rvariables = o._rvariables; _timeout = o._timeout; _clicktype = o._clicktype; _testsuites = o._testsuites; _testclass = o._testclass; _targetdir = o._targetdir; _maxretries = o._maxretries; _screenshots = o._screenshots; _cout.clear(); _cerr.clear(); _ignoreSignalsUntil.clear(); _functions.unite(o._functions); } void unset(QString name) { _rvariables.remove(_variables[name]); _variables.remove(name); } void function(QString name, std::shared_ptr f) { _functions[name] = f; } std::shared_ptr function(QString name) { QMap >::iterator it(_functions.find(name)); if (it==_functions.end()) throw FunctionNotFound(name); return *it; } void timeout(int t) { _timeout = t; } void clicktype(ClickType c) { _clicktype = c; } ClickType clicktype() { return _clicktype; } QString replacevars(QString txt) { for(QMap::iterator it(_variables.begin()); it!=_variables.end(); ++it) txt.replace(it.key(), it.value()); return txt; } QString insertvars(QString txt) { QMapIterator it(_rvariables); it.toBack(); while (it.hasPrevious()) { it.previous(); txt.replace(it.key(), it.value()); } return txt; } QString result() { if (_script.size()) return (*_script.rbegin())->result(); return QString(); } void addSignals(QWebFrame* frame) { connect(dynamic_cast (frame->page()->networkAccessManager()), SIGNAL(log(QString)), SLOT(log(QString))); connect(frame, SIGNAL(contentsSizeChanged(const QSize&)), SLOT(contentsSizeChanged(const QSize&))); connect(frame, SIGNAL(iconChanged()), SLOT(iconChanged())); connect(frame, SIGNAL(initialLayoutCompleted()), SLOT(initialLayoutCompleted())); connect(frame, SIGNAL(javaScriptWindowObjectCleared()), SLOT(javaScriptWindowObjectCleared())); connect(frame, SIGNAL(loadFinished(bool)), SLOT(loadFinished(bool))); connect(frame, SIGNAL(loadStarted()), SLOT(loadStarted())); connect(frame, SIGNAL(titleChanged(const QString&)), SLOT(titleChanged(const QString&))); connect(frame, SIGNAL(urlChanged(const QUrl&)), SLOT(urlChanged(const QUrl&))); connect(&_timer, SIGNAL(timeout()), SLOT(timeout())); } void removeSignals(QWebFrame* frame) { disconnect(dynamic_cast (frame->page()->networkAccessManager()), SIGNAL(log(QString)), this, SLOT(log(QString))); disconnect(frame, SIGNAL(contentsSizeChanged(const QSize&)), this, SLOT(contentsSizeChanged(const QSize&))); disconnect(frame, SIGNAL(iconChanged()), this, SLOT(iconChanged())); disconnect(frame, SIGNAL(initialLayoutCompleted()), this, SLOT(initialLayoutCompleted())); disconnect(frame, SIGNAL(javaScriptWindowObjectCleared()), this, SLOT(javaScriptWindowObjectCleared())); disconnect(frame, SIGNAL(loadFinished(bool)), this, SLOT(loadFinished(bool))); disconnect(frame, SIGNAL(loadStarted()), this, SLOT(loadStarted())); disconnect(frame, SIGNAL(titleChanged(const QString&)), this, SLOT(titleChanged(const QString&))); disconnect(frame, SIGNAL(urlChanged(const QUrl&)), this, SLOT(urlChanged(const QUrl&))); disconnect(frame, SIGNAL(urlChanged(const QUrl&)), this, SLOT(urlChanged(const QUrl&))); disconnect(&_timer, SIGNAL(timeout()), this, SLOT(timeout())); } public Q_SLOTS: void log(QString text, Command* command = 0) { QString prefix (QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss ")); Command* cmd(command?command:_command); if (cmd) prefix += QString("%2:%3%1 ") .arg(QString(cmd->indent(), QChar(' '))) .arg(cmd->file(), 20, QChar(' ')) .arg(cmd->line(), -4, 10, QChar(' ')); text = prefix+text.split('\n').join("\n"+prefix+" "); logging(text); std::cout< unknown(QString command) { if (!command.size()) return std::shared_ptr(new Empty()); if (command[0]=='#') return std::shared_ptr(new Comment(command)); throw UnknownCommand(command); // error } void initPrototypes(); void add(Command* c) { _prototypes[c->tag()] = std::shared_ptr(c); } private Q_SLOTS: void contentsSizeChanged(const QSize&) { } void iconChanged() { } void initialLayoutCompleted() { } void javaScriptWindowObjectCleared() { } void loadFinished(bool ok) { QString sig(ok?"true":"false"); if (_ignoreSignalsUntil.size() && _ignoreSignalsUntil != "loadFinished "+sig) { log("warning: ignored loadFinished, waiting for "+_ignoreSignalsUntil); return; } _ignoreSignalsUntil.clear(); log(" signal received: loadFinished "+QString(ok?"true":"false")); _signals.push(std::make_pair("loadFinished", QStringList(sig))); } void loadStarted() { if (_ignoreSignalsUntil.size() && _ignoreSignalsUntil != "loadStarted") { log("warning: ignored loadStarted, waiting for "+_ignoreSignalsUntil); return; } _ignoreSignalsUntil.clear(); log(" signal received: loadStarted"); _signals.push(std::make_pair("loadStarted", QStringList())); } void frameChanged() { } void titleChanged(const QString&) { //_signals.push(std::make_pair("titleChanged", QStringList(title))); } void urlChanged(const QUrl& url) { if (_ignoreSignalsUntil.size() && _ignoreSignalsUntil != "urlChanged") { log("warning: ignored urlChanged, waiting for "+_ignoreSignalsUntil); return; } _ignoreSignalsUntil.clear(); log(" signal received: urlChanged "+url.toString()); _signals.push(std::make_pair("urlChanged", QStringList(url.toString()))); } void timeout() { throw TimeOut(); } private: typedef std::map> Prototypes; typedef std::vector> Commands; Prototypes _prototypes; Commands _script; std::queue _signals; QTimer _timer; QSet _ignores; QString _cout; QString _cerr; bool _screenshots; int _maxretries; QString _ignoreSignalsUntil; QMap _variables; ///< variable mapping QMap _rvariables; ///< reverse variable mapping QMap > _functions; int _timeout; ClickType _clicktype; QString _targetdir; std::shared_ptr _testsuites; ///< only valid within run QString _testclass; Command* _command; QString _path; }; class Do: public Command { public: QString tag() const { return "do"; } QString description() const { return tag()+" []\n \n " "\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 " "refere to the selected object within the scripy by \"this\". The " "JavaScript code is on the following lines and at least intended by " "one space"; } QString command() const { return tag()+" "+_selector+(_javascript.size()?"\n"+_javascript:""); } std::shared_ptr parse(Script*, QString args, QStringList& in, QString, int, int) { std::shared_ptr cmd(new Do()); cmd->_selector = args.trimmed(); cmd->_javascript = subCommandBlock(in).join("\n"); return cmd; } bool execute(Script* script, QWebFrame* frame) { Logger log(this, script); QWebElement element(frame->documentElement()); if (_selector.size()) { element = find(frame, script->replacevars(_selector)); if (element.isNull()) throw ElementNotFound(script->replacevars(_selector)); } _result = element.evaluateJavaScript(script->replacevars(_javascript)).toString(); log("result: "+(_result.size()?_result:"(void)")); return true; } private: QString _selector; QString _javascript; }; class Load: public Command { public: QString tag() const { return "load"; } QString description() const { return tag()+" " "\n\n" "Load an URL, the URL is given as parameter in full syntax."; } QString command() const { return tag()+" "+_url; } std::shared_ptr parse(Script*, QString args, QStringList&, QString, int, int) { std::shared_ptr cmd(new Load()); cmd->_url = args; return cmd; } bool execute(Script* script, QWebFrame* frame) { Logger log(this, script); frame->load(script->replacevars(_url)); return true; } private: QString _url; }; class Expect: public Command { public: QString tag() const { return "expect"; } QString description() const { return tag()+" []" "\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 " "must match exactly. If no parameter is given, then the signal must " "be emitted, but the parameters of the signal are not checked." "\n\n" "Known signals and parameters are:\n" " - loadFinished \n" " - loadStarted\n" " - urlChanged \n" "There is a short hand:\n" " - load \n" " stands for the three signals:\n" " - loadStarted\n" " - urlChanged \n" " - loadFinished true"; } QString command() const { return tag()+" "+_signal._signal +(_signal._args.size()?" "+_signal._args.join(' '):QString()); } std::shared_ptr parse(Script*, QString args, QStringList&, QString, int, int) { std::shared_ptr cmd(new Expect()); cmd->_signal._args = args.split(" "); cmd->_signal._signal = cmd->_signal._args.takeFirst(); return cmd; } bool execute(Script* script, QWebFrame*) { Logger log(this, script); QList sigs; if (_signal._signal=="load") { // special signal load sigs += Signal("loadStarted"); sigs += Signal("urlChanged", _signal._args); sigs += Signal("loadFinished", "true"); } else { sigs += _signal; } Q_FOREACH(Signal signal, sigs) { QStringList args; Q_FOREACH(QString arg, signal._args) args.push_back(script->replacevars(arg)); Script::Signal lastsignal(script->getSignal()); QStringList lastargs; Q_FOREACH(QString arg, lastsignal.second) lastargs.push_back(script->replacevars(arg)); if (lastsignal.first!=signal._signal || (args.size() && args!=lastargs)) throw WrongSignal(signal._signal, args, lastsignal); } return true; } private: struct Signal { Signal(QString s, QStringList a): _signal(s), _args(a) {} Signal(QString s, QString a): _signal(s), _args(a) {} Signal(QString s): _signal(s) {} Signal() {} QString _signal; QStringList _args; }; Signal _signal; }; class Open: public Command { public: QString tag() const { return "open"; } QString description() const { return tag()+ "\n\n" "Open the browser window, so you can follow the test steps visually."; } QString command() const { return tag(); } std::shared_ptr parse(Script*, QString, QStringList&, QString, int, int) { std::shared_ptr cmd(new Open()); return cmd; } bool execute(Script* script, QWebFrame* frame) { Logger log(this, script); frame->page()->view()->show(); return true; } }; class Sleep: public Command { public: QString tag() const { return "sleep"; } QString description() const { return tag()+" " "\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 tag()+" "+_time; } std::shared_ptr parse(Script*, QString time, QStringList&, QString, int, int) { std::shared_ptr cmd(new Sleep()); cmd->_time = "10"; // default: 10s if (time.size()) cmd->_time = time; return cmd; } bool execute(Script* script, QWebFrame*) { Logger log(this, script); script->timer().stop(); bool ok; unsigned int time(script->replacevars(_time).toUInt(&ok)); if (!ok) throw BadArgument(script->replacevars(_time) +" should be a number of seconds"); sleep(time); return true; } private: QString _time; }; class Exit: public Command { public: QString tag() const { return "exit"; } QString description() const { return 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 tag(); } std::shared_ptr parse(Script*, QString, QStringList&, QString, int, int) { std::shared_ptr cmd(new Exit()); return cmd; } bool execute(Script* script, QWebFrame*) { Logger log(this, script); return false; } }; class IgnoreTo: public Command { public: QString tag() const { return "ignoreto"; } QString description() const { return tag()+"