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