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.
474 lines
17 KiB
474 lines
17 KiB
10 years ago
|
/*! @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 <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())
|
||
|
_testscript->appendPlainText("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();
|
||
|
_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())
|
||
|
_testscript->appendPlainText("load "+url.url());
|
||
|
}
|
||
|
void on__web_loadProgress(int progress) {
|
||
|
enterText(true);
|
||
|
_progress->setValue(progress);
|
||
|
}
|
||
|
void on__web_loadStarted() {
|
||
|
enterText(true);
|
||
|
if (_record->isChecked())
|
||
|
_testscript->appendPlainText("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())
|
||
|
_testscript->appendPlainText("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())
|
||
|
_testscript->appendPlainText("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())
|
||
|
_testscript->appendPlainText("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(QRegExp("attachment; *filename="))) {
|
||
|
part.replace(QRegExp(".*attachment; *filename="), "");
|
||
|
if (part.size()) filename = part;
|
||
|
}
|
||
|
}
|
||
|
QString text(_testscript->toPlainText());
|
||
|
int pos1(text.lastIndexOf(QRegExp("^do ")));
|
||
|
int pos2(text.lastIndexOf(QRegExp("^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())
|
||
|
_testscript->appendPlainText("download2");
|
||
|
}
|
||
|
void logging(QString txt) {
|
||
|
_log->appendPlainText(txt);
|
||
|
}
|
||
|
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())
|
||
|
_testscript->appendPlainText("click "+selector(_lastFocused));
|
||
|
} 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) return frame->hitTestContent(event->pos()).element();
|
||
|
}
|
||
|
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())
|
||
|
_testscript->appendPlainText("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
|