/*! @file @id $Id$ */ // 1 2 3 4 5 6 7 8 // 45678901234567890123456789012345678901234567890123456789012345678901234567890 #ifndef TESTGUI_HXX #define TESTGUI_HXX #include #include #include #include #include #include #include #include #include #include #include #include #include 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: "<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(event))); switch (event->type()) { case QEvent::KeyPress: { QKeyEvent* k(dynamic_cast(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: "<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