/*! @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 #include #ifdef HAVE_CXXABI_H #include inline QString demangle(const char* mangled) { int status; std::unique_ptr result( abi::__cxa_demangle(mangled, 0, 0, &status), free); return QString(result.get() ? result.get() : mangled); } #else inline QString demangle(const char* mangled) { return QString(mangled); } #endif 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; static void error(Logger& log, const Exception& e) { log(QString(" FAILED[")+demangle(typeid(e).name())+"]: "+e.what()); throw e; } 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(Logger& log, QWebFrame* frame, QString selector) { QWebElement element(find(frame, selector)); if (element.isNull()) error(log, 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]==' ' && pos<=(signed)in[0].toStdString().find_first_not_of(' ')) { if (pos<0) pos=in[0].toStdString().find_first_not_of(' '); commands += in.takeFirst().mid(pos); } return commands; } QStringList quotedStrings(QString value, QString delimiter = " ", bool keepDelimiters = false) { QStringList res; QString quot("'\""); while (value=value.trimmed(), value.size()) { QRegularExpression re; int start(0); if (quot.contains(value[0])) { re = QRegularExpression(value[0]+" *(("+delimiter+" *)|$)"); start = 1; } else { re = QRegularExpression("( *"+delimiter+" *)|$"); } int pos(value.indexOf(re, start)); if (pospage()->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(Logger& log, int line, QString target, QString base, QString name, QWebFrame* frame) { if (!QDir(target).exists() && !QDir().mkpath(target)) error(log, 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)) error(log, CannotWriteSouceHTML(filename)); QTextStream out(&file); out<toHtml(); if (out.status()!=QTextStream::Ok) error(log, CannotWriteSouceHTML(filename)); return QDir(filename).absolutePath(); } static QString screenshot(Logger& log, 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)) error(log, 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)) error(log, 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) { assert(connect(_reply, SIGNAL(finished()), SLOT(finished()))); assert(connect(_reply, SIGNAL(downloadProgress(qint64, qint64)), SLOT(downloadProgress(qint64, qint64)))); _file.open(QIODevice::WriteOnly); } ~RunDownload() { assert(disconnect(_reply, SIGNAL(finished()), this, SLOT(finished()))); assert(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); void progress(QString, int, int); 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()"); return attr.toHtmlEscaped(); } 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), _defaultTimeout(20) { 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 = _defaultTimeout; _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 = _defaultTimeout; // 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)<file()).arg((*cmd)->line()), step, steps()); xml::Node testcase("testcase"); try { testcase.attr("classname") = xmlattr(_testclass.size() ?_testclass :"file."+(*cmd)->file() .replace(QRegularExpression(".wt$"), "") .replace(".", "-"), true) .toStdString(); testcase.attr("name") = xmlattr(QString("%1: %2") .arg((*cmd)->line()) .arg((*cmd)->command().split('\n').takeFirst(), 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()<file()).arg((*cmd)->line()), step, steps()); } catch (PossibleRetryLoad& e) { _timer.stop(); // timeout may happen during load due to bad internet connection if (screenshots) try { // take a screenshot on error Logger log(0, this); QString filename(Screenshot::screenshot (log, (*cmd)->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; ------step; back += 3; _ignoreSignalsUntil = "loadStarted"; frame->load(url); } else if ((*cmd)->command()=="expect loadStarted") { ----cmd; ----step; 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; ----step; 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; ----step; 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 { Logger log(0, this); QString filename(Screenshot::sourceHtml (log, (*cmd)->line(), targetdir(), _testclass, "error", frame)); plainlog("[[ATTACHMENT|"+filename+"]]"); } { Logger log(0, this); QString filename(Screenshot::screenshot (log, (*cmd)->line(), targetdir(), _testclass, "error", frame)); plainlog("[[ATTACHMENT|"+filename+"]]"); } } catch (... ) {} // ignore exception in screenshot throw; } } removeSignals(frame); if (!_signals.empty()) error(UnhandledSignals(_signals)); progress("success", 0, 0); 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()) error(VariableNotFound(name)); return *it; } /// Copy context from other script void set(const Script& o) { _variables = o._variables; _rvariables = o._rvariables; _timeout = o._timeout; _defaultTimeout = 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); } QStringList functions() { return _functions.keys(); } void function(QString name, std::shared_ptr f) { _functions[name] = f; } std::shared_ptr function(Logger& log, QString name) { QMap >::iterator it(_functions.find(name)); if (it==_functions.end()) error(FunctionNotFound(name)); return *it; } void timeout(int t) { _timeout = t; } void defaultTimeout(int t) { _defaultTimeout = t; } void auth(const QString& realm, const QString& username, const QString& password) { if (!username.isEmpty() && !password.isEmpty()) _auth[realm] = {username, password}; else if (_auth.contains(realm)) _auth.erase(_auth.find(realm)); } 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(), Qt::CaseSensitive); return txt; } QStringList replacevars(QStringList txts) { for (QStringList::iterator txt(txts.begin()); txt!=txts.end(); ++txt) *txt = replacevars(*txt); return txts; } QString insertvars(QString txt) { QMapIterator it(_rvariables); it.toBack(); while (it.hasPrevious()) { it.previous(); txt.replace(it.key(), it.value(), Qt::CaseSensitive); } return txt; } QString result() { if (_script.size()) return (*_script.rbegin())->result(); return QString(); } void addSignals(QWebFrame* frame) { assert(connect(dynamic_cast (frame->page()->networkAccessManager()), SIGNAL(log(QString)), SLOT(log(QString)))); assert(connect(frame->page()->networkAccessManager(), SIGNAL(authenticationRequired(QNetworkReply*, QAuthenticator*)), SLOT(authenticationRequired(QNetworkReply*, QAuthenticator*)))); assert(connect(frame, SIGNAL(contentsSizeChanged(const QSize&)), SLOT(contentsSizeChanged(const QSize&)))); assert(connect(frame, SIGNAL(iconChanged()), SLOT(iconChanged()))); assert(connect(frame, SIGNAL(initialLayoutCompleted()), SLOT(initialLayoutCompleted()))); assert(connect(frame, SIGNAL(javaScriptWindowObjectCleared()), SLOT(javaScriptWindowObjectCleared()))); assert(connect(frame, SIGNAL(loadFinished(bool)), SLOT(loadFinished(bool)))); assert(connect(frame, SIGNAL(loadStarted()), SLOT(loadStarted()))); assert(connect(frame, SIGNAL(titleChanged(const QString&)), SLOT(titleChanged(const QString&)))); assert(connect(frame, SIGNAL(urlChanged(const QUrl&)), SLOT(urlChanged(const QUrl&)))); assert(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->page()->networkAccessManager(), SIGNAL(authenticationRequired(QNetworkReply*, QAuthenticator*)), this, SLOT(authenticationRequired(QNetworkReply*, QAuthenticator*))); 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); for (QChar& c: text) if (c<32&&c!='\n') c='?'; if (cmd) prefix += QString("%2:%3%1 ") .arg(QString(cmd->indent()+2, QChar(' '))) .arg(cmd->file(), 20, QChar(' ')) .arg(cmd->line(), -4, 10, QChar(' ')); else prefix += " .... "; 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 authenticationRequired(QNetworkReply*, QAuthenticator* a) { if (_auth.contains(a->realm())) { log("network: login to "+a->realm()); a->setUser(_auth[a->realm()].username); a->setPassword(_auth[a->realm()].password); } else { log("network: no credentials for "+a->realm()); } } 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() { error(TimeOut()); } private: struct AuthRealm { QString username; QString password; }; 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; int _defaultTimeout; ClickType _clicktype; QString _targetdir; std::shared_ptr _testsuites; ///< only valid within run QString _testclass; Command* _command; QString _path; QMap _auth; }; 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()) error(log, 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 (optional)\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); QStringList args(script->replacevars(_signal._args)); Script::Signal lastsignal(script->getSignal()); if (_signal._signal=="load") { // special signal load while (lastsignal.first=="loadStarted") { log("ignore signal: loadStarted"); lastsignal = script->getSignal(); // ignore optional loadStarted } if (lastsignal.first!="urlChanged" || (args.size() && lastsignal.second!=args)) error(log, WrongSignal("urlChanged", args, lastsignal)); lastsignal = script->getSignal(); args=QStringList("true"); if (lastsignal.first!="loadFinished" || (lastsignal.second!=args)) error(log, WrongSignal("loadFinished", args, lastsignal)); } else { if (lastsignal.first!=_signal._signal || (args.size() && lastsignal.second!=args)) error(log, 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) error(log, 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()+"