Test your websites with this simple GUI based scripted webtester. Generate simple testscripts directly from surfng on the webpage, enhance them with your commands, with variables, loops, checks, … and finally run automated web tests.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

511 lines
19 KiB

/*! @file
@id $Id$
*/
// 1 2 3 4 5 6 7 8
// 45678901234567890123456789012345678901234567890123456789012345678901234567890
#ifndef TESTGUI_HXX
#define TESTGUI_HXX
#include <webpage.hxx>
#include <commands.hxx>
#include <QMainWindow>
#include <QSettings>
#include <QWebFrame>
#include <QWebElement>
#include <QFileDialog>
#include <QScrollBar>
#include <QFile>
#include <QMessageBox>
#include <ui_testgui.h>
#include <stdexcept>
#include <QNetworkReply>
class TestGUI: public QMainWindow, protected Ui::TestGUI {
Q_OBJECT;
public:
explicit TestGUI(QWidget *parent = 0, QString url = QString()):
QMainWindow(parent),
_typing(false),
_inEventFilter(false) {
setupUi(this);
QSettings settings("mrw", "webtester");
restoreGeometry(settings.value("geometry").toByteArray());
restoreState(settings.value("windowstate").toByteArray());
if (!url.isEmpty()) {
_url->setText(url);
}
TestWebPage* page(new TestWebPage(_web));
_web->setPage(page);
_web->installEventFilter(this); // track mouse and keyboard
page->setForwardUnsupportedContent(true);
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&)));
}
virtual ~TestGUI() {}
public Q_SLOTS:
void on__load_clicked() {
enterText(true);
if (_record->isChecked())
appendCommand("load "+_url->text());
_web->load(_url->text());
}
void on__abort_clicked() {
enterText(true);
_web->stop();
}
void on__actionOpen_triggered() {
QString name(QFileDialog::getOpenFileName(this, tr("Open Test Script")));
if (name.isEmpty()) return;
on__actionRevertToSaved_triggered(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()));
}
}
void on__actionSaveAs_triggered() {
QString name(QFileDialog::getSaveFileName(this, tr("Save Test Script")));
if (name.isEmpty()) return;
_filename = name;
on__actionSave_triggered();
}
void on__actionSave_triggered() {
QFile file(_filename);
try {
if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
throw std::runtime_error("file open failed");
QTextStream out(&file);
out<<_testscript->toPlainText();
if (out.status()!=QTextStream::Ok)
throw std::runtime_error(std::string("file write failed (")
+char(out.status()+48)+")");
_actionSave->setEnabled(true);
_actionRevertToSaved->setEnabled(true);
} catch(const std::exception& x) {
QMessageBox::critical(this, tr("Save Failed"),
tr("Saving test script failed, %2. "
"Cannot write test script to file %1.")
.arg(_filename).arg(x.what()));
}
}
void on__actionClear_triggered() {
_testscript->clear();
_log->clear();
_filename.clear();
_actionSave->setEnabled(false);
_actionRevertToSaved->setEnabled(false);
}
void on__run_clicked() {
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)));
script.parse(text.split('\n'));
script.run(_web->page()->mainFrame(), testsuite, QString(), false);
} catch (std::exception &x) {
QMessageBox::critical(this, tr("Script Failed"),
tr("Script failed with message:\n%1")
.arg(x.what()));
}
_run->setEnabled(true);
_record->setChecked(oldRecordState);
}
void on__focused_clicked() {
enterText(true);
QWebElement element(focused());
if (element.isNull()) return;
highlight(element);
_focusedText->setText(selector(element));
}
void on__select_clicked() {
enterText(true);
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());
}
void on__web_linkClicked(const QUrl& url) {
enterText(true);
if (_record->isChecked())
appendCommand("load "+url.url());
}
void on__web_loadProgress(int progress) {
enterText(true);
_progress->setValue(progress);
}
void on__web_loadStarted() {
enterText(true);
if (_record->isChecked())
appendCommand("expect loadStarted");
_progress->setValue(0);
_urlStack->setCurrentIndex(PROGRESS_VIEW);
}
void on__web_statusBarMessage(const QString&) {
//std::cout<<"statusBarMessage: "<<text.toStdString()<<std::endl;
}
void on__web_titleChanged(const QString&) {
//std::cout<<"titleChanged: "<<title.toStdString()<<std::endl;
}
void on__web_urlChanged(const QUrl& url) {
enterText(true);
if (_record->isChecked())
appendCommand("expect urlChanged "+url.url());
}
void on__web_selectionChanged() {
_source->setPlainText(_web->hasSelection()
? _web->selectedHtml()
: _web->page()->mainFrame()->toHtml());
}
void on__web_loadFinished(bool ok) {
enterText(true);
if (_record->isChecked())
appendCommand("expect loadFinished "
+QString(ok?"true":"false"));
_urlStack->setCurrentIndex(URL_VIEW);
on__web_selectionChanged();
setLinks();
setForms();
setDom();
}
void on__forms_currentItemChanged(QTreeWidgetItem* item, QTreeWidgetItem*) {
if (!item) return;
_source->setPlainText(item->data(0, Qt::UserRole).toString());
}
void on__dom_currentItemChanged(QTreeWidgetItem* item, QTreeWidgetItem*) {
if (!item) return;
_source->setPlainText(item->data(0, Qt::UserRole).toString());
}
void uploadFile(QString filename) {
enterText(true);
if (_record->isChecked())
appendCommand("upload "+filename);
}
void unsupportedContent(QNetworkReply* reply) {
if (!_record->isChecked()) return;
QString filename(reply->url().toString().split('/').last());
if (reply->header(QNetworkRequest::ContentDispositionHeader).isValid()) {
QString part(reply->header(QNetworkRequest::ContentDispositionHeader)
.toString());
if (part.contains(QRegularExpression("attachment; *filename="))) {
part.replace(QRegularExpression(".*attachment; *filename="), "");
if (part.size()) filename = part;
}
}
QString text(_testscript->toPlainText());
int pos1(text.lastIndexOf(QRegularExpression("^do ")));
int pos2(text.lastIndexOf(QRegularExpression("^load ")));
text.insert(pos1>pos2?pos1:pos2, "download "+filename);
_testscript->setPlainText(text);
_testscript->moveCursor(QTextCursor::End);
_testscript->ensureCursorVisible();
}
void downloadRequested(const QNetworkRequest&) {
if (_record->isChecked())
appendCommand("download2");
}
void logging(const QString& txt) {
_log->appendPlainText(txt);
QScrollBar *vb(_log->verticalScrollBar());
if (!vb) return;
vb->setValue(vb->maximum());
}
void appendCommand(const QString& txt) {
_testscript->appendPlainText(txt);
QScrollBar *vb(_testscript->verticalScrollBar());
if (!vb) return;
vb->setValue(vb->maximum());
}
protected:
void closeEvent(QCloseEvent* event) {
QSettings settings("mrw", "webtester");
settings.setValue("geometry", saveGeometry());
settings.setValue("windowstate", saveState());
QMainWindow::closeEvent(event);
}
bool eventFilter(QObject*, QEvent* event) {
if (_inEventFilter) return false;
_inEventFilter = true;
enterText();
QWebElement element(focused(dynamic_cast<QMouseEvent*>(event)));
switch (event->type()) {
case QEvent::KeyPress: {
QKeyEvent* k(dynamic_cast<QKeyEvent*>(event));
switch (k->key()) {
case Qt::Key_Tab:
case Qt::Key_Backtab: {
enterText(true);
} break;
case Qt::Key_Backspace: {
_keyStrokes.chop(1);
} break;
case Qt::Key_Shift: break;
case Qt::Key_Enter:
case Qt::Key_Return: {
_keyStrokes += "\\n";
_lastFocused=element;
_typing = true;
} break;
default: {
_keyStrokes += k->text();
_lastFocused=element;
_typing = true;
}
}
} break;
case QEvent::MouseButtonRelease: {
enterText(true);
_lastFocused=element;
if (_record->isChecked() && !element.isNull()) {
QString selected(selector(_lastFocused));
QRegularExpressionMatch mooCombo
(QRegularExpression("^(#jform_[_A-Za-z0-9]+)_chzn>.*$")
.match(selected));
QRegularExpressionMatch mooComboItem
(QRegularExpression
("^li\\.highlighted(\\.result-selected)?\\.active-result$")
.match(selected));
if (mooCombo.hasMatch()) {
// special treatment for moo tools combobox (e.g. used in joomla)
appendCommand("click "+mooCombo.captured(1)+">a");
appendCommand("sleep 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");
} else {
appendCommand("click "+selected);
}
}
} break;
case QEvent::InputMethodQuery:
case QEvent::ToolTipChange:
case QEvent::MouseMove:
case QEvent::UpdateLater:
case QEvent::Paint: break;
default: ;//LOG("Event: "<<event->type());
}
_inEventFilter = false;
return false;
}
private:
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")+"';");
}
_lastFocused = QWebElement();
_keyStrokes.clear();
_typing = false;
}
QWebElement selected() {
return _web->page()->mainFrame()->documentElement().findFirst(selector());
}
QString selector() {
if (_takeFocused->isChecked())
return selector(focused());
else if (_takeSelect->isChecked())
return _selector->text();
else
return QString(); // error
}
void highlight(QWebElement element) {
element
.evaluateJavaScript("var selection = window.getSelection();"
"selection.setBaseAndExtent(this, 0, this, 1);");
}
QWebElement focused(QMouseEvent* event = 0) {
Q_FOREACH(QWebElement element,
_web->page()->currentFrame()->findAllElements("*")) {
if (element.hasFocus()) {
return element;
}
}
if (event) { // try to find element using mouse position
QWebFrame* frame(_web->page()->frameAt(event->pos()));
if (frame) {
QWebHitTestResult hit(frame->hitTestContent(event->pos()));
if (!hit.element().isNull())
return hit.element();
if (!hit.enclosingBlockElement().isNull())
return hit.enclosingBlockElement();
}
}
return QWebElement();
}
bool unique(QString selector) {
return _web->page()->mainFrame()->findAllElements(selector).count()==1;
}
QString quote(QString txt) {
if (txt.contains('"')) return "'"+txt+"'";
return '"'+txt+'"';
}
QString selector(const QWebElement& element) {
if (element.isNull()) return QString();
if (element.hasAttribute("id") && unique("#"+element.attribute("id"))) {
return "#"+element.attribute("id");
} else if (element.hasAttribute("name") &&
unique(element.tagName().toLower()
+"[name="+quote(element.attribute("name"))+"]")) {
return element.tagName().toLower()
+"[name="+quote(element.attribute("name"))+"]";
} else {
QString res;
Q_FOREACH(QString attr, element.attributeNames()) {
if (attr=="id")
res = "#"+element.attribute("id")+res;
else if (attr=="class")
Q_FOREACH(QString c, element.attribute(attr).split(' ')) {
if (!c.isEmpty()) res = '.'+c+res;
}
else if (element.attribute(attr).isEmpty())
res+="["+attr+"]";
else
res+="["+attr+"="+quote(element.attribute(attr))+"]";
if (unique(element.tagName().toLower()+res))
return element.tagName().toLower()+res;
}
QString p(selector(element.parent()));
if (unique(p+">"+element.tagName().toLower()+res))
return p+">"+element.tagName().toLower()+res;
QString s(selector(element.previousSibling()));
if (unique(s+"+"+element.tagName().toLower()+res))
return s+"+"+element.tagName().toLower()+res;
if (!p.isEmpty())
return p+">"+element.tagName().toLower()+res;
if (!s.isEmpty())
return s+"+"+element.tagName().toLower()+res;
return element.tagName().toLower()+res;
}
}
QString value(QWebElement element) {
return element.evaluateJavaScript("this.value").toString();
//! @bug Bug in Qt, attribute("value") is always empty
// if (element.hasAttribute("value"))
// return element.attribute("value");
// else
// return element.toPlainText();
}
void store(const QString& selector, QString code) {
if (_record->isChecked())
appendCommand("do "+selector+"\n "
+code.replace("\n", "\\n"));
}
void execute(const QString& selector, const QString& code) {
store(selector, code);
_web->page()->mainFrame()->documentElement().findFirst(selector)
.evaluateJavaScript(code);
}
void setLinks() {
QWebElementCollection links(_web->page()->mainFrame()->documentElement()
.findAll("a"));
_links->setRowCount(links.count());
for (int row(0); row<_links->rowCount(); ++row) {
{
QTableWidgetItem* item(new QTableWidgetItem());
item->setText(links[row].attribute("href"));
_links->setItem(row, 0, item);
} {
QTableWidgetItem* item(new QTableWidgetItem());
item->setText(links[row].hasAttribute("title")
? links[row].attribute("title")
: links[row].toInnerXml());
_links->setItem(row, 1, item);
}
_links->horizontalHeader()->resizeSections(QHeaderView::Stretch);
}
}
void setForms() {
QWebElementCollection forms(_web->page()->mainFrame()->documentElement()
.findAll("form"));
_forms->clear();
Q_FOREACH(const QWebElement &form, forms) {
addDomElement(form, _forms->invisibleRootItem());
}
}
void setDom() {
_dom->clear();
addDomElement(_web->page()->mainFrame()->documentElement(),
_dom->invisibleRootItem());
}
//void addDomChildren(const QWebElement&, QTreeWidgetItem*);
void addDomElement(const QWebElement &element,
QTreeWidgetItem *parent) {
QTreeWidgetItem *item(new QTreeWidgetItem());
item->setText(0, element.tagName());
item->setData(0, Qt::UserRole, element.toOuterXml());
parent->addChild(item);
addDomChildren(element, item);
}
void addDomChildren(const QWebElement &parentElement,
QTreeWidgetItem *parentItem) {
for (QWebElement element = parentElement.firstChild();
!element.isNull();
element = element.nextSibling()) {
addDomElement(element, parentItem);
}
}
private:
enum UrlStack {
URL_VIEW = 0,
PROGRESS_VIEW
};
private:
QString _filename;
QWebElement _lastFocused; // cache for last focussed element
QString _keyStrokes; // collect key strokes
bool _typing; // user is typing
bool _inEventFilter; // actually handling event filter
};
#endif // TESTGUI_HXX