diff options
Diffstat (limited to 'tests/auto/widgets')
50 files changed, 2602 insertions, 943 deletions
diff --git a/tests/auto/widgets/accessibility/tst_accessibility.cpp b/tests/auto/widgets/accessibility/tst_accessibility.cpp index d69a4c0a7..0c235382a 100644 --- a/tests/auto/widgets/accessibility/tst_accessibility.cpp +++ b/tests/auto/widgets/accessibility/tst_accessibility.cpp @@ -20,9 +20,13 @@ #include <qtest.h> #include "../util.h" +#include <QHBoxLayout> +#include <QMainWindow> + #include <qaccessible.h> #include <qwebengineview.h> #include <qwebenginepage.h> +#include <qwebenginesettings.h> #include <qwidget.h> class tst_Accessibility : public QObject @@ -38,6 +42,8 @@ public Q_SLOTS: private Q_SLOTS: void noPage(); void hierarchy(); + void focusChild(); + void focusChild_data(); void text(); void value(); void roles_data(); @@ -142,6 +148,83 @@ void tst_Accessibility::hierarchy() QCOMPARE(input, child); } +void tst_Accessibility::focusChild_data() +{ + QTest::addColumn<QString>("interfaceName"); + QTest::addColumn<QVector<QAccessible::Role>>("ancestorRoles"); + + QTest::newRow("QWebEngineView") << QString("QWebEngineView") << QVector<QAccessible::Role>({QAccessible::Client}); + QTest::newRow("RenderWidgetHostViewQtDelegate") << QString("RenderWidgetHostViewQtDelegate") << QVector<QAccessible::Role>({QAccessible::Client}); + QTest::newRow("QMainWindow") << QString("QMainWindow") << QVector<QAccessible::Role>({QAccessible::Window, QAccessible::Client /* central widget */, QAccessible::Client /* view */}); +} + +void tst_Accessibility::focusChild() +{ +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 1) + QSKIP("Requires newer base Qt"); +#endif + auto traverseToWebDocumentAccessibleInterface = [](QAccessibleInterface *iface) -> QAccessibleInterface * { + QFETCH(QVector<QAccessible::Role>, ancestorRoles); + for (int i = 0; i < ancestorRoles.size(); ++i) { + if (iface->childCount() == 0 || iface->role() != ancestorRoles[i]) + return nullptr; + iface = iface->child(0); + } + + if (iface->role() != QAccessible::WebDocument) + return nullptr; + + return iface; + }; + + QMainWindow mainWindow; + QWebEngineView *webView = new QWebEngineView; + QWidget *centralWidget = new QWidget; + QHBoxLayout *centralLayout = new QHBoxLayout; + centralWidget->setLayout(centralLayout); + mainWindow.setCentralWidget(centralWidget); + centralLayout->addWidget(webView); + + mainWindow.show(); + QVERIFY(QTest::qWaitForWindowExposed(&mainWindow)); + + webView->settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, true); + webView->setHtml("<html><body>" \ + "<input id='input1' type='text' value='some text'/>" \ + "</body></html>"); + webView->show(); + QSignalSpy spyFinished(webView, &QWebEngineView::loadFinished); + QVERIFY(spyFinished.wait()); + + QVERIFY(webView->focusWidget()); + QAccessibleInterface *iface = nullptr; + QFETCH(QString, interfaceName); + if (interfaceName == "QWebEngineView") + iface = QAccessible::queryAccessibleInterface(webView); + else if (interfaceName == "RenderWidgetHostViewQtDelegate") + iface = QAccessible::queryAccessibleInterface(webView->focusWidget()); + else if (interfaceName == "QMainWindow") + iface = QAccessible::queryAccessibleInterface(&mainWindow); + QVERIFY(iface); + + // Make sure the input field does not have the focus. + evaluateJavaScriptSync(webView->page(), "document.getElementById('input1').blur()"); + QTRY_VERIFY(evaluateJavaScriptSync(webView->page(), "document.activeElement.id").toString().isEmpty()); + + QVERIFY(iface->focusChild()); + QTRY_COMPARE(iface->focusChild()->role(), QAccessible::WebDocument); + QCOMPARE(traverseToWebDocumentAccessibleInterface(iface), iface->focusChild()); + + // Set active focus on the input field. + evaluateJavaScriptSync(webView->page(), "document.getElementById('input1').focus()"); + QTRY_COMPARE(evaluateJavaScriptSync(webView->page(), "document.activeElement.id").toString(), QStringLiteral("input1")); + + QVERIFY(iface->focusChild()); + QTRY_COMPARE(iface->focusChild()->role(), QAccessible::EditableText); + // <html> -> <body> -> <input> + QCOMPARE(traverseToWebDocumentAccessibleInterface(iface)->child(0)->child(0), iface->focusChild()); +} + void tst_Accessibility::text() { QWebEngineView webView; @@ -253,138 +336,196 @@ void tst_Accessibility::value() void tst_Accessibility::roles_data() { QTest::addColumn<QString>("html"); - QTest::addColumn<bool>("isSection"); + QTest::addColumn<int>("nested"); QTest::addColumn<QAccessible::Role>("role"); - QTest::newRow("AX_ROLE_ABBR") << QString("<abbr>a</abbr>") << false << QAccessible::StaticText; - QTest::newRow("AX_ROLE_ALERT") << QString("<div role='alert'>alert</div>") << true << QAccessible::AlertMessage; - QTest::newRow("AX_ROLE_ALERT_DIALOG") << QString("<div role='alertdialog'>alert</div>") << true << QAccessible::AlertMessage; - //QTest::newRow("AX_ROLE_ANCHOR") << QString("<a>target</a>") << false << QAccessible::Link; // FIXME: The test case might be wrong (see https://codereview.chromium.org/2713193003) - QTest::newRow("AX_ROLE_ANNOTATION") << QString("<rt>a</rt>") << false << QAccessible::StaticText; - QTest::newRow("AX_ROLE_APPLICATION") << QString("<div role='application'>landmark</div>") << true << QAccessible::Document; - QTest::newRow("AX_ROLE_ARTICLE") << QString("<article>a</article>") << true << QAccessible::Section; - QTest::newRow("AX_ROLE_AUDIO") << QString("<audio controls><source src='test.mp3' type='audio/mpeg'></audio>") << false << QAccessible::Sound; - QTest::newRow("AX_ROLE_BANNER") << QString("<header>a</header>") << true << QAccessible::Section; - QTest::newRow("AX_ROLE_BLOCKQUOTE") << QString("<blockquote>a</blockquote>") << true << QAccessible::Section; - QTest::newRow("AX_ROLE_BUTTON") << QString("<button>a</button>") << false << QAccessible::Button; - //QTest::newRow("AX_ROLE_BUTTON_DROP_DOWN"); // TODO: Remove this during the next Chromium update: https://chromium-review.googlesource.com/842475 - //QTest::newRow("AX_ROLE_CANVAS") << QString("<canvas width='10' height='10'></canvas>") << true << QAccessible::Canvas; // FIXME: The test case might be wrong (see AXLayoutObject.cpp) - QTest::newRow("AX_ROLE_CAPTION") << QString("<table><caption>a</caption></table>") << false << QAccessible::Heading; - //QTest::newRow("AX_ROLE_CARET"); // Not a blink accessibility role - //QTest::newRow("AX_ROLE_CELL") << QString("<td role='cell'>a</td>") << true << QAccessible::Cell; // FIXME: Aria role 'cell' should work for <td> - QTest::newRow("AX_ROLE_CHECK_BOX") << QString("<input type='checkbox'>a</input>") << false << QAccessible::CheckBox; - QTest::newRow("AX_ROLE_CLIENT") << QString("") << true << QAccessible::Client; - QTest::newRow("AX_ROLE_COLOR_WELL") << QString("<input type='color'>a</input>") << false << QAccessible::ColorChooser; - //QTest::newRow("AX_ROLE_COLUMN") << QString("<table><tr><td>a</td></tr>") << true << QAccessible::Column; // FIXME: The test case might be wrong (see AXTableColumn.h) - QTest::newRow("AX_ROLE_COLUMN_HEADER") << QString("<div role='columnheader'>a</div>") << true << QAccessible::ColumnHeader; - QTest::newRow("AX_ROLE_COMBO_BOX_GROUPING") << QString("<div role='combobox'><input></div>") << true << QAccessible::ComboBox; - QTest::newRow("AX_ROLE_COMBO_BOX_MENU_BUTTON") << QString("<div tabindex=0 role='combobox'>Select</div>") << true << QAccessible::ComboBox; - QTest::newRow("AX_ROLE_TEXT_FIELD_WITH_COMBO_BOX") << QString("<input role='combobox'>") << false << QAccessible::ComboBox; - QTest::newRow("AX_ROLE_COMPLEMENTARY") << QString("<aside>a</aside>") << true << QAccessible::ComplementaryContent; - QTest::newRow("AX_ROLE_CONTENT_INFO") << QString("<address>a</address>") << true << QAccessible::Section; - QTest::newRow("AX_ROLE_DATE") << QString("<input type='date'></input>") << false << QAccessible::Clock; - QTest::newRow("AX_ROLE_DATE_TIME") << QString("<input type='datetime-local'></input>") << false << QAccessible::Clock; - QTest::newRow("AX_ROLE_DEFINITION") << QString("<div role='definition'>landmark</div>") << true << QAccessible::Paragraph; - QTest::newRow("AX_ROLE_DESCRIPTION_LIST") << QString("<dl>a</dl>") << true << QAccessible::List; - QTest::newRow("AX_ROLE_DESCRIPTION_LIST_DETAIL") << QString("<dd>a</dd>") << true << QAccessible::Paragraph; - QTest::newRow("AX_ROLE_DESCRIPTION_LIST_TERM") << QString("<dt>a</dt>") << true << QAccessible::ListItem; - QTest::newRow("AX_ROLE_DETAILS") << QString("<details>a</details>") << true << QAccessible::Grouping; - //QTest::newRow("AX_ROLE_DESKTOP"); // Not a blink accessibility role - QTest::newRow("AX_ROLE_DIALOG") << QString("<div role='dialog'></div>") << true << QAccessible::Dialog; - //QTest::newRow("AX_ROLE_DIRECTORY") << QString("<div role='directory'></div>") << true << QAccessible::NoRole; // FIXME: Aria role 'directory' should work - QTest::newRow("AX_ROLE_DISCLOSURE_TRIANGLE") << QString("<details><summary>a</summary></details>") << false << QAccessible::NoRole; - QTest::newRow("AX_ROLE_GENERIC_CONTAINER") << QString("<div>a</div>") << true << QAccessible::Section; - QTest::newRow("AX_ROLE_DOCUMENT") << QString("<div role='document'>a</div>") << true << QAccessible::Document; - QTest::newRow("AX_ROLE_EMBEDDED_OBJECT") << QString("<object width='10' height='10'></object>") << false << QAccessible::Grouping; - QTest::newRow("AX_ROLE_FEED") << QString("<div role='feed'>a</div>") << true << QAccessible::Section; - QTest::newRow("AX_ROLE_FIGCAPTION") << QString("<figcaption>a</figcaption>") << true << QAccessible::Heading; - QTest::newRow("AX_ROLE_FIGURE") << QString("<figure>a</figure>") << true << QAccessible::Section; - QTest::newRow("AX_ROLE_FOOTER") << QString("<footer>a</footer>") << true << QAccessible::Footer; - QTest::newRow("AX_ROLE_FORM") << QString("<form></form>") << true << QAccessible::Form; - QTest::newRow("AX_ROLE_GRID") << QString("<div role='grid'></div>") << true << QAccessible::Table; - QTest::newRow("AX_ROLE_GROUP") << QString("<fieldset></fieldset>") << true << QAccessible::Grouping; - QTest::newRow("AX_ROLE_HEADING") << QString("<h1>a</h1>") << true << QAccessible::Heading; - QTest::newRow("AX_ROLE_IFRAME") << QString("<iframe>a</iframe>") << true << QAccessible::Section; - QTest::newRow("AX_ROLE_IFRAME_PRESENTATIONAL") << QString("<iframe role='presentation'>a</iframe>") << false << QAccessible::NoRole; - //QTest::newRow("AX_ROLE_IGNORED") << QString("<tag>a</tag>") << true << QAccessible::NoRole; // FIXME: The HTML element should not be exposed as an element (see AXNodeObject.cpp) - QTest::newRow("AX_ROLE_IMAGE") << QString("<img>") << false << QAccessible::Graphic; - //QTest::newRow("AX_ROLE_IMAGE_MAP") << QString("<map>a</map>") << true << QAccessible::Graphic; // FIXME: The test case might be wrong (see AXLayoutObject.cpp) - QTest::newRow("AX_ROLE_INLINE_TEXT_BOX") << QString("<textarea rows='4' cols='50'></textarea>") << false << QAccessible::EditableText; - QTest::newRow("AX_ROLE_INPUT_TIME") << QString("<input type='time'></input>") << false << QAccessible::SpinBox; - QTest::newRow("AX_ROLE_LABEL_TEXT") << QString("<label>a</label>") << false << QAccessible::StaticText; - QTest::newRow("AX_ROLE_LEGEND") << QString("<legend>a</legend>") << true << QAccessible::StaticText; - QTest::newRow("AX_ROLE_LINE_BREAK") << QString("<br>") << false << QAccessible::Separator; - QTest::newRow("AX_ROLE_LINK") << QString("<a href=''>link</a>") << false << QAccessible::Link; - QTest::newRow("AX_ROLE_LIST") << QString("<ul></ul>") << true << QAccessible::List; - QTest::newRow("AX_ROLE_LIST_BOX") << QString("<select multiple></select>") << false << QAccessible::ComboBox; - QTest::newRow("AX_ROLE_LIST_BOX_OPTION") << QString("<option>a</option>") << true << QAccessible::ListItem; - QTest::newRow("AX_ROLE_LIST_ITEM") << QString("<li>a</li>") << true << QAccessible::ListItem; - QTest::newRow("AX_ROLE_LIST_MARKER") << QString("<li><ul></ul></li>") << false << QAccessible::StaticText; - //QTest::newRow("AX_ROLE_LOCATION_BAR"); // Not a blink accessibility role - QTest::newRow("AX_ROLE_LOG") << QString("<div role='log'>a</div>") << true << QAccessible::Section; - QTest::newRow("AX_ROLE_MAIN") << QString("<main>a</main>") << true << QAccessible::Grouping; - QTest::newRow("AX_ROLE_MARK") << QString("<mark>a</mark>") << false << QAccessible::StaticText; - QTest::newRow("AX_ROLE_MARQUEE") << QString("<div role='marquee'>a</div>") << true << QAccessible::Section; - QTest::newRow("AX_ROLE_MATH") << QString("<math>a</math>") << false << QAccessible::Equation; - QTest::newRow("AX_ROLE_MENU") << QString("<div role='menu'>a</div>") << true << QAccessible::PopupMenu; - QTest::newRow("AX_ROLE_MENU_BAR") << QString("<div role='menubar'>a</div>") << true << QAccessible::MenuBar; - QTest::newRow("AX_ROLE_MENU_ITEM") << QString("<menu role='menu'><input type='button' /></menu>") << false << QAccessible::MenuItem; - QTest::newRow("AX_ROLE_MENU_ITEM_CHECK_BOX") << QString("<menu role='menu'><input type='checkbox'></input></menu>") << false << QAccessible::CheckBox; - QTest::newRow("AX_ROLE_MENU_ITEM_RADIO") << QString("<menu role='menu'><input type='radio'></input></menu>") << false << QAccessible::RadioButton; - QTest::newRow("AX_ROLE_MENU_BUTTON") << QString("<menu role='group'><div role='menuitem'>a</div></menu>") << false << QAccessible::MenuItem; - //QTest::newRow("AX_ROLE_MENU_LIST_OPTION") << QString("<select role='menu'><option>a</option></select>") << false << QAccessible::MenuItem; // FIXME: <select> should be a menu list (see AXMenuListOption.cpp) - //QTest::newRow("AX_ROLE_MENU_LIST_POPUP") << QString("<menu type='context'>a</menu>") << true << QAccessible::PopupMenu; // FIXME: <menu> is not fully supported by Chromium (see AXMenuListPopup.h) - QTest::newRow("AX_ROLE_METER") << QString("<meter>a</meter>") << false << QAccessible::Chart; - QTest::newRow("AX_ROLE_NAVIGATION") << QString("<nav>a</nav>") << true << QAccessible::Section; - QTest::newRow("AX_ROLE_NOTE") << QString("<div role='note'>a</div>") << true << QAccessible::Note; - //QTest::newRow("AX_ROLE_PANE"); // Not a blink accessibility role - QTest::newRow("AX_ROLE_PARAGRAH") << QString("<p>a</p>") << true << QAccessible::Paragraph; - QTest::newRow("AX_ROLE_POP_UP_BUTTON") << QString("<select><option>a</option></select>") << false << QAccessible::ComboBox; - QTest::newRow("AX_ROLE_PRE") << QString("<pre>a</pre>") << true << QAccessible::Section; - //QTest::newRow("AX_ROLE_PRESENTATIONAL") << QString("<div role='presentation'>a</div>") << true << QAccessible::NoRole; // FIXME: Aria role 'presentation' should work - QTest::newRow("AX_ROLE_PROGRESS_INDICATOR") << QString("<div role='progressbar' aria-valuenow='77' aria-valuemin='22' aria-valuemax='99'></div>") << true << QAccessible::ProgressBar; - QTest::newRow("AX_ROLE_RADIO_BUTTON") << QString("<input type='radio'></input>") << false << QAccessible::RadioButton; - QTest::newRow("AX_ROLE_RADIO_GROUP") << QString("<fieldset role='radiogroup'></fieldset>") << true << QAccessible::Grouping; - QTest::newRow("AX_ROLE_REGION") << QString("<section>a</section>") << true << QAccessible::Section; - //QTest::newRow("AX_ROLE_ROW") << QString("<tr role='row'>a</tr>") << true << QAccessible::Row; // FIXME: Aria role 'row' should work for <tr> - //QTest::newRow("AX_ROLE_ROW_HEADER") << QString("<td role='rowheader'>a</td>") << true << QAccessible::RowHeader; // FIXME: Aria role 'rowheader' should work for <td> - QTest::newRow("AX_ROLE_RUBY") << QString("<ruby>a</ruby>") << false << QAccessible::StaticText; - QTest::newRow("AX_ROLE_SCROLL_BAR") << QString("<div role='scrollbar'>a</a>") << true << QAccessible::ScrollBar; - QTest::newRow("AX_ROLE_SEARCH") << QString("<div role='search'>landmark</div>") << true << QAccessible::Section; - QTest::newRow("AX_ROLE_SEARCH_BOX") << QString("<input type='search'></input>") << false << QAccessible::EditableText; - QTest::newRow("AX_ROLE_SLIDER") << QString("<input type='range'></input>") << false << QAccessible::Slider; - //QTest::newRow("AX_ROLE_SLIDER_THUMB"); // No mapping to ARIA role - QTest::newRow("AX_ROLE_SPIN_BUTTON") << QString("<input type='number'></input>") << false << QAccessible::SpinBox; - //QTest::newRow("AX_ROLE_SPIN_BUTTON_PART"); // No mapping to ARIA role - QTest::newRow("AX_ROLE_SPLITER") << QString("<hr>") << true << QAccessible::Splitter; - QTest::newRow("AX_ROLE_STATIC_TEXT") << QString("a") << false << QAccessible::StaticText; - QTest::newRow("AX_ROLE_STATUS") << QString("<output>a</output>") << false << QAccessible::Indicator; - QTest::newRow("AX_ROLE_SVG_ROOT") << QString("<svg width='10' height='10'></svg>") << false << QAccessible::Graphic; - QTest::newRow("AX_ROLE_SWITCH") << QString("<button aria-checked='false'>a</button>") << false << QAccessible::Button; - //QTest::newRow("AX_ROLE_TABLE") << QString("<table>a</table>") << true << QAccessible::Table; // FIXME: The test case might be wrong (see AXTable.cpp) - //QTest::newRow("AX_ROLE_TABLE_HEADER_CONTAINER"); // No mapping to ARIA role - QTest::newRow("AX_ROLE_TAB") << QString("<div role='tab'>a</div>") << true << QAccessible::PageTab; - QTest::newRow("AX_ROLE_TAB_LIST") << QString("<div role='tablist'>a</div>") << true << QAccessible::PageTabList; - QTest::newRow("AX_ROLE_TAB_PANEL") << QString("<div role='tab'>a</div>") << true << QAccessible::PageTab; - QTest::newRow("AX_ROLE_TERM") << QString("<div role='term'>a</div>") << true << QAccessible::StaticText; - QTest::newRow("AX_ROLE_TEXT_FIELD") << QString("<input type='text'></input>") << false << QAccessible::EditableText; - QTest::newRow("AX_ROLE_TIME") << QString("<time>a</time>") << false << QAccessible::Clock; - QTest::newRow("AX_ROLE_TIMER") << QString("<div role='timer'>a</div>") << true << QAccessible::Clock; - //QTest::newRow("AX_ROLE_TITLE_BAR"); // Not a blink accessibility role - QTest::newRow("AX_ROLE_TOGGLE_BUTTON") << QString("<button aria-pressed='false'>a</button>") << false << QAccessible::Button; - QTest::newRow("AX_ROLE_TOOLBAR") << QString("<div role='toolbar'>a</div>") << true << QAccessible::ToolBar; - QTest::newRow("AX_ROLE_TOOLTIP") << QString("<div role='tooltip'>a</div>") << true << QAccessible::ToolTip; - QTest::newRow("AX_ROLE_TREE") << QString("<div role='tree'>a</div>") << true << QAccessible::Tree; - QTest::newRow("AX_ROLE_TREE_GRID") << QString("<div role='treegrid'>a</div>") << true << QAccessible::Tree; - QTest::newRow("AX_ROLE_TREE_ITEM") << QString("<div role='treeitem'>a</div>") << true << QAccessible::TreeItem; - QTest::newRow("AX_ROLE_VIDEO") << QString("<video><source src='test.mp4' type='video/mp4'></video>") << false << QAccessible::Animation; - //QTest::newRow("AX_ROLE_WINDOW"); // No mapping to ARIA role + QTest::newRow("ax::mojom::Role::kAbbr") << QString("<abbr>a</abbr>") << 1 << QAccessible::StaticText; + QTest::newRow("ax::mojom::Role::kAlert") << QString("<div role='alert'>alert</div>") << 0 << QAccessible::AlertMessage; + QTest::newRow("ax::mojom::Role::kAlertDialog") << QString("<div role='alertdialog'>alert</div>") << 0 << QAccessible::AlertMessage; + QTest::newRow("ax::mojom::Role::kAnchor") << QString("<a id='a'>Chapter a</a>") << 1 << QAccessible::Link; + QTest::newRow("ax::mojom::Role::kApplication") << QString("<div role='application'>landmark</div>") << 0 << QAccessible::Document; + QTest::newRow("ax::mojom::Role::kArticle") << QString("<article>a</article>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kAudio") << QString("<audio controls><source src='test.mp3' type='audio/mpeg'></audio>") << 1 << QAccessible::Sound; + QTest::newRow("ax::mojom::Role::kBanner") << QString("<div role='banner'>a</div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kBlockquote") << QString("<blockquote>a</blockquote>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kButton") << QString("<button>a</button>") << 1 << QAccessible::Button; + //QTest::newRow("ax::mojom::Role::kCanvas") << QString("<canvas width='10' height='10'></canvas>") << 0 << QAccessible::Canvas; // FIXME: The test case might be wrong (see AXLayoutObject.cpp) + QTest::newRow("ax::mojom::Role::kCaption") << QString("<table><caption>a</caption></table>") << 1 << QAccessible::Heading; + //QTest::newRow("ax::mojom::Role::kCaret"); // No mapping to ARIA role + QTest::newRow("ax::mojom::Role::kCell") << QString("<table role=table><tr><td>a</td></tr></table>") << 2 << QAccessible::Cell; + QTest::newRow("ax::mojom::Role::kCheckBox") << QString("<input type='checkbox'>a</input>") << 1 << QAccessible::CheckBox; + QTest::newRow("ax::mojom::Role::kClient") << QString("") << 0 << QAccessible::Client; + QTest::newRow("ax::mojom::Role::kCode") << QString("<code>a</code>") << 1 << QAccessible::StaticText; + QTest::newRow("ax::mojom::Role::kColorWell") << QString("<input type='color'>a</input>") << 1 << QAccessible::ColorChooser; + //QTest::newRow("ax::mojom::Role::kColumn") << QString("<table><tr><td>a</td></tr></table>") << 0 << QAccessible::Column; // FIXME: The test case might be wrong (see AXTableColumn.h) + QTest::newRow("ax::mojom::Role::kColumnHeader") << QString("<table role=table><tr><th>a</th></tr><tr><td>a</td></tr></table>") << 2 << QAccessible::ColumnHeader; + QTest::newRow("ax::mojom::Role::kComboBoxGrouping") << QString("<div role='combobox'><input></div>") << 0 << QAccessible::ComboBox; + QTest::newRow("ax::mojom::Role::kComboBoxMenuButton") << QString("<div tabindex=0 role='combobox'>Select</div>") << 0 << QAccessible::ComboBox; + QTest::newRow("ax::mojom::Role::kTextFieldWithComboBox") << QString("<input role='combobox'>") << 1 << QAccessible::ComboBox; + QTest::newRow("ax::mojom::Role::kComplementary") << QString("<aside>a</aside>") << 0 << QAccessible::ComplementaryContent; + QTest::newRow("ax::mojom::Role::kComment") << QString("<div role='comment'></div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kContentDeletion") << QString("<div role='deletion'></div>") << 0 << QAccessible::Grouping; + QTest::newRow("ax::mojom::Role::kContentInsertion") << QString("<div role='insertion'></div>") << 0 << QAccessible::Grouping; + QTest::newRow("ax::mojom::Role::kContentInfo") << QString("<div role='contentinfo'></div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kData") << QString("<input type='date'></input>") << 1 << QAccessible::Clock; + QTest::newRow("ax::mojom::Role::kDateTime") << QString("<input type='datetime-local'></input>") << 1 << QAccessible::Clock; + QTest::newRow("ax::mojom::Role::kDefinition") << QString("<div role='definition'>landmark</div>") << 0 << QAccessible::Paragraph; + QTest::newRow("ax::mojom::Role::kDescriptionList") << QString("<dl>a</dl>") << 0 << QAccessible::List; + QTest::newRow("ax::mojom::Role::kDescriptionListDetail") << QString("<dd>a</dd>") << 0 << QAccessible::Paragraph; + QTest::newRow("ax::mojom::Role::kDescriptionListTerm") << QString("<dt>a</dt>") << 0 << QAccessible::ListItem; + QTest::newRow("ax::mojom::Role::kDetails") << QString("<details>a</details>") << 0 << QAccessible::Grouping; + //QTest::newRow("ax::mojom::Role::kDesktop"); // No mapping to ARIA role + QTest::newRow("ax::mojom::Role::kDialog") << QString("<div role='dialog'></div>") << 0 << QAccessible::Dialog; + //QTest::newRow("ax::mojom::Role::kDirectory") << QString("<ul role='directory'></ul>") << 0 << QAccessible::List; // FIXME: Aria role 'directory' should work + QTest::newRow("ax::mojom::Role::kDisclosureTriangle") << QString("<details><summary>a</summary></details>") << 1 << QAccessible::Button; + QTest::newRow("ax::mojom::Role::kGenericContainer") << QString("<div>a</div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kDocCover") << QString("<div role='doc-cover'></div>") << 0 << QAccessible::Graphic; + QTest::newRow("ax::mojom::Role::kDocBackLink") << QString("<div role='doc-backlink'></div>") << 0 << QAccessible::Link; + QTest::newRow("ax::mojom::Role::kDocBiblioRef") << QString("<div role='doc-biblioref'></div>") << 0 << QAccessible::Link; + QTest::newRow("ax::mojom::Role::kDocGlossRef") << QString("<div role='doc-glossref'></div>") << 0 << QAccessible::Link; + QTest::newRow("ax::mojom::Role::kDocNoteRef") << QString("<div role='doc-noteref'></div>") << 0 << QAccessible::Link; + QTest::newRow("ax::mojom::Role::kDocBiblioEntry") << QString("<div role='doc-biblioentry'></div>") << 0 << QAccessible::ListItem; + QTest::newRow("ax::mojom::Role::kDocEndnote") << QString("<div role='doc-endnote'></div>") << 0 << QAccessible::ListItem; + QTest::newRow("ax::mojom::Role::kDocFootnote") << QString("<div role='doc-footnote'></div>") << 0 << QAccessible::ListItem; + QTest::newRow("ax::mojom::Role::kDocPageBreak") << QString("<div role='doc-pagebreak'></div>") << 0 << QAccessible::Separator; + QTest::newRow("ax::mojom::Role::kDocAbstract") << QString("<div role='doc-abstract'></div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kDocAcknowledgements") << QString("<div role='doc-acknowledgments'></div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kDocAfterword") << QString("<div role='doc-afterword'></div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kDocAppendix") << QString("<div role='doc-appendix'></div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kDocBibliography") << QString("<div role='doc-bibliography'></div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kDocChapter") << QString("<div role='doc-chapter'></div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kDocColophon") << QString("<div role='doc-colophon'></div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kDocConclusion") << QString("<div role='doc-conclusion'></div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kDocCredit") << QString("<div role='doc-credit'></div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kDocCredits") << QString("<div role='doc-credits'></div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kDocDedication") << QString("<div role='doc-dedication'></div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kDocEndnotes") << QString("<div role='doc-endnotes'></div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kDocEpigraph") << QString("<div role='doc-epigraph'></div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kDocEpilogue") << QString("<div role='doc-epilogue'></div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kDocErrata") << QString("<div role='doc-errata'></div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kDocExample") << QString("<div role='doc-example'></div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kDocForeword") << QString("<div role='doc-foreword'></div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kDocGlossary") << QString("<div role='doc-glossary'></div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kDocIndex") << QString("<div role='doc-index'></div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kDocIntroduction") << QString("<div role='doc-introduction'></div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kDocNotice") << QString("<div role='doc-notice'></div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kDocPageList") << QString("<div role='doc-pagelist'></div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kDocPart") << QString("<div role='doc-part'></div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kDocPreface") << QString("<div role='doc-preface'></div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kDocPrologue") << QString("<div role='doc-prologue'></div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kDocPullquote") << QString("<div role='doc-pullquote'></div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kDocQna") << QString("<div role='doc-qna'></div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kDocSubtitle") << QString("<div role='doc-subtitle'></div>") << 0 << QAccessible::Heading; + QTest::newRow("ax::mojom::Role::kDocTip") << QString("<div role='doc-tip'></div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kDocToc") << QString("<div role='doc-toc'></div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kDocument") << QString("<div role='document'>a</div>") << 0 << QAccessible::Document; + QTest::newRow("ax::mojom::Role::kEmbeddedObject") << QString("<object width='10' height='10'></object>") << 1 << QAccessible::Grouping; + QTest::newRow("ax::mojom::Role::kEmphasis") << QString("<em>a</em>") << 1 << QAccessible::StaticText; + QTest::newRow("ax::mojom::Role::kFeed") << QString("<div role='feed'>a</div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kFigcaption") << QString("<figcaption>a</figcaption>") << 0 << QAccessible::Heading; + QTest::newRow("ax::mojom::Role::kFigure") << QString("<figure>a</figure>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kFooter") << QString("<footer>a</footer>") << 0 << QAccessible::Footer; + QTest::newRow("ax::mojom::Role::kFooterAsNonLandmark") << QString("<article><footer>a</footer><article>") << 1 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kForm") << QString("<form></form>") << 0 << QAccessible::Form; + QTest::newRow("ax::mojom::Role::kGraphicsDocument") << QString("<div role='graphics-document'></div>") << 0 << QAccessible::Document; + QTest::newRow("ax::mojom::Role::kGraphicsObject") << QString("<div role='graphics-object'></div>") << 0 << QAccessible::Pane; + QTest::newRow("ax::mojom::Role::kGraphicsSymbol") << QString("<div role='graphics-symbol'></div>") << 0 << QAccessible::Graphic; + QTest::newRow("ax::mojom::Role::kGrid") << QString("<div role='grid'></div>") << 0 << QAccessible::Table; + QTest::newRow("ax::mojom::Role::kGroup") << QString("<fieldset></fieldset>") << 0 << QAccessible::Grouping; + QTest::newRow("ax::mojom::Role::Header") << QString("<header>a</header>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::HeaderAsNonLandMark") << QString("<article><header>a</header><article>") << 1 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kHeading") << QString("<h1>a</h1>") << 0 << QAccessible::Heading; + QTest::newRow("ax::mojom::Role::kIframe") << QString("<iframe>a</iframe>") << 1 << QAccessible::WebDocument; + QTest::newRow("ax::mojom::Role::kIframePresentational") << QString("<iframe role='presentation'>a</iframe>") << 1 << QAccessible::Grouping; + //QTest::newRow("ax::mojom::Role::kIgnored") << QString("<tag>a</tag>") << 0 << QAccessible::NoRole; // FIXME: The HTML element should not be exposed as an element (see AXNodeObject.cpp) + QTest::newRow("ax::mojom::Role::kImage") << QString("<img>") << 1 << QAccessible::Graphic; + //QTest::newRow("ax::mojom::Role::kImageMap") << QString("<img usemap='map'>") << 0 << QAccessible::Document; // FIXME: AXLayoutObject::DetermineAccessiblityRole returns kImageMap but something overrides it + //QTest::newRow("ax::mojom::Role::kInlineTextBox"); // No mapping to ARIA role + QTest::newRow("ax::mojom::Role::kInputTime") << QString("<input type='time'></input>") << 1 << QAccessible::SpinBox; + //QTest::newRow("ax::mojom::Role::kKeyboard"); // No mapping to ARIA role + QTest::newRow("ax::mojom::Role::kLabelText") << QString("<label>a</label>") << 1 << QAccessible::StaticText; + QTest::newRow("ax::mojom::Role::kLayoutTable") << QString("<table><tr><td></td></tr></table>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kLayoutTableCell") << QString("<table><tr><td></td></tr></table>") << 2 << QAccessible::Section; + //QTest::newRow("ax::mojom::Role::kLayoutTableColumn") << QString("<table><tr></tr></table>") << 1 << QAccessible::Section; // FIXME: The test case might be wrong + QTest::newRow("ax::mojom::Role::kLayoutTableRow") << QString("<table><tr><td></td></tr></table>") << 1 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kLegend") << QString("<legend>a</legend>") << 0 << QAccessible::StaticText; + QTest::newRow("ax::mojom::Role::kLineBreak") << QString("<br>") << 1 << QAccessible::Separator; + QTest::newRow("ax::mojom::Role::kLink") << QString("<a href=''>link</a>") << 1 << QAccessible::Link; + QTest::newRow("ax::mojom::Role::kList") << QString("<ul></ul>") << 0 << QAccessible::List; + QTest::newRow("ax::mojom::Role::kListBox") << QString("<select multiple></select>") << 1 << QAccessible::ComboBox; + QTest::newRow("ax::mojom::Role::kListBoxOption") << QString("<option>a</option>") << 0 << QAccessible::ListItem; + QTest::newRow("ax::mojom::Role::kListItem") << QString("<li>a</li>") << 0 << QAccessible::ListItem; + //QTest::newRow("ax::mojom::Role::kListGrid"); // No mapping to ARIA role + QTest::newRow("ax::mojom::Role::kListMarker") << QString("<li><ul></ul></li>") << 1 << QAccessible::StaticText; + QTest::newRow("ax::mojom::Role::kLog") << QString("<div role='log'>a</div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kMain") << QString("<main>a</main>") << 0 << QAccessible::Grouping; + QTest::newRow("ax::mojom::Role::kMark") << QString("<mark>a</mark>") << 1 << QAccessible::StaticText; + QTest::newRow("ax::mojom::Role::kMarquee") << QString("<div role='marquee'>a</div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kMath") << QString("<math>a</math>") << 1 << QAccessible::Equation; + QTest::newRow("ax::mojom::Role::kMenu") << QString("<div role='menu'>a</div>") << 0 << QAccessible::PopupMenu; + QTest::newRow("ax::mojom::Role::kMenuBar") << QString("<div role='menubar'>a</div>") << 0 << QAccessible::MenuBar; + QTest::newRow("ax::mojom::Role::kMenuItem") << QString("<menu role='menu'><input type='button' /></menu>") << 1 << QAccessible::MenuItem; + QTest::newRow("ax::mojom::Role::kMenuItemCheckBox") << QString("<menu role='menu'><input type='checkbox'></input></menu>") << 1 << QAccessible::CheckBox; + QTest::newRow("ax::mojom::Role::kMenuItemRadio") << QString("<menu role='menu'><input type='radio'></input></menu>") << 1 << QAccessible::RadioButton; + QTest::newRow("ax::mojom::Role::kMenuButton") << QString("<menu role='group'><div role='menuitem'>a</div></menu>") << 1 << QAccessible::MenuItem; + QTest::newRow("ax::mojom::Role::kMenuListOption") << QString("<select role='menu'><option>a</option></select>") << 3 << QAccessible::MenuItem; + QTest::newRow("ax::mojom::Role::kMenuListPopup") << QString("<select role='menu'><option>a</option></select>") << 2 << QAccessible::PopupMenu; + QTest::newRow("ax::mojom::Role::kMeter") << QString("<meter>a</meter>") << 1 << QAccessible::Chart; + QTest::newRow("ax::mojom::Role::kNavigation") << QString("<nav>a</nav>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kNote") << QString("<div role='note'>a</div>") << 0 << QAccessible::Note; + //QTest::newRow("ax::mojom::Role::kPane"); // No mapping to ARIA role + QTest::newRow("ax::mojom::Role::kParagraph") << QString("<p>a</p>") << 0 << QAccessible::Paragraph; + QTest::newRow("ax::mojom::Role::kPopUpButton") << QString("<select><option>a</option></select>") << 1 << QAccessible::ComboBox; + QTest::newRow("ax::mojom::Role::kPre") << QString("<pre>a</pre>") << 0 << QAccessible::Section; + //QTest::newRow("ax::mojom::Role::kPresentational") << QString("<div role='presentation'>a</div>") << 0 << QAccessible::NoRole; // FIXME: Aria role 'presentation' should work + QTest::newRow("ax::mojom::Role::kProgressIndicator") << QString("<div role='progressbar' aria-valuenow='77' aria-valuemin='22' aria-valuemax='99'></div>") << 0 << QAccessible::ProgressBar; + QTest::newRow("ax::mojom::Role::kRadioButton") << QString("<input type='radio'></input>") << 1 << QAccessible::RadioButton; + QTest::newRow("ax::mojom::Role::kRadioGroup") << QString("<fieldset role='radiogroup'></fieldset>") << 0 << QAccessible::Grouping; + QTest::newRow("ax::mojom::Role::kRegion") << QString("<div role='region'>a</div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kRow") << QString("<table role=table><tr><td>a</td></tr></table>") << 1 << QAccessible::Row; + QTest::newRow("ax::mojom::Role::kRowGroup") << QString("<table role=table><tbody role=rowgroup><tr><td>a</td></tr></tbody></table>") << 1 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kRowHeader") << QString("<table role=table><tr><th>a</td><td>b</td></tr></table>") << 2 << QAccessible::RowHeader; + QTest::newRow("ax::mojom::Role::kRuby") << QString("<ruby>a</ruby>") << 1 << QAccessible::StaticText; + QTest::newRow("ax::mojom::Role::kRubyAnnotation") << QString("<ruby><rt>a</rt></ruby>") << 2 << QAccessible::StaticText; + QTest::newRow("ax::mojom::Role::kScrollBar") << QString("<div role='scrollbar'>a</a>") << 0 << QAccessible::ScrollBar; + //QTest::newRow("ax::mojom::Role::kScrollView"); // No mapping to ARIA role + QTest::newRow("ax::mojom::Role::kSearch") << QString("<div role='search'>landmark</div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kSearchBox") << QString("<input type='search'></input>") << 1 << QAccessible::EditableText; + QTest::newRow("ax::mojom::Role::kSection") << QString("<section></section>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kSlider") << QString("<input type='range'>") << 1 << QAccessible::Slider; + //QTest::newRow("ax::mojom::Role::kSliderThumb") << QString("<input type='range'>") << 1 << QAccessible::Slider; // TODO: blink/renderer/modules/accessibility/ax_slider.cc + QTest::newRow("ax::mojom::Role::kSpinButton") << QString("<input type='number'></input>") << 1 << QAccessible::SpinBox; + QTest::newRow("ax::mojom::Role::kSplitter") << QString("<hr>") << 0 << QAccessible::Splitter; + QTest::newRow("ax::mojom::Role::kStaticText") << QString("a") << 1 << QAccessible::StaticText; + QTest::newRow("ax::mojom::Role::kStatus") << QString("<output>a</output>") << 1 << QAccessible::Indicator; + QTest::newRow("ax::mojom::Role::kStrong") << QString("<strong>a</strong>") << 1 << QAccessible::StaticText; + QTest::newRow("ax::mojom::Role::kSuggestion") << QString("<div role='suggestion'></div>") << 0 << QAccessible::Section; + QTest::newRow("ax::mojom::Role::kSvgRoot") << QString("<svg width='10' height='10'></svg>") << 1 << QAccessible::Graphic; + QTest::newRow("ax::mojom::Role::kSwitch") << QString("<button aria-checked='false'>a</button>") << 1 << QAccessible::Button; + QTest::newRow("ax::mojom::Role::kTable") << QString("<table role=table><td>a</td></table>") << 0 << QAccessible::Table; + //QTest::newRow("ax::mojom::Role::kTableHeaderContainer"); // No mapping to ARIA role + QTest::newRow("ax::mojom::Role::kTab") << QString("<div role='tab'>a</div>") << 0 << QAccessible::PageTab; + QTest::newRow("ax::mojom::Role::kTabList") << QString("<div role='tablist'>a</div>") << 0 << QAccessible::PageTabList; + QTest::newRow("ax::mojom::Role::kTabPanel") << QString("<div role='tabpanel'>a</div>") << 0 << QAccessible::Pane; + QTest::newRow("ax::mojom::Role::kTerm") << QString("<div role='term'>a</div>") << 0 << QAccessible::StaticText; + QTest::newRow("ax::mojom::Role::kTextField") << QString("<input type='text'></input>") << 1 << QAccessible::EditableText; + QTest::newRow("ax::mojom::Role::kTime") << QString("<time>a</time>") << 1 << QAccessible::Clock; + QTest::newRow("ax::mojom::Role::kTimer") << QString("<div role='timer'>a</div>") << 0 << QAccessible::Clock; + //QTest::newRow("ax::mojom::Role::kTitleBar"); // No mapping to ARIA role + QTest::newRow("ax::mojom::Role::kToggleButton") << QString("<button aria-pressed='false'>a</button>") << 1 << QAccessible::Button; + QTest::newRow("ax::mojom::Role::kToolbar") << QString("<div role='toolbar'>a</div>") << 0 << QAccessible::ToolBar; + QTest::newRow("ax::mojom::Role::kToolTip") << QString("<div role='tooltip'>a</div>") << 0 << QAccessible::ToolTip; + QTest::newRow("ax::mojom::Role::kTree") << QString("<div role='tree'>a</div>") << 0 << QAccessible::Tree; + QTest::newRow("ax::mojom::Role::kTreeGrid") << QString("<div role='treegrid'>a</div>") << 0 << QAccessible::Tree; + QTest::newRow("ax::mojom::Role::kTreeItem") << QString("<div role='treeitem'>a</div>") << 0 << QAccessible::TreeItem; + QTest::newRow("ax::mojom::Role::kVideo") << QString("<video><source src='test.mp4' type='video/mp4'></video>") << 1 << QAccessible::Animation; + //QTest::newRow("ax::mojom::Role::kWindow"); // No mapping to ARIA role } void tst_Accessibility::roles() { QFETCH(QString, html); - QFETCH(bool, isSection); + QFETCH(int, nested); QFETCH(QAccessible::Role, role); QWebEngineView webView; @@ -403,20 +544,19 @@ void tst_Accessibility::roles() QTRY_COMPARE(view->child(0)->childCount(), 1); QAccessibleInterface *document = view->child(0); - QAccessibleInterface *section = document->child(0); + QAccessibleInterface *element = document->child(0); - if (isSection) { - QCOMPARE(section->role(), role); - return; + while (nested--) { + QTRY_VERIFY(element->child(0)); + element = element->child(0); } - QVERIFY(section->childCount() > 0); - QAccessibleInterface *element = section->child(0); QCOMPARE(element->role(), role); } static QByteArrayList params = QByteArrayList() - << "--force-renderer-accessibility"; + << "--force-renderer-accessibility" + << "--enable-features=AccessibilityExposeARIAAnnotations"; W_QTEST_MAIN(tst_Accessibility, params) #include "tst_accessibility.moc" diff --git a/tests/auto/widgets/certificateerror/BLACKLIST b/tests/auto/widgets/certificateerror/BLACKLIST new file mode 100644 index 000000000..a8fd16bf3 --- /dev/null +++ b/tests/auto/widgets/certificateerror/BLACKLIST @@ -0,0 +1,2 @@ +[fatalError] +* diff --git a/tests/auto/widgets/certificateerror/tst_certificateerror.cpp b/tests/auto/widgets/certificateerror/tst_certificateerror.cpp index f11d9236c..7a55e306d 100644 --- a/tests/auto/widgets/certificateerror/tst_certificateerror.cpp +++ b/tests/auto/widgets/certificateerror/tst_certificateerror.cpp @@ -30,6 +30,7 @@ #include <QWebEngineCertificateError> #include <QWebEnginePage> +#include <QWebEngineProfile> #include <QWebEngineSettings> #include <QtTest/QtTest> @@ -43,6 +44,7 @@ public: private Q_SLOTS: void handleError_data(); void handleError(); + void fatalError(); }; struct PageWithCertificateErrorHandler : QWebEnginePage @@ -98,8 +100,8 @@ void tst_CertificateError::handleError() QVERIFY(page.error->isOverridable()); auto chain = page.error->certificateChain(); QCOMPARE(chain.size(), 2); - QCOMPARE(chain[0].serialNumber(), "3b:dd:1a:b7:2f:40:32:3b:c1:bf:37:d4:86:bd:56:c1:d0:6b:2a:43"); - QCOMPARE(chain[1].serialNumber(), "6d:52:fb:b4:57:3b:b2:03:c8:62:7b:7e:44:45:5c:d3:08:87:74:17"); + QCOMPARE(chain[0].serialNumber(), "15:91:08:23:37:91:ee:51:00:d7:4a:db:d7:8c:3b:31:f8:4f:f3:b3"); + QCOMPARE(chain[1].serialNumber(), "3c:16:83:83:59:c4:2a:65:8f:7a:b2:07:10:14:4e:2d:70:9a:3e:23"); if (deferError) { QVERIFY(page.error->deferred()); @@ -120,5 +122,22 @@ void tst_CertificateError::handleError() QCOMPARE(toPlainTextSync(&page), expectedContent); } +void tst_CertificateError::fatalError() +{ + PageWithCertificateErrorHandler page(false, false); + page.profile()->setUseForGlobalCertificateVerification(); + page.settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, false); + QSignalSpy loadFinishedSpy(&page, &QWebEnginePage::loadFinished); + + page.setUrl(QUrl("https://revoked.badssl.com")); + if (!loadFinishedSpy.wait(10000)) + QSKIP("Couldn't load page from network, skipping test."); + QTRY_VERIFY(page.error); + QVERIFY(!page.error->isOverridable()); + + // Fatal certificate errors are implicitly rejected. This should not cause crash. + page.error->rejectCertificate(); +} + QTEST_MAIN(tst_CertificateError) #include <tst_certificateerror.moc> diff --git a/tests/auto/widgets/loadsignals/BLACKLIST b/tests/auto/widgets/loadsignals/BLACKLIST deleted file mode 100644 index 570666a83..000000000 --- a/tests/auto/widgets/loadsignals/BLACKLIST +++ /dev/null @@ -1,14 +0,0 @@ -[secondLoadForError_WhenErrorPageEnabled:ErrorPageEnabled] -* - -# QTBUG-65223 -[loadStartedAndFinishedCount:WithAnchorClickedFromJS] -* - -# QTBUG-66869 (https://codereview.qt-project.org/#/c/222112/ is only a workaround) -[loadAfterInPageNavigation_qtbug66869] -* - -# QTBUG-66661 -[fileDownloadDoesNotTriggerLoadSignals_qtbug66661] -* diff --git a/tests/auto/widgets/loadsignals/loadsignals.pro b/tests/auto/widgets/loadsignals/loadsignals.pro index e99c7f493..9c239f1a7 100644 --- a/tests/auto/widgets/loadsignals/loadsignals.pro +++ b/tests/auto/widgets/loadsignals/loadsignals.pro @@ -1 +1,2 @@ include(../tests.pri) +include(../../shared/http.pri) diff --git a/tests/auto/widgets/loadsignals/resources/downloadable.tar.gz b/tests/auto/widgets/loadsignals/resources/downloadable.tar.gz Binary files differdeleted file mode 100644 index 741cb8ca6..000000000 --- a/tests/auto/widgets/loadsignals/resources/downloadable.tar.gz +++ /dev/null diff --git a/tests/auto/widgets/loadsignals/resources/page1.html b/tests/auto/widgets/loadsignals/resources/page1.html deleted file mode 100644 index 5cd479ab6..000000000 --- a/tests/auto/widgets/loadsignals/resources/page1.html +++ /dev/null @@ -1,8 +0,0 @@ -<html> - <head> - <title>page1</title> - </head> - <body> - <h1>page1</h1> - </body> -</html> diff --git a/tests/auto/widgets/loadsignals/resources/page2.html b/tests/auto/widgets/loadsignals/resources/page2.html deleted file mode 100644 index e3031f56a..000000000 --- a/tests/auto/widgets/loadsignals/resources/page2.html +++ /dev/null @@ -1,14 +0,0 @@ -<html> - <head> - <title>page2</title> - </head> - <style> - .fardown { - position: absolute; - top: 2500px; - } - </style> - <body> - <div class="fardown" id="anchor">page2 anchor</div> - </body> -</html> diff --git a/tests/auto/widgets/loadsignals/resources/page3.html b/tests/auto/widgets/loadsignals/resources/page3.html deleted file mode 100644 index d38ca31f0..000000000 --- a/tests/auto/widgets/loadsignals/resources/page3.html +++ /dev/null @@ -1,20 +0,0 @@ -<html> - <head> - <title>page3</title> - </head> - <script> - setTimeout(function(){ - document.getElementById('anchorLink').click(); - },500); - </script> - <style> - .fardown { - position: absolute; - top: 2500px; - } - </style> - <body> - <div><a id="anchorLink" href="#anchor">page3</a></div> - <div class="fardown" id="anchor">page3 anchor</div> - </body> -</html> diff --git a/tests/auto/widgets/loadsignals/resources/page4.html b/tests/auto/widgets/loadsignals/resources/page4.html deleted file mode 100644 index 61976b4fb..000000000 --- a/tests/auto/widgets/loadsignals/resources/page4.html +++ /dev/null @@ -1,8 +0,0 @@ -<html> - <head> - <title>page4</title> - </head> - <body onload="document.getElementById('downloadLink').focus();"> - <a id="downloadLink" href="downloadable.tar.gz">download</a> - </body> -</html> diff --git a/tests/auto/widgets/loadsignals/tst_loadsignals.cpp b/tests/auto/widgets/loadsignals/tst_loadsignals.cpp index 20e5fbf0d..8462f0559 100644 --- a/tests/auto/widgets/loadsignals/tst_loadsignals.cpp +++ b/tests/auto/widgets/loadsignals/tst_loadsignals.cpp @@ -28,6 +28,7 @@ #include <QtTest/QtTest> +#include "httpserver.h" #include "../util.h" #include "qdebug.h" #include "qwebenginepage.h" @@ -35,63 +36,108 @@ #include "qwebenginesettings.h" #include "qwebengineview.h" +enum { LoadStarted, LoadSucceeded, LoadFailed }; +static const QList<int> SignalsOrderOnce({ LoadStarted, LoadSucceeded}); +static const QList<int> SignalsOrderTwice({ LoadStarted, LoadSucceeded, LoadStarted, LoadSucceeded }); +static const QList<int> SignalsOrderOnceFailure({ LoadStarted, LoadFailed }); +static const QList<int> SignalsOrderTwiceWithFailure({ LoadStarted, LoadSucceeded, LoadStarted, LoadFailed }); + +class TestPage : public QWebEnginePage +{ +public: + QSet<QUrl> blacklist; + int navigationRequestCount = 0; + QList<int> signalsOrder; + QList<int> loadProgress; + + explicit TestPage(QObject *parent = nullptr) : TestPage(nullptr, parent) { } + TestPage(QWebEngineProfile *profile, QObject *parent = nullptr) : QWebEnginePage(profile, parent) { + connect(this, &QWebEnginePage::loadStarted, [this] () { signalsOrder.append(LoadStarted); }); + connect(this, &QWebEnginePage::loadProgress, [this] (int p) { loadProgress.append(p); }); + connect(this, &QWebEnginePage::loadFinished, [this] (bool r) { signalsOrder.append(r ? LoadSucceeded : LoadFailed); }); + } + + void reset() + { + blacklist.clear(); + navigationRequestCount = 0; + signalsOrder.clear(); + loadProgress.clear(); + } + +protected: + bool acceptNavigationRequest(const QUrl &url, NavigationType, bool) override + { + ++navigationRequestCount; + return !blacklist.contains(url); + } +}; + class tst_LoadSignals : public QObject { Q_OBJECT -public: - tst_LoadSignals(); - virtual ~tst_LoadSignals(); - public Q_SLOTS: void initTestCase(); void init(); - void cleanup(); private Q_SLOTS: void monotonicity(); void loadStartedAndFinishedCount_data(); void loadStartedAndFinishedCount(); - void secondLoadForError_WhenErrorPageEnabled_data(); - void secondLoadForError_WhenErrorPageEnabled(); + void loadStartedAndFinishedCountClick_data(); + void loadStartedAndFinishedCountClick(); + void rejectNavigationRequest_data(); + void rejectNavigationRequest(); void loadAfterInPageNavigation_qtbug66869(); - void fileDownloadDoesNotTriggerLoadSignals_qtbug66661(); + void fileDownload(); + void numberOfStartedAndFinishedSignalsIsSame_data(); + void numberOfStartedAndFinishedSignalsIsSame(); + void loadFinishedAfterNotFoundError_data(); + void loadFinishedAfterNotFoundError(); + void errorPageTriggered_data(); + void errorPageTriggered(); private: - QWebEngineView* view; - QScopedPointer<QSignalSpy> loadStartedSpy; - QScopedPointer<QSignalSpy> loadProgressSpy; - QScopedPointer<QSignalSpy> loadFinishedSpy; + void clickLink(QPoint linkPos); + + QWebEngineProfile profile; + TestPage page{&profile}; + QWebEngineView view; + QSignalSpy loadStartedSpy{&page, &QWebEnginePage::loadStarted}; + QSignalSpy loadFinishedSpy{&page, &QWebEnginePage::loadFinished}; + void resetSpies() { + loadStartedSpy.clear(); + loadFinishedSpy.clear(); + } }; -tst_LoadSignals::tst_LoadSignals() -{ -} - -tst_LoadSignals::~tst_LoadSignals() -{ -} - void tst_LoadSignals::initTestCase() { + view.setPage(&page); + view.resize(640, 480); + view.show(); + QVERIFY(QTest::qWaitForWindowExposed(&view)); } void tst_LoadSignals::init() { - view = new QWebEngineView(); - view->resize(1024,768); - view->show(); - loadStartedSpy.reset(new QSignalSpy(view->page(), &QWebEnginePage::loadStarted)); - loadProgressSpy.reset(new QSignalSpy(view->page(), &QWebEnginePage::loadProgress)); - loadFinishedSpy.reset(new QSignalSpy(view->page(), &QWebEnginePage::loadFinished)); + // Reset content + if (!view.url().isEmpty()) { + loadFinishedSpy.clear(); + view.load(QUrl("about:blank")); + QTRY_COMPARE(loadFinishedSpy.count(), 1); + } + resetSpies(); + page.reset(); } -void tst_LoadSignals::cleanup() +void tst_LoadSignals::clickLink(QPoint linkPos) { - loadFinishedSpy.reset(); - loadProgressSpy.reset(); - loadStartedSpy.reset(); - delete view; + // Simulate left-clicking on link. + QTRY_VERIFY(view.focusProxy()); + QWidget *renderWidget = view.focusProxy(); + QTest::mouseClick(renderWidget, Qt::LeftButton, {}, linkPos); } /** @@ -100,94 +146,159 @@ void tst_LoadSignals::cleanup() void tst_LoadSignals::loadStartedAndFinishedCount_data() { QTest::addColumn<QUrl>("url"); - QTest::addColumn<int>("expectedLoadCount"); - QTest::newRow("Normal") << QUrl("qrc:///resources/page1.html") << 1; - QTest::newRow("WithAnchor") << QUrl("qrc:///resources/page2.html#anchor") << 1; - - // In this case, we get an unexpected additional loadStarted, but no corresponding - // loadFinished, so expectedLoadCount=2 would also not work. See also QTBUG-65223 - QTest::newRow("WithAnchorClickedFromJS") << QUrl("qrc:///resources/page3.html") << 1; + QTest::addColumn<QList<int>>("expectedSignals"); + QTest::newRow("Simple") << QUrl("qrc:///resources/page1.html") << SignalsOrderOnce; + QTest::newRow("SimpleWithAnchor") << QUrl("qrc:///resources/page2.html#anchor") << SignalsOrderOnce; + QTest::newRow("SamePageImmediate") << QUrl("qrc:///resources/page5.html") << SignalsOrderOnce; + QTest::newRow("SamePageDeferred") << QUrl("qrc:///resources/page3.html") << SignalsOrderOnce; + QTest::newRow("OtherPageImmediate") << QUrl("qrc:///resources/page6.html") << SignalsOrderOnce; + QTest::newRow("OtherPageDeferred") << QUrl("qrc:///resources/page7.html") << SignalsOrderTwice; + QTest::newRow("SamePageImmediateJS") << QUrl("qrc:///resources/page8.html") << SignalsOrderOnce; } void tst_LoadSignals::loadStartedAndFinishedCount() { QFETCH(QUrl, url); - QFETCH(int, expectedLoadCount); + QFETCH(QList<int>, expectedSignals); - view->load(url); - QTRY_COMPARE(loadFinishedSpy->size(), expectedLoadCount); - bool loadSucceeded = (*loadFinishedSpy)[0][0].toBool(); - QVERIFY(loadSucceeded); + view.load(url); - // Wait for 10 seconds (abort waiting if another loadStarted or loadFinished occurs) - QTRY_LOOP_IMPL((loadStartedSpy->size() != expectedLoadCount) - || (loadFinishedSpy->size() != expectedLoadCount), 10000, 100); + int expectedLoadCount = expectedSignals.size() / 2; + QTRY_COMPARE(loadStartedSpy.size(), expectedLoadCount); + QTRY_COMPARE(loadFinishedSpy.size(), expectedLoadCount); - // No further loadStarted should have occurred within this time - QCOMPARE(loadStartedSpy->size(), expectedLoadCount); - QCOMPARE(loadFinishedSpy->size(), expectedLoadCount); + // verify no more signals is emitted by waiting for another loadStarted or loadFinished + QTRY_LOOP_IMPL(loadStartedSpy.size() != expectedLoadCount || loadFinishedSpy.size() != expectedLoadCount, 1000, 100); + + // No further signals should have occurred within this time and expected number of signals is preserved + QCOMPARE(loadStartedSpy.size(), expectedLoadCount); + QCOMPARE(loadFinishedSpy.size(), expectedLoadCount); + QCOMPARE(page.signalsOrder, expectedSignals); } /** - * Test monotonicity of loadProgress signals - */ -void tst_LoadSignals::monotonicity() + * Load a URL, then simulate a click to load a different URL. + */ +void tst_LoadSignals::loadStartedAndFinishedCountClick_data() { - view->load(QUrl("qrc:///resources/page1.html")); - QTRY_COMPARE(loadFinishedSpy->size(), 1); - bool loadSucceeded = (*loadFinishedSpy)[0][0].toBool(); - QVERIFY(loadSucceeded); - - // first loadProgress should have 0% progress - QCOMPARE(loadProgressSpy->first()[0].toInt(), 0); + QTest::addColumn<QUrl>("url"); + QTest::addColumn<int>("numberOfSignals"); + QTest::newRow("SamePage") << QUrl("qrc:///resources/page2.html") << 0; // in-page navigation to anchor shouldn't emit anything + QTest::newRow("OtherPage") << QUrl("qrc:///resources/page1.html") << 1; +} - // every loadProgress should have at least as much progress as the one before - int progress = 0; - for (auto item : *loadProgressSpy) { - QVERIFY(item[0].toInt() >= progress); - progress = item[0].toInt(); +void tst_LoadSignals::loadStartedAndFinishedCountClick() +{ + QFETCH(QUrl, url); + QFETCH(int, numberOfSignals); + + view.load(url); + QTRY_COMPARE(loadStartedSpy.size(), 1); + QTRY_COMPARE(loadFinishedSpy.size(), 1); + QVERIFY(loadFinishedSpy[0][0].toBool()); + resetSpies(); + + clickLink(QPoint(10, 10)); + if (numberOfSignals > 0) { + QTRY_COMPARE(loadStartedSpy.size(), numberOfSignals); + QTRY_COMPARE(loadFinishedSpy.size(), numberOfSignals); + QVERIFY(loadFinishedSpy[0][0].toBool()); } - // last loadProgress should have 100% progress - QCOMPARE(loadProgressSpy->last()[0].toInt(), 100); + // verify no more signals is emitted by waiting for another loadStarted or loadFinished + QTRY_LOOP_IMPL(loadStartedSpy.size() != numberOfSignals || loadFinishedSpy.size() != numberOfSignals, 1000, 100); + + // No further loadStarted should have occurred within this time + QCOMPARE(loadStartedSpy.size(), numberOfSignals); + QCOMPARE(loadFinishedSpy.size(), numberOfSignals); + QCOMPARE(page.signalsOrder, numberOfSignals > 0 ? SignalsOrderTwice : SignalsOrderOnce); } -/** - * Test that we get a second loadStarted and loadFinished signal - * for error-pages (unless error-pages are disabled) - */ -void tst_LoadSignals::secondLoadForError_WhenErrorPageEnabled_data() +void tst_LoadSignals::rejectNavigationRequest_data() { - QTest::addColumn<bool>("enabled"); - // in this case, we get no second loadStarted and loadFinished, although we had - // agreed on making the navigation to an error page an individual load - QTest::newRow("ErrorPageEnabled") << true; - QTest::newRow("ErrorPageDisabled") << false; + QTest::addColumn<QUrl>("initialUrl"); + QTest::addColumn<QUrl>("rejectedUrl"); + QTest::addColumn<int>("expectedNavigations"); + QTest::addColumn<QList<int>>("expectedSignals"); + QTest::newRow("Simple") + << QUrl("qrc:///resources/page1.html") + << QUrl("qrc:///resources/page1.html") + << 1 << SignalsOrderOnceFailure; + QTest::newRow("SamePageImmediate") + << QUrl("qrc:///resources/page5.html") + << QUrl("qrc:///resources/page5.html#anchor") + << 1 << SignalsOrderOnce; + QTest::newRow("SamePageDeferred") + << QUrl("qrc:///resources/page3.html") + << QUrl("qrc:///resources/page3.html#anchor") + << 1 << SignalsOrderOnce; + QTest::newRow("OtherPageImmediate") + << QUrl("qrc:///resources/page6.html") + << QUrl("qrc:///resources/page2.html#anchor") + << 2 << SignalsOrderOnceFailure; + QTest::newRow("OtherPageDeferred") + << QUrl("qrc:///resources/page7.html") + << QUrl("qrc:///resources/page2.html#anchor") + << 2 << SignalsOrderTwiceWithFailure; } -void tst_LoadSignals::secondLoadForError_WhenErrorPageEnabled() +/** + * Returning false from acceptNavigationRequest means that the load + * fails, not that the load never starts. + * + * See QTBUG-75185. + */ +void tst_LoadSignals::rejectNavigationRequest() { - QFETCH(bool, enabled); - view->settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, enabled); - int expectedLoadCount = (enabled ? 2 : 1); - - // RFC 2606 guarantees that this will never become a valid domain - view->load(QUrl("http://nonexistent.invalid")); - QTRY_COMPARE_WITH_TIMEOUT(loadFinishedSpy->size(), expectedLoadCount, 10000); - bool loadSucceeded = (*loadFinishedSpy)[0][0].toBool(); - QVERIFY(!loadSucceeded); - if (enabled) { - bool errorPageLoadSucceeded = (*loadFinishedSpy)[1][0].toBool(); - QVERIFY(errorPageLoadSucceeded); - } + QFETCH(QUrl, initialUrl); + QFETCH(QUrl, rejectedUrl); + QFETCH(int, expectedNavigations); + QFETCH(QList<int>, expectedSignals); - // Wait for 10 seconds (abort waiting if another loadStarted or loadFinished occurs) - QTRY_LOOP_IMPL((loadStartedSpy->size() != expectedLoadCount) - || (loadFinishedSpy->size() != expectedLoadCount), 10000, 100); + page.blacklist.insert(rejectedUrl); + page.load(initialUrl); + QTRY_COMPARE(page.navigationRequestCount, expectedNavigations); + int expectedLoadCount = expectedSignals.size() / 2; + QTRY_COMPARE(loadFinishedSpy.size(), expectedLoadCount); + QCOMPARE(page.signalsOrder, expectedSignals); + + // verify no more signals is emitted by waiting for another loadStarted or loadFinished + QTRY_LOOP_IMPL(loadStartedSpy.size() != expectedLoadCount || loadFinishedSpy.size() != expectedLoadCount, 1000, 100); // No further loadStarted should have occurred within this time - QCOMPARE(loadStartedSpy->size(), expectedLoadCount); - QCOMPARE(loadFinishedSpy->size(), expectedLoadCount); + QCOMPARE(loadStartedSpy.size(), expectedLoadCount); + QCOMPARE(loadFinishedSpy.size(), expectedLoadCount); +} + +/** + * Test monotonicity of loadProgress signals + */ +void tst_LoadSignals::monotonicity() +{ + HttpServer server; + server.setResourceDirs({ TESTS_SHARED_DATA_DIR }); + connect(&server, &HttpServer::newRequest, [] (HttpReqRep *) { + QTest::qWait(250); // just add delay to trigger some progress for every sub resource + }); + QVERIFY(server.start()); + + view.load(server.url("/loadprogress/main.html")); + QTRY_COMPARE(loadFinishedSpy.size(), 1); + QVERIFY(loadFinishedSpy[0][0].toBool()); + + QVERIFY(page.loadProgress.size() >= 3); + // first loadProgress should have 0% progress + QCOMPARE(page.loadProgress.first(), 0); + + // every loadProgress should have more progress than the one before + int progress = -1; + for (int p : page.loadProgress) { + QVERIFY(progress < p); + progress = p; + } + + // last loadProgress should have 100% progress + QCOMPARE(page.loadProgress.last(), 100); } /** @@ -196,70 +307,194 @@ void tst_LoadSignals::secondLoadForError_WhenErrorPageEnabled() */ void tst_LoadSignals::loadAfterInPageNavigation_qtbug66869() { - view->load(QUrl("qrc:///resources/page3.html")); - QTRY_COMPARE(loadFinishedSpy->size(), 1); - bool loadSucceeded = (*loadFinishedSpy)[0][0].toBool(); - QVERIFY(loadSucceeded); + view.load(QUrl("qrc:///resources/page3.html")); + QTRY_COMPARE(loadFinishedSpy.size(), 1); + QVERIFY(loadFinishedSpy[0][0].toBool()); // page3 does an in-page navigation after 500ms - QTest::qWait(2000); - loadFinishedSpy->clear(); - loadProgressSpy->clear(); - loadStartedSpy->clear(); + QTRY_COMPARE(view.url(), QUrl("qrc:///resources/page3.html#anchor")); // second load - view->load(QUrl("qrc:///resources/page1.html")); - QTRY_COMPARE(loadFinishedSpy->size(), 1); - loadSucceeded = (*loadFinishedSpy)[0][0].toBool(); - QVERIFY(loadSucceeded); + view.load(QUrl("qrc:///resources/page1.html")); + QTRY_COMPARE(loadFinishedSpy.size(), 2); + QVERIFY(loadFinishedSpy[0][0].toBool()); // loadStarted and loadFinished should have been signalled - QCOMPARE(loadStartedSpy->size(), 1); - - // reminder that we still need to solve the core issue - QFAIL("https://codereview.qt-project.org/#/c/222112/ only hides the symptom, the core issue still needs to be solved"); + QCOMPARE(loadStartedSpy.size(), 2); } -/** - * Test that file-downloads don't trigger loadStarted or loadFinished signals. - * See QTBUG-66661 - */ -void tst_LoadSignals::fileDownloadDoesNotTriggerLoadSignals_qtbug66661() +void tst_LoadSignals::fileDownload() { - view->load(QUrl("qrc:///resources/page4.html")); - QTRY_COMPARE(loadFinishedSpy->size(), 1); - bool loadSucceeded = (*loadFinishedSpy)[0][0].toBool(); - QVERIFY(loadSucceeded); + view.load(QUrl("qrc:///resources/page4.html")); + QTRY_COMPARE(loadFinishedSpy.size(), 1); + QVERIFY(loadFinishedSpy[0][0].toBool()); // allow the download QTemporaryDir tempDir; + QVERIFY(tempDir.isValid()); QWebEngineDownloadItem::DownloadState downloadState = QWebEngineDownloadItem::DownloadRequested; - connect(view->page()->profile(), &QWebEngineProfile::downloadRequested, - [&downloadState, &tempDir](QWebEngineDownloadItem* item){ - connect(item, &QWebEngineDownloadItem::stateChanged, [&downloadState](QWebEngineDownloadItem::DownloadState newState){ - downloadState = newState; - }); - item->setDownloadDirectory(tempDir.filePath(QFileInfo(item->path()).path())); - item->setDownloadFileName(QFileInfo(item->path()).fileName()); - item->accept(); - }); + ScopedConnection sc1 = + connect(&profile, &QWebEngineProfile::downloadRequested, + [&downloadState, &tempDir](QWebEngineDownloadItem *item) { + connect(item, &QWebEngineDownloadItem::stateChanged, + [&downloadState](QWebEngineDownloadItem::DownloadState newState) { + downloadState = newState; + }); + item->setDownloadDirectory(tempDir.path()); + item->accept(); + }); // trigger the download link that becomes focused on page4 - QTest::qWait(1000); - QTest::sendKeyEvent(QTest::Press, view->focusProxy(), Qt::Key_Return, QString("\r"), Qt::NoModifier); - QTest::sendKeyEvent(QTest::Release, view->focusProxy(), Qt::Key_Return, QString("\r"), Qt::NoModifier); - - // Wait for 10 seconds (abort waiting if another loadStarted or loadFinished occurs) - QTRY_LOOP_IMPL((loadStartedSpy->size() != 1) - || (loadFinishedSpy->size() != 1), 10000, 100); + QTest::sendKeyEvent(QTest::Press, view.focusProxy(), Qt::Key_Return, QString("\r"), Qt::NoModifier); + QTest::sendKeyEvent(QTest::Release, view.focusProxy(), Qt::Key_Return, QString("\r"), Qt::NoModifier); // Download must have occurred QTRY_COMPARE(downloadState, QWebEngineDownloadItem::DownloadCompleted); + QTRY_COMPARE(loadFinishedSpy.size() + loadStartedSpy.size(), 4); - // No further loadStarted should have occurred within this time - QCOMPARE(loadStartedSpy->size(), 1); - QCOMPARE(loadFinishedSpy->size(), 1); + // verify no more signals is emitted by waiting for another loadStarted or loadFinished + QTRY_LOOP_IMPL(loadStartedSpy.size() != 2 || loadFinishedSpy.size() != 2, 1000, 100); + + QCOMPARE(page.signalsOrder, SignalsOrderTwiceWithFailure); } +void tst_LoadSignals::numberOfStartedAndFinishedSignalsIsSame_data() +{ + QTest::addColumn<bool>("imageFromServer"); + QTest::addColumn<QString>("imageResourceUrl"); + // triggers these calls in delegate internally: + // just two ordered triples DidStartNavigation/DidFinishNavigation/DidFinishLoad + QTest::newRow("no_image_resource") << false << ""; + // out of order: DidStartNavigation/DidFinishNavigation/DidStartNavigation/DidFailLoad/DidFinishNavigation/DidFinishLoad + QTest::newRow("with_invalid_image") << false << "https://non.existent.locahost/image.png"; + // out of order: DidStartNavigation/DidFinishNavigation/DidStartNavigation/DidFinishLoad/DidFinishNavigation/DidFinishLoad + QTest::newRow("with_server_image") << true << ""; +} + +void tst_LoadSignals::numberOfStartedAndFinishedSignalsIsSame() +{ + QFETCH(bool, imageFromServer); + QFETCH(QString, imageResourceUrl); + + HttpServer server; + server.setResourceDirs({ TESTS_SOURCE_DIR "/qwebengineprofile/resources" }); + QVERIFY(server.start()); + + QUrl serverImage = server.url("/hedgehog.png"); + QString imageUrl(!imageFromServer && imageResourceUrl.isEmpty() + ? "" : (imageFromServer ? serverImage.toEncoded() : imageResourceUrl)); + + auto html = "<html><head><link rel='icon' href='data:,'></head><body>" + "%1" "<form method='GET' name='hiddenform' action='qrc:///resources/page1.html' />" + "<script language='javascript'>document.forms[0].submit();</script>" + "</body></html>"; + view.page()->setHtml(QString(html).arg(imageUrl.isEmpty() ? "" : "<img src='" + imageUrl + "'>")); + QTRY_COMPARE(loadFinishedSpy.size(), 1); + + resetSpies(); + QTRY_LOOP_IMPL(loadStartedSpy.size() || loadFinishedSpy.size(), 1000, 100); + QCOMPARE(page.signalsOrder, SignalsOrderOnce); +} + +void tst_LoadSignals::loadFinishedAfterNotFoundError_data() +{ + QTest::addColumn<bool>("rfcInvalid"); + QTest::addColumn<bool>("withServer"); + QTest::addRow("rfc_invalid") << true << false; + QTest::addRow("non_existent") << false << false; + QTest::addRow("server_404") << false << true; +} + +void tst_LoadSignals::loadFinishedAfterNotFoundError() +{ + QFETCH(bool, withServer); + QFETCH(bool, rfcInvalid); + + QScopedPointer<HttpServer> server; + if (withServer) { + server.reset(new HttpServer); + QVERIFY(server->start()); + } + view.settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, false); + auto url = server + ? server->url("/not-found-page.html") + : QUrl(rfcInvalid ? "http://some.invalid" : "http://non.existent/url"); + view.load(url); + QTRY_COMPARE_WITH_TIMEOUT(loadFinishedSpy.count(), 1, 20000); + QVERIFY(!loadFinishedSpy.at(0).at(0).toBool()); + QCOMPARE(toPlainTextSync(view.page()), QString()); + QCOMPARE(loadFinishedSpy.count(), 1); + QCOMPARE(loadStartedSpy.count(), 1); + QVERIFY(std::is_sorted(page.loadProgress.begin(), page.loadProgress.end())); + page.loadProgress.clear(); + + view.settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, true); + url = server + ? server->url("/another-missing-one.html") + : QUrl(rfcInvalid ? "http://some.other.invalid" : "http://another.non.existent/url"); + view.load(url); + QTRY_COMPARE_WITH_TIMEOUT(loadFinishedSpy.count(), 2, 20000); + QVERIFY(!loadFinishedSpy.at(1).at(0).toBool()); + QCOMPARE(loadStartedSpy.count(), 2); + + QEXPECT_FAIL("", "No more loads (like separate load for error pages) are expected", Continue); + QTRY_COMPARE_WITH_TIMEOUT(loadFinishedSpy.count(), 3, 1000); + QCOMPARE(loadStartedSpy.count(), 2); + QVERIFY(std::is_sorted(page.loadProgress.begin(), page.loadProgress.end())); +} + +void tst_LoadSignals::errorPageTriggered_data() +{ + QTest::addColumn<QString>("urlPath"); + QTest::addColumn<bool>("loadSucceed"); + QTest::addColumn<bool>("triggersErrorPage"); + QTest::newRow("/content/200") << QStringLiteral("/content/200") << true << false; + QTest::newRow("/empty/200") << QStringLiteral("/content/200") << true << false; + QTest::newRow("/content/404") << QStringLiteral("/content/404") << false << false; + QTest::newRow("/empty/404") << QStringLiteral("/empty/404") << false << true; +} + +void tst_LoadSignals::errorPageTriggered() +{ + HttpServer server; + connect(&server, &HttpServer::newRequest, [] (HttpReqRep *rr) { + QList<QByteArray> parts = rr->requestPath().split('/'); + if (parts.length() != 3) { + // For example, /favicon.ico + rr->sendResponse(404); + return; + } + bool isDocumentEmpty = (parts[1] == "empty"); + int httpStatusCode = parts[2].toInt(); + + rr->setResponseHeader(QByteArrayLiteral("content-type"), QByteArrayLiteral("text/html")); + if (!isDocumentEmpty) { + rr->setResponseBody(QByteArrayLiteral("<html></html>")); + } + rr->sendResponse(httpStatusCode); + }); + QVERIFY(server.start()); + + QFETCH(QString, urlPath); + QFETCH(bool, loadSucceed); + QFETCH(bool, triggersErrorPage); + + view.settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, true); + view.load(server.url(urlPath)); + QTRY_COMPARE(loadFinishedSpy.size(), 1); + QCOMPARE(loadFinishedSpy[0][0].toBool(), loadSucceed); + if (triggersErrorPage) + QVERIFY(toPlainTextSync(view.page()).contains("HTTP ERROR 404")); + else + QVERIFY(toPlainTextSync(view.page()).isEmpty()); + loadFinishedSpy.clear(); + + view.settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, false); + view.load(server.url(urlPath)); + QTRY_COMPARE(loadFinishedSpy.size(), 1); + QCOMPARE(loadFinishedSpy[0][0].toBool(), loadSucceed); + QVERIFY(toPlainTextSync(view.page()).isEmpty()); + loadFinishedSpy.clear(); +} QTEST_MAIN(tst_LoadSignals) #include "tst_loadsignals.moc" diff --git a/tests/auto/widgets/loadsignals/tst_loadsignals.qrc b/tests/auto/widgets/loadsignals/tst_loadsignals.qrc index 316deecb8..b4ee36676 100644 --- a/tests/auto/widgets/loadsignals/tst_loadsignals.qrc +++ b/tests/auto/widgets/loadsignals/tst_loadsignals.qrc @@ -1,9 +1,13 @@ <RCC> - <qresource prefix="/"> - <file>resources/page1.html</file> - <file>resources/page2.html</file> - <file>resources/page3.html</file> - <file>resources/page4.html</file> - <file>resources/downloadable.tar.gz</file> + <qresource prefix="/resources"> + <file alias="page1.html">../../shared/data/loadprogress/page1.html</file> + <file alias="page2.html">../../shared/data/loadprogress/page2.html</file> + <file alias="page3.html">../../shared/data/loadprogress/page3.html</file> + <file alias="page4.html">../../shared/data/loadprogress/page4.html</file> + <file alias="page5.html">../../shared/data/loadprogress/page5.html</file> + <file alias="page6.html">../../shared/data/loadprogress/page6.html</file> + <file alias="page7.html">../../shared/data/loadprogress/page7.html</file> + <file alias="page8.html">../../shared/data/loadprogress/page8.html</file> + <file alias="downloadable.tar.gz">../../shared/data/loadprogress/downloadable.tar.gz</file> </qresource> </RCC> diff --git a/tests/auto/widgets/origins/origins.pro b/tests/auto/widgets/origins/origins.pro index 7498354de..8b2fca2e4 100644 --- a/tests/auto/widgets/origins/origins.pro +++ b/tests/auto/widgets/origins/origins.pro @@ -1,4 +1,5 @@ include(../tests.pri) +include(../../shared/http.pri) CONFIG += c++14 qtConfig(webengine-webchannel):qtHaveModule(websockets) { QT += websockets diff --git a/tests/auto/widgets/origins/tst_origins.cpp b/tests/auto/widgets/origins/tst_origins.cpp index c63f4d690..e7d71d7fe 100644 --- a/tests/auto/widgets/origins/tst_origins.cpp +++ b/tests/auto/widgets/origins/tst_origins.cpp @@ -27,6 +27,7 @@ ****************************************************************************/ #include "../util.h" +#include "httpserver.h" #include <QtCore/qfile.h> #include <QtTest/QtTest> @@ -215,8 +216,11 @@ private Q_SLOTS: void jsUrlOrigin(); void subdirWithAccess(); void subdirWithoutAccess(); + void fileAccessRemoteUrl_data(); + void fileAccessRemoteUrl(); void mixedSchemes(); void mixedSchemesWithCsp(); + void mixedXHR_data(); void mixedXHR(); #if defined(WEBSOCKETS) void webSocket(); @@ -481,6 +485,8 @@ void tst_Origins::subdirWithoutAccess() { ScopedAttribute sa(m_page->settings(), QWebEngineSettings::LocalContentCanAccessFileUrls, false); + QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Uncaught SecurityError"))); + QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Uncaught SecurityError"))); QVERIFY(verifyLoad(QSL("file:" THIS_DIR "resources/subdir/index.html"))); QCOMPARE(eval(QSL("msg[0]")), QVariant()); QCOMPARE(eval(QSL("msg[1]")), QVariant()); @@ -494,6 +500,31 @@ void tst_Origins::subdirWithoutAccess() QCOMPARE(eval(QSL("msg[1]")), QVariant(QSL("world"))); } +void tst_Origins::fileAccessRemoteUrl_data() +{ + QTest::addColumn<bool>("EnableAccess"); + QTest::addRow("enabled") << true; + QTest::addRow("disabled") << false; +} + +void tst_Origins::fileAccessRemoteUrl() +{ + QFETCH(bool, EnableAccess); + + HttpServer server; + server.setResourceDirs({ THIS_DIR "resources" }); + QVERIFY(server.start()); + + ScopedAttribute sa(m_page->settings(), QWebEngineSettings::LocalContentCanAccessRemoteUrls, EnableAccess); + if (!EnableAccess) + QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("blocked by CORS policy"))); + + QVERIFY(verifyLoad(QSL("file:" THIS_DIR "resources/mixedXHR.html"))); + + eval("sendXHR('" + server.url("/mixedXHR.txt").toString() + "')"); + QTRY_COMPARE(eval("result"), (EnableAccess ? QString("ok") : QString("error"))); +} + // Load the main page over one scheme with an iframe over another scheme. // // For file and qrc schemes, the iframe should load but it should not be @@ -507,22 +538,28 @@ void tst_Origins::mixedSchemes() QVERIFY(verifyLoad(QSL("file:" THIS_DIR "resources/mixedSchemes.html"))); eval(QSL("setIFrameUrl('file:" THIS_DIR "resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadAndAccess"))); + QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Uncaught SecurityError"))); eval(QSL("setIFrameUrl('qrc:/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); + QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Uncaught SecurityError"))); eval(QSL("setIFrameUrl('tst:/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); QVERIFY(verifyLoad(QSL("qrc:/resources/mixedSchemes.html"))); + QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Uncaught SecurityError"))); eval(QSL("setIFrameUrl('file:" THIS_DIR "resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); eval(QSL("setIFrameUrl('qrc:/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadAndAccess"))); + QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Uncaught SecurityError"))); eval(QSL("setIFrameUrl('tst:/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); QVERIFY(verifyLoad(QSL("tst:/resources/mixedSchemes.html"))); + QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Not allowed to load local resource"))); eval(QSL("setIFrameUrl('file:" THIS_DIR "resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("cannotLoad"))); + QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Uncaught SecurityError"))); eval(QSL("setIFrameUrl('qrc:/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); eval(QSL("setIFrameUrl('tst:/resources/mixedSchemes_frame.html')")); @@ -531,36 +568,47 @@ void tst_Origins::mixedSchemes() QVERIFY(verifyLoad(QSL("PathSyntax:/resources/mixedSchemes.html"))); eval(QSL("setIFrameUrl('PathSyntax:/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadAndAccess"))); + QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Not allowed to load local resource"))); eval(QSL("setIFrameUrl('PathSyntax-Local:/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("cannotLoad"))); eval(QSL("setIFrameUrl('PathSyntax-LocalAccessAllowed:/resources/mixedSchemes_frame.html')")); + QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Uncaught SecurityError"))); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); + QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Uncaught SecurityError"))); eval(QSL("setIFrameUrl('PathSyntax-NoAccessAllowed:/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); QVERIFY(verifyLoad(QSL("PathSyntax-LocalAccessAllowed:/resources/mixedSchemes.html"))); + QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Uncaught SecurityError"))); eval(QSL("setIFrameUrl('PathSyntax:/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); + QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Uncaught SecurityError"))); eval(QSL("setIFrameUrl('PathSyntax-Local:/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); eval(QSL("setIFrameUrl('PathSyntax-LocalAccessAllowed:/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadAndAccess"))); + QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Uncaught SecurityError"))); eval(QSL("setIFrameUrl('PathSyntax-NoAccessAllowed:/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); QVERIFY(verifyLoad(QSL("PathSyntax-NoAccessAllowed:/resources/mixedSchemes.html"))); + QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Uncaught SecurityError"))); eval(QSL("setIFrameUrl('PathSyntax:/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); + QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Not allowed to load local resource"))); eval(QSL("setIFrameUrl('PathSyntax-Local:/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("cannotLoad"))); + QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Uncaught SecurityError"))); eval(QSL("setIFrameUrl('PathSyntax-LocalAccessAllowed:/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); + QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Uncaught SecurityError"))); eval(QSL("setIFrameUrl('PathSyntax-NoAccessAllowed:/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); QVERIFY(verifyLoad(QSL("HostSyntax://a/resources/mixedSchemes.html"))); eval(QSL("setIFrameUrl('HostSyntax://a/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadAndAccess"))); + QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Uncaught SecurityError"))); eval(QSL("setIFrameUrl('HostSyntax://b/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); } @@ -569,14 +617,17 @@ void tst_Origins::mixedSchemes() void tst_Origins::mixedSchemesWithCsp() { QVERIFY(verifyLoad(QSL("HostSyntax://a/resources/mixedSchemesWithCsp.html"))); + QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("violates the following Content Security Policy"))); eval(QSL("setIFrameUrl('HostSyntax://a/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); + QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("violates the following Content Security Policy"))); eval(QSL("setIFrameUrl('HostSyntax://b/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); QVERIFY(verifyLoad(QSL("HostSyntax-ContentSecurityPolicyIgnored://a/resources/mixedSchemesWithCsp.html"))); eval(QSL("setIFrameUrl('HostSyntax-ContentSecurityPolicyIgnored://a/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadAndAccess"))); + QTest::ignoreMessage(QtSystemMsg, QRegularExpression(QSL("Uncaught SecurityError"))); eval(QSL("setIFrameUrl('HostSyntax-ContentSecurityPolicyIgnored://b/resources/mixedSchemes_frame.html')")); QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); } @@ -587,43 +638,72 @@ void tst_Origins::mixedSchemesWithCsp() // Cross-origin XMLHttpRequests can only be made to CORS-enabled schemes. These // include the builtin schemes http, https, data, and chrome, as well as custom // schemes with the CorsEnabled flag. -void tst_Origins::mixedXHR() +void tst_Origins::mixedXHR_data() { - QVERIFY(verifyLoad(QSL("file:" THIS_DIR "resources/mixedXHR.html"))); - eval(QSL("sendXHR('file:" THIS_DIR "resources/mixedXHR.txt')")); - QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("ok"))); - eval(QSL("sendXHR('qrc:/resources/mixedXHR.txt')")); - QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("error"))); - eval(QSL("sendXHR('tst:/resources/mixedXHR.txt')")); - QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("error"))); - eval(QSL("sendXHR('data:,ok')")); - QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("ok"))); - eval(QSL("sendXHR('cors:/resources/mixedXHR.txt')")); - QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("ok"))); + QTest::addColumn<QString>("url"); + QTest::addColumn<QString>("command"); + QTest::addColumn<QVariant>("result"); + QTest::newRow("file->file") << QString("file:" THIS_DIR "resources/mixedXHR.html") + << QString("sendXHR('file:" THIS_DIR "resources/mixedXHR.txt')") + << QVariant(QString("ok")); + QTest::newRow("file->qrc") << QString("file:" THIS_DIR "resources/mixedXHR.html") + << QString("sendXHR('qrc:/resources/mixedXHR.txt')") + << QVariant(QString("error")); + QTest::newRow("file->tst") << QString("file:" THIS_DIR "resources/mixedXHR.html") + << QString("sendXHR('tst:/resources/mixedXHR.txt')") + << QVariant(QString("error")); + QTest::newRow("file->data") << QString("file:" THIS_DIR "resources/mixedXHR.html") + << QString("sendXHR('data:,ok')") + << QVariant(QString("ok")); + QTest::newRow("file->cors") << QString("file:" THIS_DIR "resources/mixedXHR.html") + << QString("sendXHR('cors:/resources/mixedXHR.txt')") + << QVariant(QString("error")); + + QTest::newRow("qrc->file") << QString("qrc:/resources/mixedXHR.html") + << QString("sendXHR('file:" THIS_DIR "resources/mixedXHR.txt')") + << QVariant(QString("ok")); + QTest::newRow("qrc->qrc") << QString("qrc:/resources/mixedXHR.html") + << QString("sendXHR('qrc:/resources/mixedXHR.txt')") + << QVariant(QString("ok")); + QTest::newRow("qrc->tst") << QString("qrc:/resources/mixedXHR.html") + << QString("sendXHR('tst:/resources/mixedXHR.txt')") + << QVariant(QString("error")); + QTest::newRow("qrc->data") << QString("qrc:/resources/mixedXHR.html") + << QString("sendXHR('data:,ok')") + << QVariant(QString("ok")); + QTest::newRow("qrc->cors") << QString("qrc:/resources/mixedXHR.html") + << QString("sendXHR('cors:/resources/mixedXHR.txt')") + << QVariant(QString("ok")); + + + QTest::newRow("tst->file") << QString("tst:/resources/mixedXHR.html") + << QString("sendXHR('file:" THIS_DIR "resources/mixedXHR.txt')") + << QVariant(QString("error")); + QTest::newRow("tst->qrc") << QString("tst:/resources/mixedXHR.html") + << QString("sendXHR('qrc:/resources/mixedXHR.txt')") + << QVariant(QString("error")); + QTest::newRow("tst->tst") << QString("tst:/resources/mixedXHR.html") + << QString("sendXHR('tst:/resources/mixedXHR.txt')") + << QVariant(QString("ok")); + QTest::newRow("tst->data") << QString("tst:/resources/mixedXHR.html") + << QString("sendXHR('data:,ok')") + << QVariant(QString("ok")); + QTest::newRow("tst->cors") << QString("tst:/resources/mixedXHR.html") + << QString("sendXHR('cors:/resources/mixedXHR.txt')") + << QVariant(QString("ok")); - QVERIFY(verifyLoad(QSL("qrc:/resources/mixedXHR.html"))); - eval(QSL("sendXHR('file:" THIS_DIR "resources/mixedXHR.txt')")); - QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("ok"))); - eval(QSL("sendXHR('qrc:/resources/mixedXHR.txt')")); - QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("ok"))); - eval(QSL("sendXHR('tst:/resources/mixedXHR.txt')")); - QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("error"))); - eval(QSL("sendXHR('data:,ok')")); - QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("ok"))); - eval(QSL("sendXHR('cors:/resources/mixedXHR.txt')")); - QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("ok"))); +} - QVERIFY(verifyLoad(QSL("tst:/resources/mixedXHR.html"))); - eval(QSL("sendXHR('file:" THIS_DIR "resources/mixedXHR.txt')")); - QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("error"))); - eval(QSL("sendXHR('qrc:/resources/mixedXHR.txt')")); - QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("error"))); - eval(QSL("sendXHR('tst:/resources/mixedXHR.txt')")); - QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("ok"))); - eval(QSL("sendXHR('data:,ok')")); - QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("ok"))); - eval(QSL("sendXHR('cors:/resources/mixedXHR.txt')")); - QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("ok"))); + +void tst_Origins::mixedXHR() +{ + QFETCH(QString, url); + QFETCH(QString, command); + QFETCH(QVariant, result); + + QVERIFY(verifyLoad(url)); + eval(command); + QTRY_COMPARE(eval(QString("result")), result); } #if defined(WEBSOCKETS) diff --git a/tests/auto/widgets/proxy/proxy_server.cpp b/tests/auto/widgets/proxy/proxy_server.cpp index 55f014914..3bf915609 100644 --- a/tests/auto/widgets/proxy/proxy_server.cpp +++ b/tests/auto/widgets/proxy/proxy_server.cpp @@ -42,8 +42,16 @@ void ProxyServer::setCredentials(const QByteArray &user, const QByteArray passwo m_auth.append(QChar(':')); m_auth.append(password); m_auth = m_auth.toBase64(); + m_authenticate = true; } +void ProxyServer::setCookie(const QByteArray &cookie) +{ + m_cookie.append(QByteArrayLiteral("Cookie: ")); + m_cookie.append(cookie); +} + + bool ProxyServer::isListening() { return m_server.isListening(); @@ -75,7 +83,7 @@ void ProxyServer::handleReadReady() if (!m_data.endsWith("\r\n\r\n")) return; - if (!m_data.contains(QByteArrayLiteral("Proxy-Authorization: Basic"))) { + if (m_authenticate && !m_data.contains(QByteArrayLiteral("Proxy-Authorization: Basic"))) { socket->write("HTTP/1.1 407 Proxy Authentication Required\nProxy-Authenticate: " "Basic realm=\"Proxy requires authentication\"\r\n" "content-length: 0\r\n" @@ -83,8 +91,12 @@ void ProxyServer::handleReadReady() return; } - if (m_data.contains(m_auth)) { - emit success(); + if (m_authenticate && m_data.contains(m_auth)) { + emit authenticationSuccess(); + } + + if (m_data.contains(m_cookie)) { + emit cookieMatch(); } m_data.clear(); } diff --git a/tests/auto/widgets/proxy/proxy_server.h b/tests/auto/widgets/proxy/proxy_server.h index cb7c30600..7bc7b100b 100644 --- a/tests/auto/widgets/proxy/proxy_server.h +++ b/tests/auto/widgets/proxy/proxy_server.h @@ -39,6 +39,7 @@ class ProxyServer : public QObject public: explicit ProxyServer(QObject *parent = nullptr); void setCredentials(const QByteArray &user, const QByteArray password); + void setCookie(const QByteArray &cookie); bool isListening(); public slots: @@ -49,11 +50,15 @@ private slots: void handleReadReady(); signals: - void success(); + void authenticationSuccess(); + void cookieMatch(); + private: QByteArray m_data; QTcpServer m_server; QByteArray m_auth; + QByteArray m_cookie; + bool m_authenticate = false; }; #endif // PROXY_SERVER_H diff --git a/tests/auto/widgets/proxy/tst_proxy.cpp b/tests/auto/widgets/proxy/tst_proxy.cpp index 5f5dec016..c3e3c88a4 100644 --- a/tests/auto/widgets/proxy/tst_proxy.cpp +++ b/tests/auto/widgets/proxy/tst_proxy.cpp @@ -32,6 +32,17 @@ #include <QNetworkProxy> #include <QWebEnginePage> #include <QWebEngineView> +#include <QWebEngineUrlRequestInterceptor> + + +struct Interceptor : public QWebEngineUrlRequestInterceptor +{ + Interceptor(const QByteArray cookie):m_cookie(cookie){}; + void interceptRequest(QWebEngineUrlRequestInfo &info) override { + info.setHttpHeader(QByteArray("Cookie"), m_cookie); + }; + QByteArray m_cookie; +}; class tst_Proxy : public QObject { @@ -41,8 +52,10 @@ public: private slots: void proxyAuthentication(); + void forwardCookie(); }; + void tst_Proxy::proxyAuthentication() { QByteArray user(QByteArrayLiteral("test")); @@ -59,11 +72,31 @@ void tst_Proxy::proxyAuthentication() server.run(); QTRY_VERIFY2(server.isListening(), "Could not setup authentication server"); QWebEnginePage page; - QSignalSpy successSpy(&server, &ProxyServer::success); + QSignalSpy successSpy(&server, &ProxyServer::authenticationSuccess); page.load(QUrl("http://www.qt.io")); QTRY_VERIFY2(successSpy.count() > 0, "Could not get authentication token"); } +void tst_Proxy::forwardCookie() +{ + QNetworkProxy proxy; + proxy.setType(QNetworkProxy::HttpProxy); + proxy.setHostName("localhost"); + proxy.setPort(5555); + QNetworkProxy::setApplicationProxy(proxy); + ProxyServer server; + QByteArray cookie("foo=bar; sessionToken=123"); + server.setCookie(cookie); + server.run(); + QTRY_VERIFY2(server.isListening(), "Could not setup proxy server"); + Interceptor interceptor(cookie); + QWebEnginePage page; + page.setUrlRequestInterceptor(&interceptor); + QSignalSpy cookieSpy(&server, &ProxyServer::cookieMatch); + page.load(QUrl("http://www.qt.io")); + QTRY_VERIFY2(cookieSpy.count() > 0, "Could not get cookie"); +} + #include "tst_proxy.moc" QTEST_MAIN(tst_Proxy) diff --git a/tests/auto/widgets/proxypac/proxypac.pri b/tests/auto/widgets/proxypac/proxypac.pri new file mode 100644 index 000000000..b3b2856c8 --- /dev/null +++ b/tests/auto/widgets/proxypac/proxypac.pri @@ -0,0 +1,5 @@ +TEMPLATE = app +CONFIG += testcase +QT += testlib network webenginewidgets webengine +HEADERS += $$PWD/proxyserver.h +SOURCES += $$PWD/proxyserver.cpp $$PWD/tst_proxypac.cpp diff --git a/tests/auto/widgets/proxypac/proxypac.pro b/tests/auto/widgets/proxypac/proxypac.pro index 4dbcd9365..f2a43d41f 100644 --- a/tests/auto/widgets/proxypac/proxypac.pro +++ b/tests/auto/widgets/proxypac/proxypac.pro @@ -1,11 +1,4 @@ -include(../tests.pri) -QT += webengine -HEADERS += proxyserver.h -SOURCES += proxyserver.cpp - -proxy_pac.name = QTWEBENGINE_CHROMIUM_FLAGS -boot2qt:proxy_pac.value = "--single-process --no-sandbox --proxy-pac-url=file://$$PWD/proxy.pac" -else: proxy_pac.value = --proxy-pac-url="file://$$PWD/proxy.pac" - -QT_TOOL_ENV += proxy_pac +TEMPLATE = subdirs +SUBDIRS = proxypac_file proxypac_qrc +CONFIG += ordered diff --git a/tests/auto/widgets/proxypac/proxypac.qrc b/tests/auto/widgets/proxypac/proxypac.qrc new file mode 100644 index 000000000..9047585a0 --- /dev/null +++ b/tests/auto/widgets/proxypac/proxypac.qrc @@ -0,0 +1,7 @@ +<!DOCTYPE RCC> +<RCC version="1.0"> +<qresource profix="/"> + <file>proxy.pac</file> +</qresource> +</RCC> + diff --git a/tests/auto/widgets/proxypac/proxypac_file/proxypac_file.pro b/tests/auto/widgets/proxypac/proxypac_file/proxypac_file.pro new file mode 100644 index 000000000..037123054 --- /dev/null +++ b/tests/auto/widgets/proxypac/proxypac_file/proxypac_file.pro @@ -0,0 +1,9 @@ +include(../proxypac.pri) + +proxy_pac.name = QTWEBENGINE_CHROMIUM_FLAGS +win32:proxy_pac.value = --proxy-pac-url="file:///$$PWD/../proxy.pac" +else:proxy_pac.value = --proxy-pac-url="file://$$PWD/../proxy.pac" +boot2qt:proxy_pac.value = "--single-process --no-sandbox --proxy-pac-url=file://$$PWD/../proxy.pac" + +QT_TOOL_ENV += proxy_pac + diff --git a/tests/auto/widgets/proxypac/proxypac_qrc/proxypac_qrc.pro b/tests/auto/widgets/proxypac/proxypac_qrc/proxypac_qrc.pro new file mode 100644 index 000000000..a5ab64605 --- /dev/null +++ b/tests/auto/widgets/proxypac/proxypac_qrc/proxypac_qrc.pro @@ -0,0 +1,7 @@ +include(../proxypac.pri) + +proxy_pac.name = QTWEBENGINE_CHROMIUM_FLAGS +proxy_pac.value = --proxy-pac-url="qrc:///proxy.pac" +boot2qt:proxy_pac.value = "--single-process --no-sandbox --proxy-pac-url=qrc:///proxy.pac" +QT_TOOL_ENV += proxy_pac +RESOURCES+= $$PWD/../proxypac.qrc diff --git a/tests/auto/widgets/proxypac/tst_proxypac.cpp b/tests/auto/widgets/proxypac/tst_proxypac.cpp index 934e23fde..dabbfb4e5 100644 --- a/tests/auto/widgets/proxypac/tst_proxypac.cpp +++ b/tests/auto/widgets/proxypac/tst_proxypac.cpp @@ -46,7 +46,7 @@ private slots: void tst_ProxyPac::proxypac() { - const QString fromEnv = QString::fromLocal8Bit(qgetenv("QTWEBENGINE_CHROMIUM_FLAGS")); + const QString fromEnv = qEnvironmentVariable("QTWEBENGINE_CHROMIUM_FLAGS"); if (!fromEnv.contains("--proxy-pac-url")) qFatal("--proxy-pac-url argument is not passed."); diff --git a/tests/auto/widgets/qwebenginedownloaditem/tst_qwebenginedownloaditem.cpp b/tests/auto/widgets/qwebenginedownloaditem/tst_qwebenginedownloaditem.cpp index bbcef2226..74082ab8c 100644 --- a/tests/auto/widgets/qwebenginedownloaditem/tst_qwebenginedownloaditem.cpp +++ b/tests/auto/widgets/qwebenginedownloaditem/tst_qwebenginedownloaditem.cpp @@ -26,6 +26,8 @@ ** ****************************************************************************/ +#include "../util.h" + #include <QCoreApplication> #include <QSignalSpy> #include <QStandardPaths> @@ -80,7 +82,10 @@ private Q_SLOTS: #if QT_DEPRECATED_SINCE(5, 14) void downloadPathValidation(); #endif + void downloadToDirectoryWithFileName_data(); void downloadToDirectoryWithFileName(); + void downloadDataUrls_data(); + void downloadDataUrls(); private: void saveLink(QPoint linkPos); @@ -99,14 +104,6 @@ private: QSet<QWebEngineDownloadItem *> m_finishedDownloads; }; -class ScopedConnection { -public: - ScopedConnection(QMetaObject::Connection connection) : m_connection(std::move(connection)) {} - ~ScopedConnection() { QObject::disconnect(m_connection); } -private: - QMetaObject::Connection m_connection; -}; - Q_DECLARE_METATYPE(tst_QWebEngineDownloadItem::UserAction) Q_DECLARE_METATYPE(tst_QWebEngineDownloadItem::FileAction) @@ -307,7 +304,7 @@ void tst_QWebEngineDownloadItem::downloadLink_data() /* fileMimeTypeDeclared */ << QByteArrayLiteral("text/plain") /* fileMimeTypeDetected */ << QByteArrayLiteral("text/plain") /* fileDisposition */ << QByteArrayLiteral("") - /* fileHasReferer */ << false // crbug.com/455987 + /* fileHasReferer */ << true /* fileAction */ << FileIsDownloaded; // ... same with the content disposition header save for the download type. @@ -331,7 +328,7 @@ void tst_QWebEngineDownloadItem::downloadLink_data() /* fileMimeTypeDeclared */ << QByteArrayLiteral("text/plain") /* fileMimeTypeDetected */ << QByteArrayLiteral("text/plain") /* fileDisposition */ << QByteArrayLiteral("attachment") - /* fileHasReferer */ << false // crbug.com/455987 + /* fileHasReferer */ << true /* fileAction */ << FileIsDownloaded; // The file's extension has no effect. @@ -439,8 +436,7 @@ void tst_QWebEngineDownloadItem::downloadLink() rr->setResponseBody(fileContents); rr->sendResponse(); } else { - rr->setResponseStatus(404); - rr->sendResponse(); + rr->sendResponse(404); } }); @@ -459,7 +455,7 @@ void tst_QWebEngineDownloadItem::downloadLink() ScopedConnection sc2 = connect(m_profile, &QWebEngineProfile::downloadRequested, [&](QWebEngineDownloadItem *item) { QCOMPARE(item->state(), QWebEngineDownloadItem::DownloadRequested); QCOMPARE(item->isFinished(), false); - QCOMPARE(item->totalBytes(), -1); + QCOMPARE(item->totalBytes(), fileContents.size()); QCOMPARE(item->receivedBytes(), 0); QCOMPARE(item->interruptReason(), QWebEngineDownloadItem::NoReason); QCOMPARE(item->type(), expectedDownloadType(userAction, fileDisposition)); @@ -560,9 +556,6 @@ void tst_QWebEngineDownloadItem::downloadTwoLinks() rr->setResponseHeader(QByteArrayLiteral("content-disposition"), QByteArrayLiteral("attachment")); rr->setResponseBody(QByteArrayLiteral("file2")); rr->sendResponse(); - } else { - rr->setResponseStatus(404); - rr->sendResponse(); } }); @@ -575,7 +568,7 @@ void tst_QWebEngineDownloadItem::downloadTwoLinks() ScopedConnection sc2 = connect(m_profile, &QWebEngineProfile::downloadRequested, [&](QWebEngineDownloadItem *item) { QCOMPARE(item->state(), QWebEngineDownloadItem::DownloadRequested); QCOMPARE(item->isFinished(), false); - QCOMPARE(item->totalBytes(), -1); + QCOMPARE(item->totalBytes(), 5); // strlen("fileN") QCOMPARE(item->receivedBytes(), 0); QCOMPARE(item->interruptReason(), QWebEngineDownloadItem::NoReason); QCOMPARE(item->savePageFormat(), QWebEngineDownloadItem::UnknownSaveFormat); @@ -643,9 +636,6 @@ void tst_QWebEngineDownloadItem::downloadPage() rr->setResponseHeader(QByteArrayLiteral("content-type"), QByteArrayLiteral("text/html")); rr->setResponseBody(QByteArrayLiteral("<html><body>Hello</body></html>")); rr->sendResponse(); - } else { - rr->setResponseStatus(404); - rr->sendResponse(); } }); @@ -734,9 +724,6 @@ void tst_QWebEngineDownloadItem::downloadViaSetUrl() rr->setResponseHeader(QByteArrayLiteral("content-disposition"), QByteArrayLiteral("attachment")); rr->setResponseBody(QByteArrayLiteral("redacted")); rr->sendResponse(); - } else { - rr->setResponseStatus(404); - rr->sendResponse(); } }); @@ -778,8 +765,7 @@ void tst_QWebEngineDownloadItem::downloadFileNot1() // Trigger file download via download() but don't accept(). ScopedConnection sc1 = connect(m_server, &HttpServer::newRequest, [&](HttpReqRep *rr) { - rr->setResponseStatus(404); - rr->sendResponse(); + rr->sendResponse(404); }); QPointer<QWebEngineDownloadItem> downloadItem; @@ -801,8 +787,7 @@ void tst_QWebEngineDownloadItem::downloadFileNot2() // Trigger file download via download() but call cancel() instead of accept(). ScopedConnection sc1 = connect(m_server, &HttpServer::newRequest, [&](HttpReqRep *rr) { - rr->setResponseStatus(404); - rr->sendResponse(); + rr->sendResponse(404); }); QPointer<QWebEngineDownloadItem> downloadItem; @@ -824,32 +809,30 @@ void tst_QWebEngineDownloadItem::downloadFileNot2() void tst_QWebEngineDownloadItem::downloadDeleted() { QPointer<QWebEngineDownloadItem> downloadItem; - m_server->setExpectError(true); - int downloadCount = 0; - int finishedCount = 0; + int downloadCount = 0, finishedCount = 0; + ScopedConnection sc2 = connect(m_profile, &QWebEngineProfile::downloadRequested, [&](QWebEngineDownloadItem *item) { QVERIFY(item); QCOMPARE(item->state(), QWebEngineDownloadItem::DownloadRequested); downloadItem = item; - connect(downloadItem, &QWebEngineDownloadItem::finished, [&]() { - finishedCount++; - }); + connect(downloadItem, &QWebEngineDownloadItem::finished, [&]() { ++finishedCount; }); + ++downloadCount; + // accept and schedule deletion, and check if it still finishes item->accept(); - downloadCount++; + item->deleteLater(); + QVERIFY(downloadItem); }); m_page->download(m_server->url(QByteArrayLiteral("/file"))); QTRY_COMPARE(downloadCount, 1); - QVERIFY(downloadItem); - QCOMPARE(finishedCount, 0); - downloadItem->deleteLater(); QTRY_COMPARE(finishedCount, 1); + QTRY_VERIFY(!downloadItem); + QCOMPARE(downloadCount, 1); + QCOMPARE(finishedCount, 1); } void tst_QWebEngineDownloadItem::downloadDeletedByProfile() { - m_server->setExpectError(true); - QPointer<QWebEngineProfile> profile(new QWebEngineProfile); profile->setHttpCacheType(QWebEngineProfile::NoCache); profile->settings()->setAttribute(QWebEngineSettings::AutoLoadIconsForPage, false); @@ -907,8 +890,7 @@ void tst_QWebEngineDownloadItem::downloadUniqueFilename() rr->setResponseBody(QByteArrayLiteral("a")); rr->sendResponse(); } else { - rr->setResponseStatus(404); - rr->sendResponse(); + rr->sendResponse(404); } }); @@ -965,8 +947,7 @@ void tst_QWebEngineDownloadItem::downloadUniqueFilenameWithTimestamp() rr->setResponseBody(QByteArrayLiteral("a")); rr->sendResponse(); } else { - rr->setResponseStatus(404); - rr->sendResponse(); + rr->sendResponse(404); } }); @@ -1015,8 +996,10 @@ void tst_QWebEngineDownloadItem::downloadUniqueFilenameWithTimestamp() QRegularExpressionMatch match = fileNameCheck.match(downloadedFilePath); QVERIFY(match.hasMatch()); // ISO 8601 Date and time in UTC - QRegExp timestamp("^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9])([0-5][0-9])([0-5][0-9])([.][0-9]+)?(Z|[+-](?:2[0-3]|[01][0-9])[0-5][0-9])?$"); - QVERIFY(timestamp.exactMatch(match.captured(1))); + QRegularExpression timestamp("^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[" + "12][0-9])T(2[0-3]|[01][0-9])([0-5][0-9])([0-5][0-9])([.][0-9]" + "+)?(Z|[+-](?:2[0-3]|[01][0-9])[0-5][0-9])?$"); + QVERIFY(timestamp.match(match.captured(1)).hasMatch()); QCOMPARE(suggestedFileName, fileName); } } @@ -1059,8 +1042,7 @@ void tst_QWebEngineDownloadItem::downloadToNonExistentDir() rr->setResponseBody(QByteArrayLiteral("a")); rr->sendResponse(); } else { - rr->setResponseStatus(404); - rr->sendResponse(); + rr->sendResponse(404); } }); @@ -1116,8 +1098,7 @@ void tst_QWebEngineDownloadItem::downloadToReadOnlyDir() rr->setResponseBody(QByteArrayLiteral("a")); rr->sendResponse(); } else { - rr->setResponseStatus(404); - rr->sendResponse(); + rr->sendResponse(404); } }); @@ -1171,8 +1152,7 @@ void tst_QWebEngineDownloadItem::downloadPathValidation() rr->setResponseBody(QByteArrayLiteral("a")); rr->sendResponse(); } else { - rr->setResponseStatus(404); - rr->sendResponse(); + rr->sendResponse(404); } }); @@ -1271,8 +1251,17 @@ void tst_QWebEngineDownloadItem::downloadPathValidation() } #endif +void tst_QWebEngineDownloadItem::downloadToDirectoryWithFileName_data() +{ + QTest::addColumn<bool>("setDirectoryFirst"); + + QTest::newRow("setDirectoryFirst") << true; + QTest::newRow("setFileNameFirst") << false; +} + void tst_QWebEngineDownloadItem::downloadToDirectoryWithFileName() { + QFETCH(bool, setDirectoryFirst); QString downloadDirectory; QString downloadFileName; QString downloadedFilePath; @@ -1294,15 +1283,14 @@ void tst_QWebEngineDownloadItem::downloadToDirectoryWithFileName() rr->setResponseBody(QByteArrayLiteral("a")); rr->sendResponse(); } else { - rr->setResponseStatus(404); - rr->sendResponse(); + rr->sendResponse(404); } }); // Set up profile and download handler ScopedConnection sc2 = connect(m_profile, &QWebEngineProfile::downloadRequested, [&](QWebEngineDownloadItem *item) { - if (!downloadDirectory.isEmpty()) { + if (!downloadDirectory.isEmpty() && setDirectoryFirst) { item->setDownloadDirectory(downloadDirectory); QCOMPARE(item->downloadDirectory(), downloadDirectory); } @@ -1312,6 +1300,11 @@ void tst_QWebEngineDownloadItem::downloadToDirectoryWithFileName() QCOMPARE(item->downloadFileName(), downloadFileName); } + if (!downloadDirectory.isEmpty() && !setDirectoryFirst) { + item->setDownloadDirectory(downloadDirectory); + QCOMPARE(item->downloadDirectory(), downloadDirectory); + } + QCOMPARE(item->path(), QDir(item->downloadDirectory()).filePath(item->downloadFileName())); item->accept(); @@ -1401,5 +1394,50 @@ void tst_QWebEngineDownloadItem::downloadToDirectoryWithFileName() QCOMPARE(downloadedSuggestedFileName, fileName); } +void tst_QWebEngineDownloadItem::downloadDataUrls_data() +{ + QTest::addColumn<QByteArray>("htmlData"); + QTest::addColumn<QString>("expectedFileName"); + QTest::newRow("data url without slash") << QByteArrayLiteral("<html><head><meta charset=\"utf-8\"></head><body><a href=\"data:application/gzip;base64,dGVzdA==\">data URL without slash</a><br/></body></html>") << QStringLiteral("qwe_download.gz") ; + QTest::newRow("data url with slash") << QByteArrayLiteral("<html><head><meta charset=\"utf-8\"></head><body><a href=\"data:application/gzip;base64,dGVzcnI/dGVzdA==\">data URL with filename</a><br/></body></html>") << QStringLiteral("qwe_download.gz") ; + QTest::newRow("data url with download tag") << QByteArrayLiteral("<html><head><meta charset=\"utf-8\"></head><body><a href=\"data:application/gzip;base64,dGVzdA/IHRlc3Q=\" download=\"filename.gz\">data URL with filename</a><br/></body></html>") << QStringLiteral("filename.gz") ; + +} + +void tst_QWebEngineDownloadItem::downloadDataUrls() +{ + QFETCH(QByteArray, htmlData); + QFETCH(QString, expectedFileName); + // Set up HTTP server + ScopedConnection sc1 = connect(m_server, &HttpServer::newRequest, [&](HttpReqRep *rr) { + if (rr->requestMethod() == "GET" && rr->requestPath() == "/") { + rr->setResponseHeader(QByteArrayLiteral("content-type"), QByteArrayLiteral("text/html")); + rr->setResponseBody(htmlData); + rr->sendResponse(); + } + }); + + // Set up profile and download handler + QTemporaryDir tmpDir; + QVERIFY(tmpDir.isValid()); + m_profile->setDownloadPath(tmpDir.path()); + + int downloadRequestCount = 0; + ScopedConnection sc2 = connect(m_profile, &QWebEngineProfile::downloadRequested, [&](QWebEngineDownloadItem *item) { + QCOMPARE(item->state(), QWebEngineDownloadItem::DownloadRequested); + QCOMPARE(item->downloadFileName(), expectedFileName); + downloadRequestCount++; + }); + + QSignalSpy loadSpy(m_page, &QWebEnginePage::loadFinished); + m_view->load(m_server->url()); + QTRY_COMPARE(loadSpy.count(), 1); + QCOMPARE(loadSpy.takeFirst().value(0).toBool(), true); + + // Trigger download + simulateUserAction(QPoint(10, 10), UserAction::ClickLink); + QTRY_COMPARE(downloadRequestCount, 1); +} + QTEST_MAIN(tst_QWebEngineDownloadItem) #include "tst_qwebenginedownloaditem.moc" diff --git a/tests/auto/widgets/qwebenginehistory/tst_qwebenginehistory.cpp b/tests/auto/widgets/qwebenginehistory/tst_qwebenginehistory.cpp index bdb486793..72a45379b 100644 --- a/tests/auto/widgets/qwebenginehistory/tst_qwebenginehistory.cpp +++ b/tests/auto/widgets/qwebenginehistory/tst_qwebenginehistory.cpp @@ -320,7 +320,8 @@ void tst_QWebEngineHistory::serialize_2() hist->forward(); QTRY_COMPARE(loadFinishedSpy->count(), 5); hist->forward(); - QTRY_COMPARE(loadFinishedSpy->count(), 6); + // In-page navigation, the last url was the page5.html + QTRY_COMPARE(loadFinishedSpy->count(), 5); QTRY_COMPARE(hist->currentItemIndex(), initialCurrentIndex); } diff --git a/tests/auto/widgets/qwebenginepage/BLACKLIST b/tests/auto/widgets/qwebenginepage/BLACKLIST index 7857ee818..d1425bfd6 100644 --- a/tests/auto/widgets/qwebenginepage/BLACKLIST +++ b/tests/auto/widgets/qwebenginepage/BLACKLIST @@ -3,6 +3,5 @@ osx [mouseMovementProperties] windows - -[fullScreenRequested] -windows +macos # Can't move cursor (QTBUG-76312) +sles-15.4 # QTBUG-111297 diff --git a/tests/auto/widgets/qwebenginepage/resources/redirect.html b/tests/auto/widgets/qwebenginepage/resources/redirect.html new file mode 100644 index 000000000..db06d73a7 --- /dev/null +++ b/tests/auto/widgets/qwebenginepage/resources/redirect.html @@ -0,0 +1,8 @@ +<html> +<body> +<script> +function doRedirect() { location.replace('qrc:///resources/content.html') } +document.addEventListener("DOMContentLoaded", doRedirect) +</script> +</body> +</html> diff --git a/tests/auto/widgets/qwebenginepage/tst_qwebenginepage.cpp b/tests/auto/widgets/qwebenginepage/tst_qwebenginepage.cpp index d8c1a5360..78d0a9862 100644 --- a/tests/auto/widgets/qwebenginepage/tst_qwebenginepage.cpp +++ b/tests/auto/widgets/qwebenginepage/tst_qwebenginepage.cpp @@ -73,6 +73,13 @@ static void removeRecursive(const QString& dirname) QDir().rmdir(dirname); } +struct TestBasePage : QWebEnginePage +{ + explicit TestBasePage(QWebEngineProfile *profile, QObject *parent = nullptr) : QWebEnginePage(profile, parent) { } + explicit TestBasePage(QObject *parent = nullptr) : QWebEnginePage(parent) { } + QSignalSpy loadSpy { this, &QWebEnginePage::loadFinished }; +}; + class tst_QWebEnginePage : public QObject { Q_OBJECT @@ -81,8 +88,6 @@ public: tst_QWebEnginePage(); virtual ~tst_QWebEnginePage(); - bool eventFilter(QObject *watched, QEvent *event); - public Q_SLOTS: void init(); void cleanup(); @@ -95,6 +100,7 @@ private Q_SLOTS: void comboBoxPopupPositionAfterChildMove(); void acceptNavigationRequest(); void acceptNavigationRequestNavigationType(); + void acceptNavigationRequestRelativeToNothing(); void geolocationRequestJS_data(); void geolocationRequestJS(); void loadFinished(); @@ -175,7 +181,6 @@ private Q_SLOTS: void setUrlUsingStateObject(); void setUrlThenLoads_data(); void setUrlThenLoads(); - void loadFinishedAfterNotFoundError(); void loadInSignalHandlers_data(); void loadInSignalHandlers(); void loadFromQrc(); @@ -198,11 +203,13 @@ private Q_SLOTS: void dataURLFragment(); void devTools(); void openLinkInDifferentProfile(); + void openLinkInNewPage_data(); + void openLinkInNewPage(); void triggerActionWithoutMenu(); void dynamicFrame(); - void notificationRequest_data(); - void notificationRequest(); + void notificationPermission_data(); + void notificationPermission(); void sendNotification(); void contentsSize(); @@ -226,14 +233,21 @@ private Q_SLOTS: void customUserAgentInNewTab(); void renderProcessCrashed(); + void renderProcessPid(); + void backgroundColor(); + void audioMuted(); + void closeContents(); + void isSafeRedirect_data(); + void isSafeRedirect(); private: static QPoint elementCenter(QWebEnginePage *page, const QString &id); + static bool isFalseJavaScriptResult(QWebEnginePage *page, const QString &javaScript); + static bool isTrueJavaScriptResult(QWebEnginePage *page, const QString &javaScript); + static bool isEmptyListJavaScriptResult(QWebEnginePage *page, const QString &javaScript); QWebEngineView* m_view; QWebEnginePage* m_page; - QWebEngineView* m_inputFieldsTestView; - int m_inputFieldTestPaintCount; QString tmpDirPath() const { static QString tmpd = QDir::tempPath() + "/tst_qwebenginepage-" @@ -250,16 +264,6 @@ tst_QWebEnginePage::~tst_QWebEnginePage() { } -bool tst_QWebEnginePage::eventFilter(QObject* watched, QEvent* event) -{ - // used on the inputFieldFocus test - if (watched == m_inputFieldsTestView) { - if (event->type() == QEvent::Paint) - m_inputFieldTestPaintCount++; - } - return QObject::eventFilter(watched, event); -} - void tst_QWebEnginePage::init() { m_view = new QWebEngineView(); @@ -522,7 +526,7 @@ void tst_QWebEnginePage::consoleOutput() class TestPage : public QWebEnginePage { Q_OBJECT public: - TestPage(QObject* parent = 0) : QWebEnginePage(parent) + TestPage(QObject *parent = nullptr) : QWebEnginePage(parent) { connect(this, SIGNAL(geometryChangeRequested(QRect)), this, SLOT(slotGeometryChangeRequested(QRect))); } @@ -599,12 +603,73 @@ void tst_QWebEnginePage::acceptNavigationRequestNavigationType() << QWebEnginePage::NavigationTypeReload << QWebEnginePage::NavigationTypeTyped << QWebEnginePage::NavigationTypeRedirect; + + // client side redirect + page.load(QUrl("qrc:///resources/redirect.html")); + QTRY_COMPARE_WITH_TIMEOUT(loadSpy.count(), 7, 20000); + QTRY_COMPARE(page.navigations.count(), 8); + expectedList += { QWebEnginePage::NavigationTypeTyped, QWebEnginePage::NavigationTypeRedirect }; + + // server side redirect + HttpServer server; + server.setResourceDirs({ ":/resources" }); + connect(&server, &HttpServer::newRequest, &server, [&] (HttpReqRep *r) { + if (r->requestMethod() == "GET") { + if (r->requestPath() == "/redirect1.html") { + r->setResponseHeader("Location", server.url("/redirect2.html").toEncoded()); + r->setResponseBody("<html><body>Redirect1</body></html>"); + r->sendResponse(307); // Internal server redirect + } else if (r->requestPath() == "/redirect2.html") { + r->setResponseHeader("Location", server.url("/content.html").toEncoded()); + r->setResponseBody("<html><body>Redirect2</body></html>"); + r->sendResponse(301); // Moved permanently + } + } + }); + QVERIFY(server.start()); + page.load(QUrl(server.url("/redirect1.html"))); + QTRY_COMPARE_WITH_TIMEOUT(loadSpy.count(), 8, 20000); + QTRY_COMPARE(page.navigations.count(), 11); + expectedList += { + QWebEnginePage::NavigationTypeTyped, + QWebEnginePage::NavigationTypeRedirect, + QWebEnginePage::NavigationTypeRedirect + }; + QVERIFY(expectedList.count() == page.navigations.count()); for (int i = 0; i < expectedList.count(); ++i) { QCOMPARE(page.navigations[i].type, expectedList[i]); } } +// Relative url without base url. +// +// See also: QTBUG-48435 +void tst_QWebEnginePage::acceptNavigationRequestRelativeToNothing() +{ + TestPage page; + QSignalSpy loadSpy(&page, SIGNAL(loadFinished(bool))); + + page.setHtml(QString("<html><body><a id='link' href='S0'>limited time offer</a></body></html>"), + /* baseUrl: */ QUrl()); + QTRY_COMPARE_WITH_TIMEOUT(loadSpy.count(), 1, 20000); + page.runJavaScript(QStringLiteral("document.getElementById(\"link\").click()")); + QTRY_COMPARE_WITH_TIMEOUT(loadSpy.count(), 2, 20000); + page.setHtml(QString("<html><body><a id='link' href='S0'>limited time offer</a></body></html>"), + /* baseUrl: */ QString("qrc:/")); + QTRY_COMPARE_WITH_TIMEOUT(loadSpy.count(), 3, 20000); + page.runJavaScript(QStringLiteral("document.getElementById(\"link\").click()")); + QTRY_COMPARE_WITH_TIMEOUT(loadSpy.count(), 4, 20000); + + // The two setHtml and the second click are counted, while the + // first click is ignored due to the empty base url. + QCOMPARE(page.navigations.count(), 3); + QCOMPARE(page.navigations[0].type, QWebEnginePage::NavigationTypeTyped); + QCOMPARE(page.navigations[1].type, QWebEnginePage::NavigationTypeTyped); + QCOMPARE(page.navigations[2].type, QWebEnginePage::NavigationTypeLinkClicked); + QCOMPARE(page.navigations[2].url, QUrl(QString("qrc:/S0"))); +} + void tst_QWebEnginePage::popupFormSubmission() { TestPage page; @@ -697,7 +762,7 @@ public: CursorTrackedPage(QWidget *parent = 0): QWebEnginePage(parent) { } - QString selectedText() { + QString jsSelectedText() { return evaluateJavaScriptSync(this, "window.getSelection().toString()").toString(); } @@ -713,62 +778,91 @@ public: int isSelectionCollapsed() { return evaluateJavaScriptSync(this, "window.getSelection().getRangeAt(0).collapsed").toBool(); } - bool hasSelection() - { - return !selectedText().isEmpty(); - } }; void tst_QWebEnginePage::textSelection() { - QWebEngineView view; - CursorTrackedPage *page = new CursorTrackedPage(&view); - QString content("<html><body><p id=one>The quick brown fox</p>" \ + CursorTrackedPage page; + + QString textToSelect("The quick brown fox"); + QString content = QString("<html><body><p id=one>%1</p>" \ "<p id=two>jumps over the lazy dog</p>" \ - "<p>May the source<br/>be with you!</p></body></html>"); - page->setView(&view); - QSignalSpy loadSpy(&view, SIGNAL(loadFinished(bool))); - page->setHtml(content); + "<p>May the source<br/>be with you!</p></body></html>").arg(textToSelect); + + QSignalSpy loadSpy(&page, SIGNAL(loadFinished(bool))); + page.setHtml(content); QTRY_COMPARE_WITH_TIMEOUT(loadSpy.count(), 1, 20000); // these actions must exist - QVERIFY(page->action(QWebEnginePage::SelectAll) != 0); + QVERIFY(page.action(QWebEnginePage::SelectAll) != 0); // ..but SelectAll is disabled because the page has no focus due to disabled FocusOnNavigationEnabled. - QCOMPARE(page->action(QWebEnginePage::SelectAll)->isEnabled(), false); + QCOMPARE(page.action(QWebEnginePage::SelectAll)->isEnabled(), false); // Verify hasSelection returns false since there is no selection yet... - QCOMPARE(page->hasSelection(), false); + QVERIFY(!page.hasSelection()); + QVERIFY(page.jsSelectedText().isEmpty()); // this will select the first paragraph QString selectScript = "var range = document.createRange(); " \ "var node = document.getElementById(\"one\"); " \ "range.selectNode(node); " \ "getSelection().addRange(range);"; - evaluateJavaScriptSync(page, selectScript); - QCOMPARE(page->selectedText().trimmed(), QString::fromLatin1("The quick brown fox")); + evaluateJavaScriptSync(&page, selectScript); + // Make sure hasSelection returns true, since there is selected text now... - QCOMPARE(page->hasSelection(), true); + QTRY_VERIFY(page.hasSelection()); + QCOMPARE(page.selectedText().trimmed(), textToSelect); + + QCOMPARE(page.jsSelectedText().trimmed(), textToSelect); + + // navigate away and check that selection is cleared + page.load(QUrl("about:blank")); + QTRY_COMPARE(loadSpy.count(), 2); + + QVERIFY(!page.hasSelection()); + QVERIFY(page.selectedText().isEmpty()); + + QVERIFY(page.jsSelectedText().isEmpty()); } void tst_QWebEnginePage::backActionUpdate() { QWebEngineView view; + view.resize(640, 480); + view.show(); + QWebEnginePage *page = view.page(); + QSignalSpy loadSpy(page, &QWebEnginePage::loadFinished); QAction *action = page->action(QWebEnginePage::Back); QVERIFY(!action->isEnabled()); - QSignalSpy loadSpy(page, SIGNAL(loadFinished(bool))); - QUrl url = QUrl("qrc:///resources/framedindex.html"); - page->load(url); + + page->load(QUrl("qrc:///resources/framedindex.html")); QTRY_COMPARE_WITH_TIMEOUT(loadSpy.count(), 1, 20000); QVERIFY(!action->isEnabled()); - QTest::mouseClick(&view, Qt::LeftButton, 0, QPoint(10, 10)); - QEXPECT_FAIL("", "Behavior change: Load signals are emitted only for the main frame in QtWebEngine.", Continue); - QTRY_COMPARE_WITH_TIMEOUT(loadSpy.count(), 2, 100); - QEXPECT_FAIL("", "FIXME: Mouse events aren't passed from the QWebEngineView down to the RWHVQtDelegateWidget", Continue); - QVERIFY(action->isEnabled()); + auto firstAnchorCenterInFrame = [](QWebEnginePage *page, const QString &frameName) { + QVariantList rectList = evaluateJavaScriptSync(page, + "(function(){" + "var frame = document.getElementsByName('" + frameName + "')[0];" + "var anchor = frame.contentDocument.getElementsByTagName('a')[0];" + "var rect = anchor.getBoundingClientRect();" + "return [(rect.left + rect.right) / 2, (rect.top + rect.bottom) / 2];" + "})()").toList(); + + if (rectList.count() != 2) { + qWarning("firstAnchorCenterInFrame failed."); + return QPoint(); + } + + return QPoint(rectList.at(0).toInt(), rectList.at(1).toInt()); + }; + + QVERIFY(evaluateJavaScriptSync(page, "document.getElementsByName('frame_b')[0].contentDocument == undefined").toBool()); + QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, firstAnchorCenterInFrame(page, "frame_c")); + QTRY_VERIFY(evaluateJavaScriptSync(page, "document.getElementsByName('frame_b')[0].contentDocument != undefined").toBool()); + QTRY_VERIFY(action->isEnabled()); } void tst_QWebEnginePage::localStorageVisibility() @@ -791,11 +885,14 @@ void tst_QWebEnginePage::localStorageVisibility() QVERIFY(evaluateJavaScriptSync(&webPage1, QString("(window.localStorage != undefined)")).toBool()); QVERIFY(!evaluateJavaScriptSync(&webPage2, QString("(window.localStorage != undefined)")).toBool()); - // Switching the feature off does not actively remove the object from webPage1. + // Toggle local setting for every page and... webPage1.settings()->setAttribute(QWebEngineSettings::LocalStorageEnabled, false); webPage2.settings()->setAttribute(QWebEngineSettings::LocalStorageEnabled, true); + // TODO: note this setting is flaky, consider settings().commit() + // ...first check second page (for storage to appear) as applying settings is batched and done asynchronously + QTRY_VERIFY(evaluateJavaScriptSync(&webPage2, QString("(window.localStorage != undefined)")).toBool()); + // Switching the feature off does not actively remove the object from webPage1. QVERIFY(evaluateJavaScriptSync(&webPage1, QString("(window.localStorage != undefined)")).toBool()); - QVERIFY(evaluateJavaScriptSync(&webPage2, QString("(window.localStorage != undefined)")).toBool()); // The object disappears only after reloading. webPage1.triggerAction(QWebEnginePage::Reload); @@ -948,7 +1045,7 @@ void tst_QWebEnginePage::findText() { CallbackSpy<bool> callbackSpy; QSignalSpy signalSpy(m_view->page(), &QWebEnginePage::findTextFinished); - m_view->findText("", 0, callbackSpy.ref()); + m_view->findText("", {}, callbackSpy.ref()); QVERIFY(callbackSpy.wasCalled()); QCOMPARE(signalSpy.count(), 1); QTRY_COMPARE(m_view->selectedText(), QString("foo bar")); @@ -959,7 +1056,7 @@ void tst_QWebEnginePage::findText() { CallbackSpy<bool> callbackSpy; QSignalSpy signalSpy(m_view->page(), &QWebEnginePage::findTextFinished); - m_view->findText("Will not be found", 0, callbackSpy.ref()); + m_view->findText("Will not be found", {}, callbackSpy.ref()); QCOMPARE(callbackSpy.waitForResult(), false); QTRY_COMPARE(signalSpy.count(), 1); auto result = signalSpy.takeFirst().value(0).value<QWebEngineFindTextResult>(); @@ -976,7 +1073,7 @@ void tst_QWebEnginePage::findText() { CallbackSpy<bool> callbackSpy; QSignalSpy signalSpy(m_view->page(), &QWebEnginePage::findTextFinished); - m_view->findText("foo", 0, callbackSpy.ref()); + m_view->findText("foo", {}, callbackSpy.ref()); QVERIFY(callbackSpy.waitForResult()); QTRY_COMPARE(signalSpy.count(), 1); QTRY_VERIFY(m_view->selectedText().isEmpty()); @@ -987,7 +1084,7 @@ void tst_QWebEnginePage::findText() { CallbackSpy<bool> callbackSpy; QSignalSpy signalSpy(m_view->page(), &QWebEnginePage::findTextFinished); - m_view->findText("", 0, callbackSpy.ref()); + m_view->findText("", {}, callbackSpy.ref()); QTRY_VERIFY(callbackSpy.wasCalled()); QTRY_COMPARE(signalSpy.count(), 1); QTRY_COMPARE(m_view->selectedText(), QString("foo")); @@ -997,8 +1094,8 @@ void tst_QWebEnginePage::findText() // should interrupt the first one. { QSignalSpy signalSpy(m_view->page(), &QWebEnginePage::findTextFinished); - m_view->findText("foo", 0); - m_view->findText("foo", 0); + m_view->findText("foo", {}); + m_view->findText("foo", {}); QTRY_COMPARE(signalSpy.count(), 2); QTRY_VERIFY(m_view->selectedText().isEmpty()); @@ -1053,11 +1150,11 @@ void tst_QWebEnginePage::findTextSuccessiveShouldCallAllCallbacks() QSignalSpy loadSpy(m_view, SIGNAL(loadFinished(bool))); m_view->setHtml(QString("<html><head></head><body><div>abcdefg abcdefg abcdefg abcdefg abcdefg</div></body></html>")); QTRY_COMPARE_WITH_TIMEOUT(loadSpy.count(), 1, 20000); - m_page->findText("abcde", 0, spy1.ref()); - m_page->findText("abcd", 0, spy2.ref()); - m_page->findText("abc", 0, spy3.ref()); - m_page->findText("ab", 0, spy4.ref()); - m_page->findText("a", 0, spy5.ref()); + m_page->findText("abcde", {}, spy1.ref()); + m_page->findText("abcd", {}, spy2.ref()); + m_page->findText("abc", {}, spy3.ref()); + m_page->findText("ab", {}, spy4.ref()); + m_page->findText("a", {}, spy5.ref()); spy5.waitForResult(); QVERIFY(spy1.wasCalled()); QVERIFY(spy2.wasCalled()); @@ -1078,10 +1175,10 @@ void tst_QWebEnginePage::findTextCalledOnMatch() // CALLBACK bool callbackCalled = false; - m_view->page()->findText("foo", 0, [this, &callbackCalled](bool found) { + m_view->page()->findText("foo", {}, [this, &callbackCalled](bool found) { QVERIFY(found); - m_view->page()->findText("bar", 0, [&callbackCalled](bool found) { + m_view->page()->findText("bar", {}, [&callbackCalled](bool found) { QVERIFY(found); callbackCalled = true; }); @@ -1115,7 +1212,7 @@ void tst_QWebEnginePage::findTextActiveMatchOrdinal() // Iterate over all "foo" matches. for (int i = 1; i <= 3; ++i) { - m_view->page()->findText("foo", 0); + m_view->page()->findText("foo", {}); QTRY_COMPARE(findTextSpy.count(), 1); result = findTextSpy.takeFirst().value(0).value<QWebEngineFindTextResult>(); QCOMPARE(result.numberOfMatches(), 3); @@ -1123,7 +1220,7 @@ void tst_QWebEnginePage::findTextActiveMatchOrdinal() } // The last match is followed by the fist one. - m_view->page()->findText("foo", 0); + m_view->page()->findText("foo", {}); QTRY_COMPARE(findTextSpy.count(), 1); result = findTextSpy.takeFirst().value(0).value<QWebEngineFindTextResult>(); QCOMPARE(result.numberOfMatches(), 3); @@ -1137,14 +1234,14 @@ void tst_QWebEnginePage::findTextActiveMatchOrdinal() QCOMPARE(result.activeMatch(), 3); // Finding another word resets the activeMatch. - m_view->page()->findText("bar", 0); + m_view->page()->findText("bar", {}); QTRY_COMPARE(findTextSpy.count(), 1); result = findTextSpy.takeFirst().value(0).value<QWebEngineFindTextResult>(); QCOMPARE(result.numberOfMatches(), 2); QCOMPARE(result.activeMatch(), 1); // If no match activeMatch is 0. - m_view->page()->findText("bla", 0); + m_view->page()->findText("bla", {}); QTRY_COMPARE(findTextSpy.count(), 1); result = findTextSpy.takeFirst().value(0).value<QWebEngineFindTextResult>(); QCOMPARE(result.numberOfMatches(), 0); @@ -1655,12 +1752,15 @@ void tst_QWebEnginePage::savePage() void tst_QWebEnginePage::openWindowDefaultSize() { TestPage page; + QSignalSpy spyFinished(&page, &QWebEnginePage::loadFinished); QSignalSpy windowCreatedSpy(&page, SIGNAL(windowCreated())); QWebEngineView view; page.setView(&view); + page.settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, true); + page.setUrl(QUrl("about:blank")); view.show(); + QTRY_COMPARE(spyFinished.count(), 1); - page.settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, true); // Open a default window. page.runJavaScript("window.open()"); QTRY_COMPARE(windowCreatedSpy.count(), 1); @@ -1682,156 +1782,51 @@ void tst_QWebEnginePage::openWindowDefaultSize() QCOMPARE(requestedGeometry.height(), 100); } -class JavaScriptCallbackBase +bool tst_QWebEnginePage::isFalseJavaScriptResult(QWebEnginePage *page, const QString &javaScript) { -public: - JavaScriptCallbackBase() - { - if (watcher) - QMetaObject::invokeMethod(watcher, "add"); - } - - void operator() (const QVariant &result) - { - check(result); - if (watcher) - QMetaObject::invokeMethod(watcher, "notify"); - } - -protected: - virtual void check(const QVariant &result) = 0; - -private: - friend class JavaScriptCallbackWatcher; - static QPointer<QObject> watcher; -}; - -QPointer<QObject> JavaScriptCallbackBase::watcher = 0; - -class JavaScriptCallback : public JavaScriptCallbackBase -{ -public: - JavaScriptCallback() { } - JavaScriptCallback(const QVariant& _expected) : expected(_expected) { } - - void check(const QVariant& result) override - { - QVERIFY(result.isValid()); - QCOMPARE(result, expected); - } - -private: - QVariant expected; -}; - -class JavaScriptCallbackNull : public JavaScriptCallbackBase -{ -public: - void check(const QVariant& result) override - { - QVERIFY(result.isNull()); -// FIXME: Returned null values are currently invalid QVariants. -// QVERIFY(result.isValid()); - } -}; + QVariant result = evaluateJavaScriptSync(page, javaScript); + return !result.isNull() && result.isValid() && result == QVariant(false); +} -class JavaScriptCallbackUndefined : public JavaScriptCallbackBase +bool tst_QWebEnginePage::isTrueJavaScriptResult(QWebEnginePage *page, const QString &javaScript) { -public: - void check(const QVariant& result) override - { - QVERIFY(result.isNull()); - QVERIFY(!result.isValid()); - } -}; + QVariant result = evaluateJavaScriptSync(page, javaScript); + return !result.isNull() && result.isValid() && result == QVariant(true); +} -class JavaScriptCallbackWatcher : public QObject +bool tst_QWebEnginePage::isEmptyListJavaScriptResult(QWebEnginePage *page, const QString &javaScript) { - Q_OBJECT -public: - JavaScriptCallbackWatcher() - { - Q_ASSERT(!JavaScriptCallbackBase::watcher); - JavaScriptCallbackBase::watcher = this; - } - - Q_INVOKABLE void add() - { - available++; - } - - Q_INVOKABLE void notify() - { - called++; - if (called == available) - emit allCalled(); - } - - bool wait(int maxSeconds = 30) - { - if (called == available) - return true; - - QTestEventLoop loop; - connect(this, SIGNAL(allCalled()), &loop, SLOT(exitLoop())); - loop.enterLoop(maxSeconds); - return !loop.timeout(); - } - -signals: - void allCalled(); - -private: - int available = 0; - int called = 0; -}; - + QVariant result = evaluateJavaScriptSync(page, javaScript); + return !result.isNull() && result.isValid() && result == QList<QVariant>(); +} void tst_QWebEnginePage::runJavaScript() { TestPage page; - JavaScriptCallbackWatcher watcher; - - JavaScriptCallback callbackBool(QVariant(false)); - page.runJavaScript("false", QWebEngineCallback<const QVariant&>(callbackBool)); - - JavaScriptCallback callbackInt(QVariant(2)); - page.runJavaScript("2", QWebEngineCallback<const QVariant&>(callbackInt)); - - JavaScriptCallback callbackDouble(QVariant(2.5)); - page.runJavaScript("2.5", QWebEngineCallback<const QVariant&>(callbackDouble)); - - JavaScriptCallback callbackString(QVariant(QStringLiteral("Test"))); - page.runJavaScript("\"Test\"", QWebEngineCallback<const QVariant&>(callbackString)); - - QVariantList list; - JavaScriptCallback callbackList(list); - page.runJavaScript("[]", QWebEngineCallback<const QVariant&>(callbackList)); - + QVariant result; QVariantMap map; - map.insert(QStringLiteral("test"), QVariant(2)); - JavaScriptCallback callbackMap(map); - page.runJavaScript("var el = {\"test\": 2}; el", QWebEngineCallback<const QVariant&>(callbackMap)); - JavaScriptCallbackNull callbackNull; - page.runJavaScript("null", QWebEngineCallback<const QVariant&>(callbackNull)); + QVERIFY(isFalseJavaScriptResult(&page, "false")); + QCOMPARE(evaluateJavaScriptSync(&page, "2").toInt(), 2); + QCOMPARE(evaluateJavaScriptSync(&page, "2.5").toDouble(), 2.5); + QCOMPARE(evaluateJavaScriptSync(&page, "\"Test\"").toString(), "Test"); + QVERIFY(isEmptyListJavaScriptResult(&page, "[]")); - JavaScriptCallbackUndefined callbackUndefined; - page.runJavaScript("undefined", QWebEngineCallback<const QVariant&>(callbackUndefined)); + map.insert(QStringLiteral("test"), QVariant(2)); + QCOMPARE(evaluateJavaScriptSync(&page, "var el = {\"test\": 2}; el").toMap(), map); - JavaScriptCallback callbackDate(QVariant(42.0)); - page.runJavaScript("new Date(42000)", QWebEngineCallback<const QVariant&>(callbackDate)); + QVERIFY(evaluateJavaScriptSync(&page, "null").isNull()); - JavaScriptCallback callbackBlob(QVariant(QByteArray(8, 0))); - page.runJavaScript("new ArrayBuffer(8)", QWebEngineCallback<const QVariant&>(callbackBlob)); + result = evaluateJavaScriptSync(&page, "undefined"); + QVERIFY(result.isNull() && !result.isValid()); - JavaScriptCallbackUndefined callbackFunction; - page.runJavaScript("(function(){})", QWebEngineCallback<const QVariant&>(callbackFunction)); + QCOMPARE(evaluateJavaScriptSync(&page, "new Date(42000)").toDate(), QVariant(42.0).toDate()); + QCOMPARE(evaluateJavaScriptSync(&page, "new ArrayBuffer(8)").toByteArray(), QByteArray(8, 0)); - JavaScriptCallback callbackPromise(QVariant(QVariantMap{})); - page.runJavaScript("new Promise(function(){})", QWebEngineCallback<const QVariant&>(callbackPromise)); + result = evaluateJavaScriptSync(&page, "(function(){})"); + QVERIFY(result.isNull() && !result.isValid()); - QVERIFY(watcher.wait()); + QCOMPARE(evaluateJavaScriptSync(&page, "new Promise(function(){})"), QVariant(QVariantMap{})); } void tst_QWebEnginePage::runJavaScriptDisabled() @@ -1874,9 +1869,9 @@ void tst_QWebEnginePage::runJavaScriptFromSlot() void tst_QWebEnginePage::fullScreenRequested() { - JavaScriptCallbackWatcher watcher; QWebEngineView view; QWebEnginePage* page = view.page(); + view.resize(640, 480); view.show(); page->settings()->setAttribute(QWebEngineSettings::FullScreenSupportEnabled, true); @@ -1885,9 +1880,8 @@ void tst_QWebEnginePage::fullScreenRequested() page->load(QUrl("qrc:///resources/fullscreen.html")); QTRY_COMPARE(loadSpy.count(), 1); - page->runJavaScript("document.webkitFullscreenEnabled", JavaScriptCallback(true)); - page->runJavaScript("document.webkitIsFullScreen", JavaScriptCallback(false)); - QVERIFY(watcher.wait()); + QTRY_VERIFY(isTrueJavaScriptResult(page, "document.webkitFullscreenEnabled")); + QVERIFY(isFalseJavaScriptResult(page, "document.webkitIsFullScreen")); // FullscreenRequest must be a user gesture bool acceptRequest = true; @@ -1897,17 +1891,22 @@ void tst_QWebEnginePage::fullScreenRequested() }); QTest::keyPress(view.focusProxy(), Qt::Key_Space); - QTRY_VERIFY(evaluateJavaScriptSync(page, "document.webkitIsFullScreen").toBool()); - page->runJavaScript("document.webkitExitFullscreen()", JavaScriptCallbackUndefined()); - QVERIFY(watcher.wait()); + QTRY_VERIFY(isTrueJavaScriptResult(page, "document.webkitIsFullScreen")); + + QTest::mouseMove(view.windowHandle(), QPoint(10,10)); + QTest::mouseClick(view.windowHandle(), Qt::RightButton); + QTRY_COMPARE(view.findChildren<QMenu *>().count(), 1); + auto menu = view.findChildren<QMenu *>().first(); + QVERIFY(menu->actions().contains(page->action(QWebEnginePage::ExitFullScreen))); + + page->runJavaScript("document.webkitExitFullscreen()"); + QTRY_VERIFY(isFalseJavaScriptResult(page, "document.webkitIsFullScreen")); acceptRequest = false; - page->runJavaScript("document.webkitFullscreenEnabled", JavaScriptCallback(true)); + QVERIFY(isTrueJavaScriptResult(page, "document.webkitFullscreenEnabled")); QTest::keyPress(view.focusProxy(), Qt::Key_Space); - QVERIFY(watcher.wait()); - page->runJavaScript("document.webkitIsFullScreen", JavaScriptCallback(false)); - QVERIFY(watcher.wait()); + QTRY_VERIFY(isFalseJavaScriptResult(page, "document.webkitIsFullScreen")); } void tst_QWebEnginePage::quotaRequested() @@ -2152,7 +2151,7 @@ void tst_QWebEnginePage::requestedUrlAfterSetAndLoadFailures() page.load(second); QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 2, 20000); - QCOMPARE(page.url(), second); + QCOMPARE(page.url(), first); QCOMPARE(page.requestedUrl(), second); QVERIFY(!spy.at(1).first().toBool()); } @@ -2308,9 +2307,6 @@ void tst_QWebEnginePage::setHtmlWithModuleImport() "}\n"); rr->setResponseHeader("Content-Type", "text/javascript"); rr->sendResponse(); - } else { - rr->setResponseStatus(404); - rr->sendResponse(); } }); QVERIFY(server.start()); @@ -2781,14 +2777,14 @@ void tst_QWebEnginePage::setUrlUsingStateObject() evaluateJavaScriptSync(m_page, "window.history.pushState(null, 'push', 'navigate/to/here')"); expectedUrlChangeCount++; - QCOMPARE(urlChangedSpy.count(), expectedUrlChangeCount); + QTRY_COMPARE(urlChangedSpy.count(), expectedUrlChangeCount); QCOMPARE(m_page->url(), QUrl("qrc:/resources/navigate/to/here")); QCOMPARE(m_page->history()->count(), 2); QVERIFY(m_page->history()->canGoBack()); evaluateJavaScriptSync(m_page, "window.history.replaceState(null, 'replace', 'another/location')"); expectedUrlChangeCount++; - QCOMPARE(urlChangedSpy.count(), expectedUrlChangeCount); + QTRY_COMPARE(urlChangedSpy.count(), expectedUrlChangeCount); QCOMPARE(m_page->url(), QUrl("qrc:/resources/navigate/to/another/location")); QCOMPARE(m_page->history()->count(), 2); QVERIFY(!m_page->history()->canGoForward()); @@ -2837,8 +2833,8 @@ void tst_QWebEnginePage::setUrlThenLoads() const QUrl urlToLoad2("qrc:/resources/test1.html"); m_page->load(urlToLoad1); - QCOMPARE(m_page->url(), urlToLoad1); - QCOMPARE(m_page->requestedUrl(), urlToLoad1); + QTRY_COMPARE(m_page->url(), urlToLoad1); + QTRY_COMPARE(m_page->requestedUrl(), urlToLoad1); // baseUrlSync spins an event loop and this sometimes return the next result. // QCOMPARE(baseUrlSync(m_page), baseUrl); QTRY_COMPARE(startedSpy.count(), 2); @@ -2852,7 +2848,7 @@ void tst_QWebEnginePage::setUrlThenLoads() QCOMPARE(baseUrlSync(m_page), extractBaseUrl(urlToLoad1)); m_page->load(urlToLoad2); - QCOMPARE(m_page->url(), urlToLoad2); + QCOMPARE(m_page->url(), urlToLoad1); QCOMPARE(m_page->requestedUrl(), urlToLoad2); QCOMPARE(baseUrlSync(m_page), extractBaseUrl(urlToLoad1)); QTRY_COMPARE(startedSpy.count(), 3); @@ -2866,20 +2862,6 @@ void tst_QWebEnginePage::setUrlThenLoads() QCOMPARE(baseUrlSync(m_page), extractBaseUrl(urlToLoad2)); } -void tst_QWebEnginePage::loadFinishedAfterNotFoundError() -{ - QWebEnginePage page; - QSignalSpy spy(&page, SIGNAL(loadFinished(bool))); - - page.settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, false); - page.setUrl(QUrl("http://non.existent/url")); - QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 1, 20000); - - page.settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, true); - page.setUrl(QUrl("http://another.non.existent/url")); - QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 2, 20000); -} - class URLSetter : public QObject { Q_OBJECT @@ -2961,11 +2943,7 @@ void tst_QWebEnginePage::loadInSignalHandlers() URLSetter setter(m_page, signal, type, urlForSetter); QSignalSpy spy(&setter, &URLSetter::finished); m_page->load(url); - // every loadStarted() call should have also loadFinished() - if (signal == URLSetter::LoadStarted) - QTRY_COMPARE(spy.count(), 2); - else - QTRY_COMPARE(spy.count(), 1); + QTRY_COMPARE(spy.count(), 1); QCOMPARE(m_page->url(), urlForSetter); } @@ -3067,25 +3045,42 @@ void tst_QWebEnginePage::toPlainTextLoadFinishedRace() void tst_QWebEnginePage::setZoomFactor() { - QWebEnginePage page; + TestBasePage page, page2; - QVERIFY(qFuzzyCompare(page.zoomFactor(), 1.0)); + QCOMPARE(page.zoomFactor(), 1.0); page.setZoomFactor(2.5); - QVERIFY(qFuzzyCompare(page.zoomFactor(), 2.5)); - - const QUrl urlToLoad("qrc:/resources/test1.html"); - - QSignalSpy finishedSpy(&page, SIGNAL(loadFinished(bool))); - page.load(urlToLoad); - QTRY_COMPARE(finishedSpy.count(), 1); - QVERIFY(finishedSpy.at(0).first().toBool()); - QVERIFY(qFuzzyCompare(page.zoomFactor(), 2.5)); - - page.setZoomFactor(5.5); - QVERIFY(qFuzzyCompare(page.zoomFactor(), 2.5)); + QCOMPARE(page.zoomFactor(), 2.5); + + const QUrl url1("qrc:/resources/test1.html"), url2(QUrl("qrc:/resources/test2.html")); + + page.load(url1); + QTRY_COMPARE(page.loadSpy.count(), 1); + QVERIFY(page.loadSpy.at(0).first().toBool()); + QCOMPARE(page.zoomFactor(), 2.5); + + page.setZoomFactor(5.5); // max accepted zoom: kMaximumPageZoomFactor = 5.0 + QCOMPARE(page.zoomFactor(), 2.5); + + page.setZoomFactor(0.1); // min accepted zoom: kMinimumPageZoomFactor = 0.25 + QCOMPARE(page.zoomFactor(), 2.5); + + // try loading different url and check new values after load + page.loadSpy.clear(); + for (auto &&p : { + qMakePair(&page, 2.5), // navigating away to different url should keep zoom + qMakePair(&page2, 1.0), // same url navigation in diffent page shouldn't be affected + }) { + auto &&page = *p.first; auto zoomFactor = p.second; + page.load(url2); + QTRY_COMPARE(page.loadSpy.count(), 1); + QVERIFY(page.loadSpy.last().first().toBool()); + QCOMPARE(page.zoomFactor(), zoomFactor); + } - page.setZoomFactor(0.1); - QVERIFY(qFuzzyCompare(page.zoomFactor(), 2.5)); + // should have no influence on first page + page2.setZoomFactor(3.5); + for (auto &&p : { qMakePair(&page, 2.5), qMakePair(&page2, 3.5), }) + QCOMPARE(p.first->zoomFactor(), p.second); } void tst_QWebEnginePage::mouseButtonTranslation() @@ -3315,9 +3310,6 @@ void tst_QWebEnginePage::registerProtocolHandler() } else if (rr->requestMethod() == "GET" && rr->requestPath() == "/mail?uri=mailto%3Afoo%40bar.com") { mailRequestCount++; rr->sendResponse(); - } else { - rr->setResponseStatus(404); - rr->sendResponse(); } }); QVERIFY(server.start()); @@ -3366,7 +3358,7 @@ void tst_QWebEnginePage::dataURLFragment() QTRY_COMPARE(loadFinishedSpy.count(), 1); QSignalSpy urlChangedSpy(m_page, SIGNAL(urlChanged(QUrl))); - QTest::mouseClick(m_view->focusProxy(), Qt::LeftButton, 0, elementCenter(m_page, "link")); + QTest::mouseClick(m_view->focusProxy(), Qt::LeftButton, {}, elementCenter(m_page, "link")); QVERIFY(urlChangedSpy.wait()); QCOMPARE(m_page->url().fragment(), QStringLiteral("anchor")); @@ -3376,7 +3368,7 @@ void tst_QWebEnginePage::dataURLFragment() "</body></html>", QUrl("http://test.qt.io/mytest.html")); QTRY_COMPARE(loadFinishedSpy.count(), 2); - QTest::mouseClick(m_view->focusProxy(), Qt::LeftButton, 0, elementCenter(m_page, "link")); + QTest::mouseClick(m_view->focusProxy(), Qt::LeftButton, {}, elementCenter(m_page, "link")); QVERIFY(urlChangedSpy.wait()); QCOMPARE(m_page->url(), QUrl("http://test.qt.io/mytest.html#anchor")); } @@ -3398,7 +3390,7 @@ void tst_QWebEnginePage::devTools() QCOMPARE(devToolsPage.devToolsPage(), nullptr); QCOMPARE(devToolsPage.inspectedPage(), &inspectedPage1); - QTRY_COMPARE(spy.count(), 1); + QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 1, 90000); QVERIFY(spy.takeFirst().value(0).toBool()); devToolsPage.setInspectedPage(&inspectedPage2); @@ -3410,7 +3402,7 @@ void tst_QWebEnginePage::devTools() QCOMPARE(devToolsPage.devToolsPage(), nullptr); QCOMPARE(devToolsPage.inspectedPage(), &inspectedPage2); - QTRY_COMPARE(spy.count(), 1); + QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 1, 90000); QVERIFY(spy.takeFirst().value(0).toBool()); devToolsPage.setInspectedPage(nullptr); @@ -3446,11 +3438,174 @@ void tst_QWebEnginePage::openLinkInDifferentProfile() QTRY_COMPARE(spy1.count(), 1); QVERIFY(spy1.takeFirst().value(0).toBool()); page1.targetPage = &page2; - QTest::mouseClick(view.focusProxy(), Qt::MiddleButton, 0, elementCenter(&page1, "link")); + QTest::mouseClick(view.focusProxy(), Qt::MiddleButton, {}, elementCenter(&page1, "link")); QTRY_COMPARE(spy2.count(), 1); QVERIFY(spy2.takeFirst().value(0).toBool()); } +// What does createWindow do? +enum class OpenLinkInNewPageDecision { + // Returns nullptr, + ReturnNull, + // Returns this, + ReturnSelf, + // Returns page != this + ReturnOther, +}; + +// What causes createWindow to be called? +enum class OpenLinkInNewPageCause { + // User clicks on a link with target=_blank. + TargetBlank, + // User clicks with MiddleButton. + MiddleClick, +}; + +// What happens after createWindow? +enum class OpenLinkInNewPageEffect { + // The navigation request disappears into the ether. + Blocked, + // The navigation request becomes a navigation in the original page. + LoadInSelf, + // The navigation request becomes a navigation in a different page. + LoadInOther, +}; + +Q_DECLARE_METATYPE(OpenLinkInNewPageCause) +Q_DECLARE_METATYPE(OpenLinkInNewPageDecision) +Q_DECLARE_METATYPE(OpenLinkInNewPageEffect) + +void tst_QWebEnginePage::openLinkInNewPage_data() +{ + using Decision = OpenLinkInNewPageDecision; + using Cause = OpenLinkInNewPageCause; + using Effect = OpenLinkInNewPageEffect; + + QTest::addColumn<Decision>("decision"); + QTest::addColumn<Cause>("cause"); + QTest::addColumn<Effect>("effect"); + + // Note that the meaning of returning nullptr from createWindow is not + // consistent between the TargetBlank and MiddleClick scenarios. + // + // With TargetBlank, the open-in-new-page disposition comes from the HTML + // target attribute; something the user is probably not aware of. Returning + // nullptr is interpreted as a decision by the app to block an unwanted + // popup. + // + // With MiddleClick, the open-in-new-page disposition comes from the user's + // explicit intent. Returning nullptr is then interpreted as a failure by + // the app to fulfill this intent, which we try to compensate by ignoring + // the disposition and performing the navigation request normally. + + QTest::newRow("BlockPopup") << Decision::ReturnNull << Cause::TargetBlank << Effect::Blocked; +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QTest::newRow("IgnoreIntent") << Decision::ReturnNull << Cause::MiddleClick << Effect::Blocked; +#else + QTest::newRow("IgnoreIntent") << Decision::ReturnNull << Cause::MiddleClick << Effect::LoadInSelf; +#endif + QTest::newRow("OverridePopup") << Decision::ReturnSelf << Cause::TargetBlank << Effect::LoadInSelf; + QTest::newRow("OverrideIntent") << Decision::ReturnSelf << Cause::MiddleClick << Effect::LoadInSelf; + QTest::newRow("AcceptPopup") << Decision::ReturnOther << Cause::TargetBlank << Effect::LoadInOther; + QTest::newRow("AcceptIntent") << Decision::ReturnOther << Cause::MiddleClick << Effect::LoadInOther; +} + +void tst_QWebEnginePage::openLinkInNewPage() +{ + using Decision = OpenLinkInNewPageDecision; + using Cause = OpenLinkInNewPageCause; + using Effect = OpenLinkInNewPageEffect; + + class Page : public QWebEnginePage + { + public: + Page *targetPage = nullptr; + QSignalSpy spy{this, &QWebEnginePage::loadFinished}; + Page(QWebEngineProfile *profile) : QWebEnginePage(profile) {} + private: + QWebEnginePage *createWindow(WebWindowType) override { return targetPage; } + }; + + class View : public QWebEngineView + { + public: + View(Page *page) + { + resize(500, 500); + setPage(page); + } + }; + + QFETCH(Decision, decision); + QFETCH(Cause, cause); + QFETCH(Effect, effect); + + QWebEngineProfile profile; + Page page1(&profile); + Page page2(&profile); + View view1(&page1); + View view2(&page2); + + view1.show(); + QVERIFY(QTest::qWaitForWindowExposed(&view1)); + + page1.setHtml("<html><body>" + "<a id='link' href='data:,hello' target='_blank'>link</a>" + "</body></html>"); + QTRY_COMPARE(page1.spy.count(), 1); + QVERIFY(page1.spy.takeFirst().value(0).toBool()); + + switch (decision) { + case Decision::ReturnNull: + page1.targetPage = nullptr; + break; + case Decision::ReturnSelf: + page1.targetPage = &page1; + break; + case Decision::ReturnOther: + page1.targetPage = &page2; + break; + } + + Qt::MouseButton button; + switch (cause) { + case Cause::TargetBlank: + button = Qt::LeftButton; + break; + case Cause::MiddleClick: + button = Qt::MiddleButton; + break; + } + QTest::mouseClick(view1.focusProxy(), button, {}, elementCenter(&page1, "link")); + + switch (effect) { + case Effect::Blocked: + // Test nothing new loaded + QTest::qWait(500); + QCOMPARE(page1.spy.count(), 0); + QCOMPARE(page2.spy.count(), 0); + break; + case Effect::LoadInSelf: + QTRY_COMPARE(page1.spy.count(), 1); + QVERIFY(page1.spy.takeFirst().value(0).toBool()); + QCOMPARE(page2.spy.count(), 0); + if (decision == Decision::ReturnSelf && cause == Cause::TargetBlank) + // History was discarded due to AddNewContents + QCOMPARE(page1.history()->count(), 1); + else + QCOMPARE(page1.history()->count(), 2); + QCOMPARE(page2.history()->count(), 0); + break; + case Effect::LoadInOther: + QTRY_COMPARE(page2.spy.count(), 1); + QVERIFY(page2.spy.takeFirst().value(0).toBool()); + QCOMPARE(page1.spy.count(), 0); + QCOMPARE(page1.history()->count(), 1); + QCOMPARE(page2.history()->count(), 1); + break; + } +} + void tst_QWebEnginePage::triggerActionWithoutMenu() { // Calling triggerAction should not crash even when for @@ -3502,30 +3657,61 @@ public: } }; -void tst_QWebEnginePage::notificationRequest_data() +void tst_QWebEnginePage::notificationPermission_data() { + QTest::addColumn<bool>("setOnInit"); QTest::addColumn<QWebEnginePage::PermissionPolicy>("policy"); QTest::addColumn<QString>("permission"); - QTest::newRow("deny") << QWebEnginePage::PermissionDeniedByUser << "denied"; - QTest::newRow("grant") << QWebEnginePage::PermissionGrantedByUser << "granted"; + QTest::newRow("denyOnInit") << true << QWebEnginePage::PermissionDeniedByUser << "denied"; + QTest::newRow("deny") << false << QWebEnginePage::PermissionDeniedByUser << "denied"; + QTest::newRow("grant") << false << QWebEnginePage::PermissionGrantedByUser << "granted"; + QTest::newRow("grantOnInit") << true << QWebEnginePage::PermissionGrantedByUser << "granted"; } -void tst_QWebEnginePage::notificationRequest() +void tst_QWebEnginePage::notificationPermission() { + QFETCH(bool, setOnInit); QFETCH(QWebEnginePage::PermissionPolicy, policy); QFETCH(QString, permission); - NotificationPage page(policy); - QVERIFY(page.spyLoad.waitForResult()); + QWebEngineProfile otr; + QWebEnginePage page(&otr, nullptr); - page.resetPermission(); - QCOMPARE(page.getPermission(), "default"); + QUrl baseUrl("https://www.example.com/somepage.html"); - page.requestPermission(); - page.spyRequest.waitForResult(); - QVERIFY(page.spyRequest.wasCalled()); + bool permissionRequested = false, errorState = false; + connect(&page, &QWebEnginePage::featurePermissionRequested, &page, [&] (const QUrl &o, QWebEnginePage::Feature f) { + if (f != QWebEnginePage::Notifications) + return; + if (permissionRequested || o != baseUrl.url(QUrl::RemoveFilename)) { + qWarning() << "Unexpected case. Can't proceed." << setOnInit << permissionRequested << o; + errorState = true; + return; + } + permissionRequested = true; + page.setFeaturePermission(o, f, policy); + }); - QCOMPARE(page.getPermission(), permission); + if (setOnInit) + page.setFeaturePermission(baseUrl, QWebEnginePage::Notifications, policy); + + QSignalSpy spy(&page, &QWebEnginePage::loadFinished); + page.setHtml(QString("<html><body>Test</body></html>"), baseUrl); + QTRY_COMPARE(spy.count(), 1); + + QCOMPARE(evaluateJavaScriptSync(&page, QStringLiteral("Notification.permission")), setOnInit ? permission : QLatin1String("default")); + + if (!setOnInit) { + page.setFeaturePermission(baseUrl, QWebEnginePage::Notifications, policy); + QTRY_COMPARE(evaluateJavaScriptSync(&page, QStringLiteral("Notification.permission")), permission); + } + + auto js = QStringLiteral("var permission; Notification.requestPermission().then(p => { permission = p })"); + evaluateJavaScriptSync(&page, js); + QTRY_COMPARE(evaluateJavaScriptSync(&page, "permission").toString(), permission); + // permission is not 'remembered' from api standpoint, hence is not suppressed on explicit call from JS + QVERIFY(permissionRequested); + QVERIFY(!errorState); } void tst_QWebEnginePage::sendNotification() @@ -3890,16 +4076,16 @@ void tst_QWebEnginePage::setLifecycleStateWithDevTools() // Ensure pages are initialized inspectedPage.load(QStringLiteral("about:blank")); devToolsPage.load(QStringLiteral("about:blank")); - QTRY_COMPARE(inspectedSpy.count(), 1); + QTRY_COMPARE_WITH_TIMEOUT(inspectedSpy.count(), 1, 90000); QCOMPARE(inspectedSpy.takeFirst().value(0), QVariant(true)); - QTRY_COMPARE(devToolsSpy.count(), 1); + QTRY_COMPARE_WITH_TIMEOUT(devToolsSpy.count(), 1, 90000); QCOMPARE(devToolsSpy.takeFirst().value(0), QVariant(true)); // Open DevTools with Frozen inspectedPage inspectedPage.setLifecycleState(QWebEnginePage::LifecycleState::Frozen); inspectedPage.setDevToolsPage(&devToolsPage); QCOMPARE(inspectedPage.lifecycleState(), QWebEnginePage::LifecycleState::Active); - QTRY_COMPARE(devToolsSpy.count(), 1); + QTRY_COMPARE_WITH_TIMEOUT(devToolsSpy.count(), 1, 90000); QCOMPARE(devToolsSpy.takeFirst().value(0), QVariant(true)); inspectedPage.setDevToolsPage(nullptr); @@ -3907,7 +4093,7 @@ void tst_QWebEnginePage::setLifecycleStateWithDevTools() inspectedPage.setLifecycleState(QWebEnginePage::LifecycleState::Discarded); inspectedPage.setDevToolsPage(&devToolsPage); QCOMPARE(inspectedPage.lifecycleState(), QWebEnginePage::LifecycleState::Active); - QTRY_COMPARE(devToolsSpy.count(), 1); + QTRY_COMPARE_WITH_TIMEOUT(devToolsSpy.count(), 1, 90000); QCOMPARE(devToolsSpy.takeFirst().value(0), QVariant(true)); QTRY_COMPARE(inspectedSpy.count(), 1); QCOMPARE(inspectedSpy.takeFirst().value(0), QVariant(true)); @@ -3917,7 +4103,7 @@ void tst_QWebEnginePage::setLifecycleStateWithDevTools() devToolsPage.setLifecycleState(QWebEnginePage::LifecycleState::Frozen); devToolsPage.setInspectedPage(&inspectedPage); QCOMPARE(devToolsPage.lifecycleState(), QWebEnginePage::LifecycleState::Active); - QTRY_COMPARE(devToolsSpy.count(), 1); + QTRY_COMPARE_WITH_TIMEOUT(devToolsSpy.count(), 1, 90000); QCOMPARE(devToolsSpy.takeFirst().value(0), QVariant(true)); devToolsPage.setInspectedPage(nullptr); @@ -3925,8 +4111,7 @@ void tst_QWebEnginePage::setLifecycleStateWithDevTools() devToolsPage.setLifecycleState(QWebEnginePage::LifecycleState::Discarded); devToolsPage.setInspectedPage(&inspectedPage); QCOMPARE(devToolsPage.lifecycleState(), QWebEnginePage::LifecycleState::Active); - QTRY_COMPARE(devToolsSpy.count(), 2); - QCOMPARE(devToolsSpy.takeFirst().value(0), QVariant(false)); + QTRY_COMPARE_WITH_TIMEOUT(devToolsSpy.count(), 1, 90000); QCOMPARE(devToolsSpy.takeFirst().value(0), QVariant(true)); // keep DevTools open @@ -4403,7 +4588,7 @@ void tst_QWebEnginePage::customUserAgentInNewTab() QTRY_COMPARE(spy.count(), 1); QVERIFY(spy.takeFirst().value(0).toBool()); QCOMPARE(evaluateJavaScriptSync(&page, QStringLiteral("navigator.userAgent")).toString(), profile1.httpUserAgent()); - QTest::mouseClick(view.focusProxy(), Qt::LeftButton, 0, elementCenter(&page, "link")); + QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, elementCenter(&page, "link")); QTRY_VERIFY(page.newPage); QTRY_VERIFY(!lastUserAgent.isEmpty()); QCOMPARE(lastUserAgent, profile1.httpUserAgent().toUtf8()); @@ -4418,7 +4603,7 @@ void tst_QWebEnginePage::customUserAgentInNewTab() QString("'>link</a></body></html>")); QTRY_COMPARE(spy.count(), 1); QVERIFY(spy.takeFirst().value(0).toBool()); - QTest::mouseClick(view.focusProxy(), Qt::LeftButton, 0, elementCenter(&page, "link")); + QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, elementCenter(&page, "link")); QTRY_VERIFY(page.newPage); QTRY_VERIFY(!lastUserAgent.isEmpty()); QCOMPARE(lastUserAgent, profile2.httpUserAgent().toUtf8()); @@ -4444,6 +4629,139 @@ void tst_QWebEnginePage::renderProcessCrashed() status == QWebEnginePage::AbnormalTerminationStatus); } +void tst_QWebEnginePage::renderProcessPid() +{ + QCOMPARE(m_page->renderProcessPid(), 0); + + m_page->load(QUrl("about:blank")); + QSignalSpy spyFinished(m_page, &QWebEnginePage::loadFinished); + QVERIFY(spyFinished.wait()); + + QVERIFY(m_page->renderProcessPid() > 1); + + bool crashed = false; + connect(m_page, &QWebEnginePage::renderProcessTerminated, [&]() { crashed = true; }); + m_page->load(QUrl("chrome://crash")); + QTRY_VERIFY_WITH_TIMEOUT(crashed, 20000); + + QCOMPARE(m_page->renderProcessPid(), 0); +} + +void tst_QWebEnginePage::backgroundColor() +{ + QWebEngineProfile profile; + QWebEngineView view; + QWebEnginePage *page = new QWebEnginePage(&profile, &view); + + view.resize(640, 480); + view.setStyleSheet("background: yellow"); + view.show(); + QPoint center(view.size().width() / 2, view.size().height() / 2); + + QCOMPARE(page->backgroundColor(), Qt::white); + QTRY_COMPARE(view.grab().toImage().pixelColor(center), Qt::white); + + page->setBackgroundColor(Qt::red); + view.setPage(page); + + QCOMPARE(page->backgroundColor(), Qt::red); + QTRY_COMPARE(view.grab().toImage().pixelColor(center), Qt::red); + + page->setHtml(QString("<html>" + "<head><style>html, body { margin:0; padding:0; }</style></head>" + "<body><div style=\"width:100%; height:10px; background-color:black\"/></body>" + "</html>")); + QSignalSpy spyFinished(page, &QWebEnginePage::loadFinished); + QVERIFY(spyFinished.wait()); + // Make sure the page is rendered and the test is not grabbing the color of the RenderWidgetHostViewQtDelegateWidget. + QTRY_COMPARE(view.grab().toImage().pixelColor(QPoint(5, 5)), Qt::black); + + QCOMPARE(page->backgroundColor(), Qt::red); + QCOMPARE(view.grab().toImage().pixelColor(center), Qt::red); + + page->setBackgroundColor(Qt::transparent); + + QCOMPARE(page->backgroundColor(), Qt::transparent); + QTRY_COMPARE(view.grab().toImage().pixelColor(center), Qt::yellow); + + page->setBackgroundColor(Qt::green); + + QCOMPARE(page->backgroundColor(), Qt::green); + QTRY_COMPARE(view.grab().toImage().pixelColor(center), Qt::green); +} + +void tst_QWebEnginePage::audioMuted() +{ + QWebEngineProfile profile; + QWebEnginePage page(&profile); + QSignalSpy spy(&page, &QWebEnginePage::audioMutedChanged); + + QCOMPARE(page.isAudioMuted(), false); + page.setAudioMuted(true); + loadSync(&page, QUrl("about:blank")); + QCOMPARE(page.isAudioMuted(), true); + QCOMPARE(spy.count(), 1); + QCOMPARE(spy[0][0], QVariant(true)); + page.setAudioMuted(false); + QCOMPARE(page.isAudioMuted(), false); + QCOMPARE(spy.count(), 2); + QCOMPARE(spy[1][0], QVariant(false)); +} + +void tst_QWebEnginePage::closeContents() +{ + TestPage page; + QSignalSpy spyFinished(&page, &QWebEnginePage::loadFinished); + QSignalSpy windowCreatedSpy(&page, &TestPage::windowCreated); + page.setUrl(QUrl("about:blank")); + QTRY_COMPARE(spyFinished.count(), 1); + page.runJavaScript("var dialog = window.open('', '', 'width=100, height=100');"); + QTRY_COMPARE(windowCreatedSpy.count(), 1); + + QWebEngineView *dialogView = new QWebEngineView; + QWebEnginePage *dialogPage = page.createdWindows[0]; + dialogPage->setView(dialogView); + QCOMPARE(dialogPage->lifecycleState(), QWebEnginePage::LifecycleState::Active); + + // This should not crash. + connect(dialogPage, &QWebEnginePage::windowCloseRequested, dialogView, &QWebEngineView::close); + page.runJavaScript("dialog.close();"); + + // QWebEngineView::closeEvent() sets the life cycle state to discarded. + QTRY_COMPARE(dialogPage->lifecycleState(), QWebEnginePage::LifecycleState::Discarded); + delete dialogView; +} + +// Based on QTBUG-84011 +void tst_QWebEnginePage::isSafeRedirect_data() +{ + QTest::addColumn<QUrl>("requestedUrl"); + QTest::addColumn<QUrl>("expectedUrl"); + QString fileScheme = "file://"; + +#ifdef Q_OS_WIN + fileScheme += "/"; +#endif + + QString tempDir(fileScheme + QDir::tempPath()); + QTest::newRow(qPrintable(tempDir)) << QUrl(tempDir) << QUrl(tempDir + "/"); + QTest::newRow(qPrintable(tempDir + QString("/foo/bar"))) << QUrl(tempDir + "/foo/bar") << QUrl(tempDir + "/foo/bar"); + QTest::newRow("filesystem:http://foo.com/bar") << QUrl("filesystem:http://foo.com/bar") << QUrl("filesystem:http://foo.com/bar/"); +} + +void tst_QWebEnginePage::isSafeRedirect() +{ + QFETCH(QUrl, requestedUrl); + QFETCH(QUrl, expectedUrl); + + TestPage page; + QSignalSpy spy(&page, SIGNAL(loadFinished(bool))); + page.setUrl(requestedUrl); + QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 1, 20000); + QCOMPARE(page.url(), expectedUrl); + spy.clear(); +} + static QByteArrayList params = {QByteArrayLiteral("--use-fake-device-for-media-stream")}; W_QTEST_MAIN(tst_QWebEnginePage, params) diff --git a/tests/auto/widgets/qwebenginepage/tst_qwebenginepage.qrc b/tests/auto/widgets/qwebenginepage/tst_qwebenginepage.qrc index 013a307de..3480341e8 100644 --- a/tests/auto/widgets/qwebenginepage/tst_qwebenginepage.qrc +++ b/tests/auto/widgets/qwebenginepage/tst_qwebenginepage.qrc @@ -14,6 +14,7 @@ <file>resources/user.css</file> <file>resources/image.png</file> <file>resources/pasteimage.html</file> + <file>resources/redirect.html</file> <file>resources/reload.html</file> <file>resources/style.css</file> <file>resources/test1.html</file> diff --git a/tests/auto/widgets/qwebengineprofile/BLACKLIST b/tests/auto/widgets/qwebengineprofile/BLACKLIST deleted file mode 100644 index 55806eec4..000000000 --- a/tests/auto/widgets/qwebengineprofile/BLACKLIST +++ /dev/null @@ -1,3 +0,0 @@ -[disableCache] -* - diff --git a/tests/auto/widgets/qwebengineprofile/qwebengineprofile.pro b/tests/auto/widgets/qwebengineprofile/qwebengineprofile.pro index e56bbe8f7..ca16cee39 100644 --- a/tests/auto/widgets/qwebengineprofile/qwebengineprofile.pro +++ b/tests/auto/widgets/qwebengineprofile/qwebengineprofile.pro @@ -1,3 +1,4 @@ include(../tests.pri) +include(../../shared/http.pri) exists($${TARGET}.qrc):RESOURCES += $${TARGET}.qrc QT *= core-private gui-private diff --git a/tests/auto/widgets/qwebengineprofile/resources/hedgehog.html b/tests/auto/widgets/qwebengineprofile/resources/hedgehog.html new file mode 100644 index 000000000..d8abbcd48 --- /dev/null +++ b/tests/auto/widgets/qwebengineprofile/resources/hedgehog.html @@ -0,0 +1,9 @@ +<!doctype html> +<html> + <head> + <title>BREAKING NEWS: 15 Hedgehogs With Things That Look Like Hedgehogs</title> + </head> + <body> + <img src="hedgehog.png"/> + </body> +</html> diff --git a/tests/auto/widgets/qwebengineprofile/resources/hedgehog.png b/tests/auto/widgets/qwebengineprofile/resources/hedgehog.png Binary files differnew file mode 100644 index 000000000..4d56d8633 --- /dev/null +++ b/tests/auto/widgets/qwebengineprofile/resources/hedgehog.png diff --git a/tests/auto/widgets/qwebengineprofile/tst_qwebengineprofile.cpp b/tests/auto/widgets/qwebengineprofile/tst_qwebengineprofile.cpp index a7a5ba62a..1b0f95064 100644 --- a/tests/auto/widgets/qwebengineprofile/tst_qwebengineprofile.cpp +++ b/tests/auto/widgets/qwebengineprofile/tst_qwebengineprofile.cpp @@ -28,6 +28,7 @@ #include "../util.h" #include <QtCore/qbuffer.h> +#include <QtCore/qmimedatabase.h> #include <QtTest/QtTest> #include <QtWebEngineCore/qwebengineurlrequestinterceptor.h> #include <QtWebEngineCore/qwebengineurlrequestjob.h> @@ -44,6 +45,9 @@ #include <QWebChannel> #endif +#include <httpserver.h> +#include <httpreqrep.h> + #include <map> #include <mutex> @@ -67,10 +71,15 @@ private Q_SLOTS: void urlSchemeHandlerInstallation(); void urlSchemeHandlerXhrStatus(); void urlSchemeHandlerScriptModule(); + void urlSchemeHandlerLongReply(); void customUserAgent(); void httpAcceptLanguage(); void downloadItem(); void changePersistentPath(); + void changeHttpUserAgent(); + void changeHttpAcceptLanguage(); + void changeUseForGlobalCertificateVerification(); + void changePersistentCookiesPolicy(); void initiator(); void badDeleteOrder(); void qtbug_71895(); // this should be the last test @@ -82,6 +91,7 @@ void tst_QWebEngineProfile::initTestCase() QWebEngineUrlScheme stream("stream"); QWebEngineUrlScheme letterto("letterto"); QWebEngineUrlScheme aviancarrier("aviancarrier"); + QWebEngineUrlScheme myscheme("myscheme"); foo.setSyntax(QWebEngineUrlScheme::Syntax::Host); stream.setSyntax(QWebEngineUrlScheme::Syntax::HostAndPort); stream.setDefaultPort(8080); @@ -92,6 +102,7 @@ void tst_QWebEngineProfile::initTestCase() QWebEngineUrlScheme::registerScheme(stream); QWebEngineUrlScheme::registerScheme(letterto); QWebEngineUrlScheme::registerScheme(aviancarrier); + QWebEngineUrlScheme::registerScheme(myscheme); } void tst_QWebEngineProfile::init() @@ -125,12 +136,13 @@ void tst_QWebEngineProfile::privateProfile() QCOMPARE(otrProfile.httpCacheType(), QWebEngineProfile::MemoryHttpCache); QCOMPARE(otrProfile.persistentCookiesPolicy(), QWebEngineProfile::NoPersistentCookies); QCOMPARE(otrProfile.cachePath(), QString()); - QCOMPARE(otrProfile.persistentStoragePath(), QString()); + QCOMPARE(otrProfile.persistentStoragePath(), QStandardPaths::writableLocation(QStandardPaths::DataLocation) + + QStringLiteral("/QtWebEngine/OffTheRecord")); // TBD: setters do not really work otrProfile.setCachePath(QStringLiteral("/home/foo/bar")); QCOMPARE(otrProfile.cachePath(), QString()); otrProfile.setPersistentStoragePath(QStringLiteral("/home/foo/bar")); - QCOMPARE(otrProfile.persistentStoragePath(), QString()); + QCOMPARE(otrProfile.persistentStoragePath(), QStringLiteral("/home/foo/bar")); otrProfile.setHttpCacheType(QWebEngineProfile::DiskHttpCache); QCOMPARE(otrProfile.httpCacheType(), QWebEngineProfile::MemoryHttpCache); otrProfile.setPersistentCookiesPolicy(QWebEngineProfile::ForcePersistentCookies); @@ -151,75 +163,114 @@ void tst_QWebEngineProfile::testProfile() + QStringLiteral("/QtWebEngine/Test")); } -void tst_QWebEngineProfile::clearDataFromCache() +class AutoDir : public QDir { - QWebEnginePage page; +public: + AutoDir(const QString &p) : QDir(p) + { + makeAbsolute(); + removeRecursively(); + } - QDir cacheDir("./tst_QWebEngineProfile_cacheDir"); - cacheDir.makeAbsolute(); - if (cacheDir.exists()) - cacheDir.removeRecursively(); - cacheDir.mkpath(cacheDir.path()); + ~AutoDir() { removeRecursively(); } +}; +qint64 totalSize(QDir dir) +{ + qint64 sum = 0; + const QDir::Filters filters{QDir::Dirs, QDir::Files, QDir::NoSymLinks, QDir::NoDotAndDotDot}; + for (const QFileInfo &entry : dir.entryInfoList(filters)) { + if (entry.isFile()) + sum += entry.size(); + else if (entry.isDir()) + sum += totalSize(entry.filePath()); + } + return sum; +} - QWebEngineProfile *profile = page.profile(); - profile->setCachePath(cacheDir.path()); - profile->setHttpCacheType(QWebEngineProfile::DiskHttpCache); +class TestServer : public HttpServer +{ +public: + TestServer() + { + connect(this, &HttpServer::newRequest, this, &TestServer::onNewRequest); + } - QSignalSpy loadFinishedSpy(&page, SIGNAL(loadFinished(bool))); - page.load(QUrl("http://qt-project.org")); - if (!loadFinishedSpy.wait(10000) || !loadFinishedSpy.at(0).at(0).toBool()) - QSKIP("Couldn't load page from network, skipping test."); +private: + void onNewRequest(HttpReqRep *rr) + { + const QDir resourceDir(TESTS_SOURCE_DIR "qwebengineprofile/resources"); + QString path = rr->requestPath(); + path.remove(0, 1); + + if (rr->requestMethod() != "GET" || !resourceDir.exists(path)) { + rr->sendResponse(404); + return; + } - cacheDir.refresh(); - QVERIFY(cacheDir.entryList().contains("Cache")); - cacheDir.cd("./Cache"); - int filesBeforeClear = cacheDir.entryList().count(); + QFile file(resourceDir.filePath(path)); + file.open(QIODevice::ReadOnly); + QByteArray data = file.readAll(); + rr->setResponseBody(data); + QMimeDatabase db; + QMimeType mime = db.mimeTypeForFileNameAndData(file.fileName(), data); + rr->setResponseHeader(QByteArrayLiteral("content-type"), mime.name().toUtf8()); + if (!mime.inherits("text/html")) + rr->setResponseHeader(QByteArrayLiteral("cache-control"), + QByteArrayLiteral("public, max-age=31536000")); + rr->sendResponse(); + } +}; - QFileSystemWatcher fileSystemWatcher; - fileSystemWatcher.addPath(cacheDir.path()); - QSignalSpy directoryChangedSpy(&fileSystemWatcher, SIGNAL(directoryChanged(const QString &))); +void tst_QWebEngineProfile::clearDataFromCache() +{ + TestServer server; + QSignalSpy serverSpy(&server, &HttpServer::newRequest); + QVERIFY(server.start()); - // It deletes most of the files, but not all of them. - profile->clearHttpCache(); - QTest::qWait(1000); - QTRY_VERIFY(directoryChangedSpy.count() > 0); + AutoDir cacheDir("./tst_QWebEngineProfile_clearDataFromCache"); - cacheDir.refresh(); - QVERIFY(filesBeforeClear > cacheDir.entryList().count()); + QWebEngineProfile profile(QStringLiteral("Test")); + profile.setCachePath(cacheDir.path()); + profile.setHttpCacheType(QWebEngineProfile::DiskHttpCache); - cacheDir.removeRecursively(); + QWebEnginePage page(&profile); + QVERIFY(loadSync(&page, server.url("/hedgehog.html"))); + // Wait for GET /favicon.ico + QTRY_COMPARE(serverSpy.size(), 3); + + QVERIFY(cacheDir.exists("Cache")); + qint64 sizeBeforeClear = totalSize(cacheDir); + profile.clearHttpCache(); + // Wait for cache to be cleared. + QTest::qWait(1000); + QVERIFY(sizeBeforeClear > totalSize(cacheDir)); + + (void)server.stop(); } void tst_QWebEngineProfile::disableCache() { - QWebEnginePage page; - QDir cacheDir("./tst_QWebEngineProfile_cacheDir"); - if (cacheDir.exists()) - cacheDir.removeRecursively(); - cacheDir.mkpath(cacheDir.path()); + TestServer server; + QVERIFY(server.start()); + AutoDir cacheDir("./tst_QWebEngineProfile_disableCache"); + + QWebEnginePage page; QWebEngineProfile *profile = page.profile(); profile->setCachePath(cacheDir.path()); - QVERIFY(!cacheDir.entryList().contains("Cache")); + QVERIFY(!cacheDir.exists("Cache")); profile->setHttpCacheType(QWebEngineProfile::NoCache); - QSignalSpy loadFinishedSpy(&page, SIGNAL(loadFinished(bool))); - page.load(QUrl("http://qt-project.org")); - if (!loadFinishedSpy.wait(10000) || !loadFinishedSpy.at(0).at(0).toBool()) - QSKIP("Couldn't load page from network, skipping test."); - - cacheDir.refresh(); - QVERIFY(!cacheDir.entryList().contains("Cache")); + // Wait for cache to be cleared. + QTest::qWait(1000); + QVERIFY(loadSync(&page, server.url("/hedgehog.html"))); + QVERIFY(!cacheDir.exists("Cache")); profile->setHttpCacheType(QWebEngineProfile::DiskHttpCache); - page.load(QUrl("http://qt-project.org")); - if (!loadFinishedSpy.wait(10000) || !loadFinishedSpy.at(1).at(0).toBool()) - QSKIP("Couldn't load page from network, skipping test."); + QVERIFY(loadSync(&page, server.url("/hedgehog.html"))); + QVERIFY(cacheDir.exists("Cache")); - cacheDir.refresh(); - QVERIFY(cacheDir.entryList().contains("Cache")); - - cacheDir.removeRecursively(); + (void)server.stop(); } class RedirectingUrlSchemeHandler : public QWebEngineUrlSchemeHandler @@ -294,7 +345,7 @@ protected: memcpy(data, m_data.constData() + m_bytesRead, len); m_bytesAvailable -= len; m_bytesRead += len; - } else if (m_data.size() > 0) + } else if (atEnd()) return -1; return len; @@ -331,21 +382,6 @@ public: } }; -static bool loadSync(QWebEngineView *view, const QUrl &url, int timeout = 5000) -{ - // Ripped off QTRY_VERIFY. - QSignalSpy loadFinishedSpy(view, SIGNAL(loadFinished(bool))); - view->load(url); - if (loadFinishedSpy.isEmpty()) - QTest::qWait(0); - for (int i = 0; i < timeout; i += 50) { - if (!loadFinishedSpy.isEmpty()) - return true; - QTest::qWait(50); - } - return false; -} - void tst_QWebEngineProfile::urlSchemeHandlers() { RedirectingUrlSchemeHandler lettertoHandler; @@ -368,7 +404,7 @@ void tst_QWebEngineProfile::urlSchemeHandlers() // Remove the letterto scheme, and check whether it is not handled anymore. profile.removeUrlScheme("letterto"); emailAddress = QStringLiteral("kjeld@olsen-banden.dk"); - QVERIFY(loadSync(&view, QUrl(QStringLiteral("letterto:") + emailAddress))); + QVERIFY(loadSync(&view, QUrl(QStringLiteral("letterto:") + emailAddress), false)); QVERIFY(toPlainTextSync(view.page()) != emailAddress); // Check if gopher is still working after removing letterto. @@ -379,7 +415,7 @@ void tst_QWebEngineProfile::urlSchemeHandlers() // Does removeAll work? profile.removeAllUrlSchemeHandlers(); url = QUrl(QStringLiteral("gopher://olsen-banden.dk/harry")); - QVERIFY(loadSync(&view, url)); + QVERIFY(loadSync(&view, url, false)); QVERIFY(toPlainTextSync(view.page()) != url.toString()); // Install a handler that is owned by the view. Make sure this doesn't crash on shutdown. @@ -478,7 +514,7 @@ void tst_QWebEngineProfile::urlSchemeHandlerStreaming() view.setPage(new QWebEnginePage(&profile, &view)); view.settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, false); view.load(QUrl(QStringLiteral("stream://whatever"))); - QVERIFY(loadFinishedSpy.wait()); + QTRY_COMPARE_WITH_TIMEOUT(loadFinishedSpy.count(), 1, 30000); QByteArray result; result.append(1000, 'c'); QCOMPARE(toPlainTextSync(view.page()), QString::fromLatin1(result)); @@ -539,7 +575,7 @@ void tst_QWebEngineProfile::urlSchemeHandlerRequestHeaders() QWebEnginePage page(&profile); QSignalSpy loadFinishedSpy(&page, SIGNAL(loadFinished(bool))); page.load(QUrl(QStringLiteral("myscheme://whatever"))); - QVERIFY(loadFinishedSpy.wait()); + QTRY_COMPARE_WITH_TIMEOUT(loadFinishedSpy.count(), 1, 30000); } void tst_QWebEngineProfile::urlSchemeHandlerInstallation() @@ -671,7 +707,7 @@ void tst_QWebEngineProfile::urlSchemeHandlerXhrStatus() profile.installUrlSchemeHandler("aviancarrier", &handler); page.setWebChannel(&channel); page.load(QUrl("aviancarrier:/")); - QTRY_VERIFY(host.isReady()); + QTRY_VERIFY_WITH_TIMEOUT(host.isReady(), 30000); host.load(QUrl("aviancarrier:/ok")); host.load(QUrl("aviancarrier:/redirect")); host.load(QUrl("aviancarrier:/fail")); @@ -714,6 +750,31 @@ void tst_QWebEngineProfile::urlSchemeHandlerScriptModule() QCOMPARE(evaluateJavaScriptSync(&page, QStringLiteral("test")).toString(), QStringLiteral("SUCCESS")); } +class LongReplyUrlSchemeHandler : public QWebEngineUrlSchemeHandler +{ +public: + LongReplyUrlSchemeHandler(QObject *parent = nullptr) : QWebEngineUrlSchemeHandler(parent) {} + ~LongReplyUrlSchemeHandler() {} + + void requestStarted(QWebEngineUrlRequestJob *job) + { + QBuffer *buffer = new QBuffer(job); + buffer->setData(QByteArray(128 * 1024, ' ') + + "<html><head><title>Minify this!</title></head></html>"); + job->reply("text/html", buffer); + } +}; + +void tst_QWebEngineProfile::urlSchemeHandlerLongReply() +{ + LongReplyUrlSchemeHandler handler; + QWebEngineProfile profile; + profile.installUrlSchemeHandler("aviancarrier", &handler); + QWebEnginePage page(&profile); + page.load(QUrl("aviancarrier:/")); + QTRY_COMPARE(page.title(), QString("Minify this!")); +} + void tst_QWebEngineProfile::customUserAgent() { QString defaultUserAgent = QWebEngineProfile::defaultProfile()->httpUserAgent(); @@ -775,28 +836,131 @@ void tst_QWebEngineProfile::downloadItem() QWebEngineProfile testProfile; QWebEnginePage page(&testProfile); QSignalSpy downloadSpy(&testProfile, SIGNAL(downloadRequested(QWebEngineDownloadItem *))); - connect(&testProfile, &QWebEngineProfile::downloadRequested, this, [=] (QWebEngineDownloadItem *item) { item->accept(); }); page.load(QUrl::fromLocalFile(QCoreApplication::applicationFilePath())); QTRY_COMPARE(downloadSpy.count(), 1); } void tst_QWebEngineProfile::changePersistentPath() { + TestServer server; + QVERIFY(server.start()); + + AutoDir dataDir1(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + + QStringLiteral("/QtWebEngine/Test")); + AutoDir dataDir2(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + + QStringLiteral("/QtWebEngine/Test2")); + QWebEngineProfile testProfile(QStringLiteral("Test")); - const QString oldPath = testProfile.persistentStoragePath(); - QVERIFY(oldPath.endsWith(QStringLiteral("Test"))); + QCOMPARE(testProfile.persistentStoragePath(), dataDir1.path()); - // Make sure the profile has been used and the url-request-context-getter instantiated: + // Make sure the profile has been used: QWebEnginePage page(&testProfile); - QSignalSpy loadFinishedSpy(&page, SIGNAL(loadFinished(bool))); - page.load(QUrl("http://qt-project.org")); - if (!loadFinishedSpy.wait(10000) || !loadFinishedSpy.at(0).at(0).toBool()) - QSKIP("Couldn't load page from network, skipping test."); + QVERIFY(loadSync(&page, server.url("/hedgehog.html"))); // Test we do not crash (QTBUG-55322): - testProfile.setPersistentStoragePath(oldPath + QLatin1Char('2')); - const QString newPath = testProfile.persistentStoragePath(); - QVERIFY(newPath.endsWith(QStringLiteral("Test2"))); + testProfile.setPersistentStoragePath(dataDir2.path()); + QCOMPARE(testProfile.persistentStoragePath(), dataDir2.path()); + QVERIFY(loadSync(&page, server.url("/hedgehog.html"))); + QVERIFY(dataDir2.exists()); + + (void)server.stop(); +} + +void tst_QWebEngineProfile::changeHttpUserAgent() +{ + TestServer server; + QVERIFY(server.start()); + + QVector<QByteArray> userAgents; + connect(&server, &HttpServer::newRequest, [&](HttpReqRep *rr) { + if (rr->requestPath() == "/hedgehog.html") + userAgents.push_back(rr->requestHeader(QByteArrayLiteral("user-agent"))); + }); + + QWebEngineProfile profile(QStringLiteral("Test")); + std::unique_ptr<QWebEnginePage> page; + page.reset(new QWebEnginePage(&profile)); + QVERIFY(loadSync(page.get(), server.url("/hedgehog.html"))); + page.reset(); + profile.setHttpUserAgent("webturbine/42"); + page.reset(new QWebEnginePage(&profile)); + QVERIFY(loadSync(page.get(), server.url("/hedgehog.html"))); + + QCOMPARE(userAgents.size(), 2); + QCOMPARE(userAgents[1], "webturbine/42"); + QVERIFY(userAgents[0] != userAgents[1]); + + QVERIFY(server.stop()); +} + +void tst_QWebEngineProfile::changeHttpAcceptLanguage() +{ + TestServer server; + QVERIFY(server.start()); + + QVector<QByteArray> languages; + connect(&server, &HttpServer::newRequest, [&](HttpReqRep *rr) { + if (rr->requestPath() == "/hedgehog.html") + languages.push_back(rr->requestHeader(QByteArrayLiteral("accept-language"))); + }); + + QWebEngineProfile profile(QStringLiteral("Test")); + std::unique_ptr<QWebEnginePage> page; + page.reset(new QWebEnginePage(&profile)); + QVERIFY(loadSync(page.get(), server.url("/hedgehog.html"))); + page.reset(); + profile.setHttpAcceptLanguage("fi"); + page.reset(new QWebEnginePage(&profile)); + QVERIFY(loadSync(page.get(), server.url("/hedgehog.html"))); + + QCOMPARE(languages.size(), 2); + QCOMPARE(languages[1], "fi"); + QVERIFY(languages[0] != languages[1]); + + QVERIFY(server.stop()); +} + +void tst_QWebEngineProfile::changeUseForGlobalCertificateVerification() +{ + TestServer server; + QVERIFY(server.start()); + + // Check that we don't crash + + QWebEngineProfile profile(QStringLiteral("Test")); + std::unique_ptr<QWebEnginePage> page; + page.reset(new QWebEnginePage(&profile)); + QVERIFY(loadSync(page.get(), server.url("/hedgehog.html"))); + page.reset(); + profile.setUseForGlobalCertificateVerification(true); + page.reset(new QWebEnginePage(&profile)); + QVERIFY(loadSync(page.get(), server.url("/hedgehog.html"))); + // Don't check for error: there can be disconnects during GET hedgehog.png. + (void)server.stop(); +} + +void tst_QWebEngineProfile::changePersistentCookiesPolicy() +{ + TestServer server; + QVERIFY(server.start()); + + AutoDir dataDir("./tst_QWebEngineProfile_dataDir"); + + QWebEngineProfile profile(QStringLiteral("Test")); + QWebEnginePage page(&profile); + + profile.setPersistentStoragePath(dataDir.path()); + profile.setPersistentCookiesPolicy(QWebEngineProfile::NoPersistentCookies); + + QVERIFY(loadSync(&page, server.url("/hedgehog.html"))); + QVERIFY(!dataDir.exists("Cookies")); + + profile.setPersistentCookiesPolicy(QWebEngineProfile::ForcePersistentCookies); + + QVERIFY(loadSync(&page, server.url("/hedgehog.html"))); + QVERIFY(dataDir.exists("Cookies")); + + (void)server.stop(); } class InitiatorSpy : public QWebEngineUrlSchemeHandler @@ -815,26 +979,32 @@ void tst_QWebEngineProfile::initiator() InitiatorSpy handler; QWebEngineProfile profile; profile.installUrlSchemeHandler("foo", &handler); - QWebEnginePage page(&profile); + QWebEnginePage page(&profile, nullptr); QSignalSpy loadFinishedSpy(&page, SIGNAL(loadFinished(bool))); + page.load(QUrl("about:blank")); + QTRY_COMPARE(loadFinishedSpy.count(), 1); + loadFinishedSpy.clear(); // about:blank has a unique origin, so initiator should be QUrl("null") evaluateJavaScriptSync(&page, "window.location = 'foo:bar'"); - QVERIFY(loadFinishedSpy.wait()); + QTRY_COMPARE(loadFinishedSpy.count(), 1); + loadFinishedSpy.clear(); QCOMPARE(handler.initiator, QUrl("null")); page.setHtml("", QUrl("http://test:123/foo%20bar")); - QVERIFY(loadFinishedSpy.wait()); + QTRY_COMPARE(loadFinishedSpy.count(), 1); + loadFinishedSpy.clear(); // baseUrl determines the origin, so QUrl("http://test:123") evaluateJavaScriptSync(&page, "window.location = 'foo:bar'"); - QVERIFY(loadFinishedSpy.wait()); + QTRY_COMPARE(loadFinishedSpy.count(), 1); + loadFinishedSpy.clear(); QCOMPARE(handler.initiator, QUrl("http://test:123")); // Directly calling load/setUrl should have initiator QUrl(), meaning // browser-initiated, trusted. page.load(QUrl("foo:bar")); - QVERIFY(loadFinishedSpy.wait()); + QTRY_COMPARE(loadFinishedSpy.count(), 1); QCOMPARE(handler.initiator, QUrl()); } diff --git a/tests/auto/widgets/qwebenginescript/resources/title_a.html b/tests/auto/widgets/qwebenginescript/resources/title_a.html new file mode 100644 index 000000000..d1ca96eaa --- /dev/null +++ b/tests/auto/widgets/qwebenginescript/resources/title_a.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> + <head> + <title>A</title> + </head> + <body> + <p>Page A</p> + </body> +</html> diff --git a/tests/auto/widgets/qwebenginescript/resources/title_b.html b/tests/auto/widgets/qwebenginescript/resources/title_b.html new file mode 100644 index 000000000..fd1fda99e --- /dev/null +++ b/tests/auto/widgets/qwebenginescript/resources/title_b.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> + <head> + <title>B</title> + </head> + <body> + <p>Page B</p> + </body> +</html> diff --git a/tests/auto/widgets/qwebenginescript/tst_qwebenginescript.cpp b/tests/auto/widgets/qwebenginescript/tst_qwebenginescript.cpp index 2044f0df4..a690d516e 100644 --- a/tests/auto/widgets/qwebenginescript/tst_qwebenginescript.cpp +++ b/tests/auto/widgets/qwebenginescript/tst_qwebenginescript.cpp @@ -40,6 +40,8 @@ static bool verifyOrder(QStringList orderList) "Deferred" }; + if (orderList.size() != 5) + return false; if (orderList.at(4) == "load (timeout)") { if (orderList.at(3) != "Deferred") return false; @@ -593,27 +595,22 @@ void tst_QWebEngineScript::webChannelWithBadString() void tst_QWebEngineScript::matchQrcUrl() { - QWebEnginePage page; - QWebEngineView view; - view.setPage(&page); + QWebEngineProfile profile; + QWebEnginePage page(&profile); QWebEngineScript s; s.setInjectionPoint(QWebEngineScript::DocumentReady); s.setWorldId(QWebEngineScript::MainWorld); - - s.setSourceCode(QStringLiteral(R"( // ==UserScript== -// @match qrc:/*main.html +// @match qrc:/*title_b.html // ==/UserScript== document.title = 'New title'; )")); - page.scripts().insert(s); - page.load(QUrl("qrc:/resources/test_iframe_main.html")); - view.show(); - QSignalSpy spyFinished(&page, &QWebEnginePage::loadFinished); - QVERIFY(spyFinished.wait()); + loadSync(&page, QUrl("qrc:/resources/title_a.html")); + QCOMPARE(page.title(), "A"); + loadSync(&page, QUrl("qrc:/resources/title_b.html")); QCOMPARE(page.title(), "New title"); } diff --git a/tests/auto/widgets/qwebenginescript/tst_qwebenginescript.qrc b/tests/auto/widgets/qwebenginescript/tst_qwebenginescript.qrc index ada06119a..3290cb588 100644 --- a/tests/auto/widgets/qwebenginescript/tst_qwebenginescript.qrc +++ b/tests/auto/widgets/qwebenginescript/tst_qwebenginescript.qrc @@ -4,6 +4,8 @@ <file>resources/test_iframe_outer.html</file> <file>resources/test_iframe_inner.html</file> <file>resources/test_window_open.html</file> + <file>resources/title_a.html</file> + <file>resources/title_b.html</file> <file>resources/webChannelWithBadString.html</file> </qresource> </RCC> diff --git a/tests/auto/widgets/qwebenginesettings/BLACKLIST b/tests/auto/widgets/qwebenginesettings/BLACKLIST new file mode 100644 index 000000000..d4a35a76a --- /dev/null +++ b/tests/auto/widgets/qwebenginesettings/BLACKLIST @@ -0,0 +1,2 @@ +[javascriptClipboard] +ubuntu-20.04 diff --git a/tests/auto/widgets/qwebenginesettings/tst_qwebenginesettings.cpp b/tests/auto/widgets/qwebenginesettings/tst_qwebenginesettings.cpp index b4061b984..a09901e69 100644 --- a/tests/auto/widgets/qwebenginesettings/tst_qwebenginesettings.cpp +++ b/tests/auto/widgets/qwebenginesettings/tst_qwebenginesettings.cpp @@ -168,7 +168,7 @@ protected: if (isMainFrame && url.scheme().startsWith("data")) settings()->setAttribute(QWebEngineSettings::JavascriptEnabled, true); - + // TODO: note this setting is flaky, consider settings().commit() return true; } }; diff --git a/tests/auto/widgets/qwebengineview/BLACKLIST b/tests/auto/widgets/qwebengineview/BLACKLIST index 266f08886..592b47c01 100644 --- a/tests/auto/widgets/qwebengineview/BLACKLIST +++ b/tests/auto/widgets/qwebengineview/BLACKLIST @@ -1,8 +1,14 @@ [microFocusCoordinates] osx -[textSelectionOutOfInputField] +[visibilityState3] +windows + +[horizontalScrollbarTest] +osx + +[mixLangLocale:eu_ES] * -[visibilityState3] +[navigateOnDrop:file] windows diff --git a/tests/auto/widgets/qwebengineview/resources/dummy.html b/tests/auto/widgets/qwebengineview/resources/dummy.html new file mode 100644 index 000000000..9075f27c3 --- /dev/null +++ b/tests/auto/widgets/qwebengineview/resources/dummy.html @@ -0,0 +1,6 @@ +<html><head> +<title>Dummy simple page without real content</title> +<link rel='icon' href='resources/image2.png'/> +</head><body> +<a>This is test content</a> +</body></html> diff --git a/tests/auto/widgets/qwebengineview/tst_qwebengineview.cpp b/tests/auto/widgets/qwebengineview/tst_qwebengineview.cpp index 044fac9d7..b56053fd2 100644 --- a/tests/auto/widgets/qwebengineview/tst_qwebengineview.cpp +++ b/tests/auto/widgets/qwebengineview/tst_qwebengineview.cpp @@ -25,7 +25,6 @@ #include <private/qinputmethod_p.h> #include <qpainter.h> #include <qpagelayout.h> -#include <qpa/qplatforminputcontext.h> #include <qwebengineview.h> #include <qwebenginepage.h> #include <qwebenginesettings.h> @@ -39,9 +38,11 @@ #include <QLineEdit> #include <QHBoxLayout> #include <QMenu> +#include <QMimeData> #include <QQuickItem> #include <QQuickWidget> #include <QtWebEngineCore/qwebenginehttprequest.h> +#include <QScopeGuard> #include <QTcpServer> #include <QTcpSocket> #include <QStyle> @@ -60,42 +61,6 @@ do { \ QCOMPARE((__expr), __expected); \ } while (0) -static QPoint elementCenter(QWebEnginePage *page, const QString &id) -{ - const QString jsCode( - "(function(){" - " var elem = document.getElementById('" + id + "');" - " var rect = elem.getBoundingClientRect();" - " return [(rect.left + rect.right) / 2, (rect.top + rect.bottom) / 2];" - "})()"); - QVariantList rectList = evaluateJavaScriptSync(page, jsCode).toList(); - - if (rectList.count() != 2) { - qWarning("elementCenter failed."); - return QPoint(); - } - - return QPoint(rectList.at(0).toInt(), rectList.at(1).toInt()); -} - -static QRect elementGeometry(QWebEnginePage *page, const QString &id) -{ - const QString jsCode( - "(function() {" - " var elem = document.getElementById('" + id + "');" - " var rect = elem.getBoundingClientRect();" - " return [rect.left, rect.top, rect.right, rect.bottom];" - "})()"); - QVariantList coords = evaluateJavaScriptSync(page, jsCode).toList(); - - if (coords.count() != 4) { - qWarning("elementGeometry faield."); - return QRect(); - } - - return QRect(coords[0].toInt(), coords[1].toInt(), coords[2].toInt(), coords[3].toInt()); -} - QT_BEGIN_NAMESPACE namespace QTest { int Q_TESTLIB_EXPORT defaultMouseDelay(); @@ -160,6 +125,8 @@ private Q_SLOTS: void doNotBreakLayout(); void changeLocale(); + void mixLangLocale_data(); + void mixLangLocale(); void inputMethodsTextFormat_data(); void inputMethodsTextFormat(); void keyboardEvents(); @@ -194,8 +161,10 @@ private Q_SLOTS: void visibilityState(); void visibilityState2(); void visibilityState3(); + void jsKeyboardEvent_data(); void jsKeyboardEvent(); void deletePage(); + void autoDeleteOnExternalPageDelete(); void closeOpenerTab(); void switchPage(); void setPageDeletesImplicitPage(); @@ -204,6 +173,9 @@ private Q_SLOTS: void setPagePreservesExplicitPage(); void setViewPreservesExplicitPage(); void closeDiscardsPage(); + void loadAfterRendererCrashed(); + void navigateOnDrop_data(); + void navigateOnDrop(); }; // This will be called before the first test function is executed. @@ -531,7 +503,7 @@ void tst_QWebEngineView::focusInputTypes() // 'text' field QPoint textInputCenter = elementCenter(webView.page(), "textInput"); - QTest::mouseClick(webView.focusProxy(), Qt::LeftButton, 0, textInputCenter); + QTest::mouseClick(webView.focusProxy(), Qt::LeftButton, {}, textInputCenter); QTRY_COMPARE(evaluateJavaScriptSync(webView.page(), "document.activeElement.id").toString(), QStringLiteral("textInput")); VERIFY_INPUTMETHOD_HINTS(webView.focusProxy()->inputMethodHints(), Qt::ImhPreferLowercase); QVERIFY(webView.focusProxy()->testAttribute(Qt::WA_InputMethodEnabled)); @@ -539,7 +511,7 @@ void tst_QWebEngineView::focusInputTypes() // 'password' field QPoint passwordInputCenter = elementCenter(webView.page(), "passwordInput"); - QTest::mouseClick(webView.focusProxy(), Qt::LeftButton, 0, passwordInputCenter); + QTest::mouseClick(webView.focusProxy(), Qt::LeftButton, {}, passwordInputCenter); QTRY_COMPARE(evaluateJavaScriptSync(webView.page(), "document.activeElement.id").toString(), QStringLiteral("passwordInput")); VERIFY_INPUTMETHOD_HINTS(webView.focusProxy()->inputMethodHints(), (Qt::ImhSensitiveData | Qt::ImhNoPredictiveText | Qt::ImhNoAutoUppercase | Qt::ImhHiddenText)); QVERIFY(!webView.focusProxy()->testAttribute(Qt::WA_InputMethodEnabled)); @@ -547,7 +519,7 @@ void tst_QWebEngineView::focusInputTypes() // 'tel' field QPoint telInputCenter = elementCenter(webView.page(), "telInput"); - QTest::mouseClick(webView.focusProxy(), Qt::LeftButton, 0, telInputCenter); + QTest::mouseClick(webView.focusProxy(), Qt::LeftButton, {}, telInputCenter); QTRY_COMPARE(evaluateJavaScriptSync(webView.page(), "document.activeElement.id").toString(), QStringLiteral("telInput")); VERIFY_INPUTMETHOD_HINTS(webView.focusProxy()->inputMethodHints(), Qt::ImhDialableCharactersOnly); QVERIFY(webView.focusProxy()->testAttribute(Qt::WA_InputMethodEnabled)); @@ -555,7 +527,7 @@ void tst_QWebEngineView::focusInputTypes() // 'number' field QPoint numberInputCenter = elementCenter(webView.page(), "numberInput"); - QTest::mouseClick(webView.focusProxy(), Qt::LeftButton, 0, numberInputCenter); + QTest::mouseClick(webView.focusProxy(), Qt::LeftButton, {}, numberInputCenter); QTRY_COMPARE(evaluateJavaScriptSync(webView.page(), "document.activeElement.id").toString(), QStringLiteral("numberInput")); VERIFY_INPUTMETHOD_HINTS(webView.focusProxy()->inputMethodHints(), Qt::ImhFormattedNumbersOnly); QVERIFY(webView.focusProxy()->testAttribute(Qt::WA_InputMethodEnabled)); @@ -563,7 +535,7 @@ void tst_QWebEngineView::focusInputTypes() // 'email' field QPoint emailInputCenter = elementCenter(webView.page(), "emailInput"); - QTest::mouseClick(webView.focusProxy(), Qt::LeftButton, 0, emailInputCenter); + QTest::mouseClick(webView.focusProxy(), Qt::LeftButton, {}, emailInputCenter); QTRY_COMPARE(evaluateJavaScriptSync(webView.page(), "document.activeElement.id").toString(), QStringLiteral("emailInput")); VERIFY_INPUTMETHOD_HINTS(webView.focusProxy()->inputMethodHints(), Qt::ImhEmailCharactersOnly); QVERIFY(webView.focusProxy()->testAttribute(Qt::WA_InputMethodEnabled)); @@ -571,28 +543,28 @@ void tst_QWebEngineView::focusInputTypes() // 'url' field QPoint urlInputCenter = elementCenter(webView.page(), "urlInput"); - QTest::mouseClick(webView.focusProxy(), Qt::LeftButton, 0, urlInputCenter); + QTest::mouseClick(webView.focusProxy(), Qt::LeftButton, {}, urlInputCenter); QTRY_COMPARE(evaluateJavaScriptSync(webView.page(), "document.activeElement.id").toString(), QStringLiteral("urlInput")); VERIFY_INPUTMETHOD_HINTS(webView.focusProxy()->inputMethodHints(), (Qt::ImhUrlCharactersOnly | Qt::ImhNoPredictiveText | Qt::ImhNoAutoUppercase)); QVERIFY(webView.focusProxy()->testAttribute(Qt::WA_InputMethodEnabled)); QTRY_VERIFY(inputMethodQuery(Qt::ImEnabled).toBool()); // 'password' field - QTest::mouseClick(webView.focusProxy(), Qt::LeftButton, 0, passwordInputCenter); + QTest::mouseClick(webView.focusProxy(), Qt::LeftButton, {}, passwordInputCenter); QTRY_COMPARE(evaluateJavaScriptSync(webView.page(), "document.activeElement.id").toString(), QStringLiteral("passwordInput")); VERIFY_INPUTMETHOD_HINTS(webView.focusProxy()->inputMethodHints(), (Qt::ImhSensitiveData | Qt::ImhNoPredictiveText | Qt::ImhNoAutoUppercase | Qt::ImhHiddenText)); QVERIFY(!webView.focusProxy()->testAttribute(Qt::WA_InputMethodEnabled)); QTRY_COMPARE(inputMethodQuery(Qt::ImEnabled).toBool(), imeHasHiddenTextCapability); // 'text' type - QTest::mouseClick(webView.focusProxy(), Qt::LeftButton, 0, textInputCenter); + QTest::mouseClick(webView.focusProxy(), Qt::LeftButton, {}, textInputCenter); QTRY_COMPARE(evaluateJavaScriptSync(webView.page(), "document.activeElement.id").toString(), QStringLiteral("textInput")); VERIFY_INPUTMETHOD_HINTS(webView.focusProxy()->inputMethodHints(), Qt::ImhPreferLowercase); QVERIFY(webView.focusProxy()->testAttribute(Qt::WA_InputMethodEnabled)); QTRY_VERIFY(inputMethodQuery(Qt::ImEnabled).toBool()); // 'password' field - QTest::mouseClick(webView.focusProxy(), Qt::LeftButton, 0, passwordInputCenter); + QTest::mouseClick(webView.focusProxy(), Qt::LeftButton, {}, passwordInputCenter); QTRY_COMPARE(evaluateJavaScriptSync(webView.page(), "document.activeElement.id").toString(), QStringLiteral("passwordInput")); VERIFY_INPUTMETHOD_HINTS(webView.focusProxy()->inputMethodHints(), (Qt::ImhSensitiveData | Qt::ImhNoPredictiveText | Qt::ImhNoAutoUppercase | Qt::ImhHiddenText)); QVERIFY(!webView.focusProxy()->testAttribute(Qt::WA_InputMethodEnabled)); @@ -600,7 +572,7 @@ void tst_QWebEngineView::focusInputTypes() // 'text area' field QPoint textAreaCenter = elementCenter(webView.page(), "textArea"); - QTest::mouseClick(webView.focusProxy(), Qt::LeftButton, 0, textAreaCenter); + QTest::mouseClick(webView.focusProxy(), Qt::LeftButton, {}, textAreaCenter); QTRY_COMPARE(evaluateJavaScriptSync(webView.page(), "document.activeElement.id").toString(), QStringLiteral("textArea")); VERIFY_INPUTMETHOD_HINTS(webView.focusProxy()->inputMethodHints(), (Qt::ImhMultiLine | Qt::ImhPreferLowercase)); QVERIFY(webView.focusProxy()->testAttribute(Qt::WA_InputMethodEnabled)); @@ -689,12 +661,12 @@ void tst_QWebEngineView::horizontalScrollbarTest() QSignalSpy scrollSpy(view.page(), SIGNAL(scrollPositionChanged(QPointF))); // Note: The test below assumes that the layout direction is Qt::LeftToRight. - QTest::mouseClick(view.focusProxy(), Qt::LeftButton, 0, QPoint(550, 595)); + QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, QPoint(550, 595)); scrollSpy.wait(); QVERIFY(view.page()->scrollPosition().x() > 0); // Note: The test below assumes that the layout direction is Qt::LeftToRight. - QTest::mouseClick(view.focusProxy(), Qt::LeftButton, 0, QPoint(20, 595)); + QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, QPoint(20, 595)); scrollSpy.wait(); QVERIFY(view.page()->scrollPosition() == QPoint(0, 0)); } @@ -1206,7 +1178,11 @@ void tst_QWebEngineView::changeLocale() QTRY_COMPARE_WITH_TIMEOUT(loadFinishedSpyDE.count(), 1, 20000); QTRY_VERIFY(!toPlainTextSync(viewDE.page()).isEmpty()); +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + errorLines = toPlainTextSync(viewDE.page()).split(QRegularExpression("[\r\n]"), Qt::SkipEmptyParts); +#else errorLines = toPlainTextSync(viewDE.page()).split(QRegularExpression("[\r\n]"), QString::SkipEmptyParts); +#endif QCOMPARE(errorLines.first().toUtf8(), QByteArrayLiteral("Die Website ist nicht erreichbar")); QLocale::setDefault(QLocale("en")); @@ -1216,7 +1192,11 @@ void tst_QWebEngineView::changeLocale() QTRY_COMPARE_WITH_TIMEOUT(loadFinishedSpyEN.count(), 1, 20000); QTRY_VERIFY(!toPlainTextSync(viewEN.page()).isEmpty()); +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + errorLines = toPlainTextSync(viewEN.page()).split(QRegularExpression("[\r\n]"), Qt::SkipEmptyParts); +#else errorLines = toPlainTextSync(viewEN.page()).split(QRegularExpression("[\r\n]"), QString::SkipEmptyParts); +#endif QCOMPARE(errorLines.first().toUtf8(), QByteArrayLiteral("This site can\xE2\x80\x99t be reached")); // Reset error page @@ -1229,10 +1209,57 @@ void tst_QWebEngineView::changeLocale() QTRY_COMPARE_WITH_TIMEOUT(loadFinishedSpyDE.count(), 1, 20000); QTRY_VERIFY(!toPlainTextSync(viewDE.page()).isEmpty()); +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + errorLines = toPlainTextSync(viewDE.page()).split(QRegularExpression("[\r\n]"), Qt::SkipEmptyParts); +#else errorLines = toPlainTextSync(viewDE.page()).split(QRegularExpression("[\r\n]"), QString::SkipEmptyParts); +#endif QCOMPARE(errorLines.first().toUtf8(), QByteArrayLiteral("Die Website ist nicht erreichbar")); } +void tst_QWebEngineView::mixLangLocale_data() +{ + QTest::addColumn<QString>("locale"); + QTest::addColumn<QByteArray>("formattedNumber"); + QTest::newRow("en_DK") << "en-DK" << QByteArray("1.234.567.890"); + QTest::newRow("de") << "de" << QByteArray("1.234.567.890"); + QTest::newRow("de_CH") << "de-CH" << QByteArray("1’234’567’890"); + QTest::newRow("eu_ES") << "eu-ES" << QByteArray("1.234.567.890"); + QTest::newRow("hu_HU") << "hu-HU" << QByteArray("1\xC2\xA0""234\xC2\xA0""567\xC2\xA0""890"); // no-break spaces +} + +void tst_QWebEngineView::mixLangLocale() +{ + QFETCH(QString, locale); + QFETCH(QByteArray, formattedNumber); + + QLocale::setDefault(locale); + + QWebEngineView view; + QSignalSpy loadSpy(&view, &QWebEngineView::loadFinished); + + bool terminated = false; + auto sc = connect(view.page(), &QWebEnginePage::renderProcessTerminated, [&] () { terminated = true; }); + + view.load(QUrl("qrc:///resources/dummy.html")); + QTRY_VERIFY(terminated || loadSpy.count() == 1); + + QVERIFY2(!terminated, + qPrintable(QString("Locale [%1] terminated: %2, loaded: %3").arg(locale).arg(terminated).arg(loadSpy.count()))); + QVERIFY(loadSpy.first().first().toBool()); + + QString content = toPlainTextSync(view.page()); + QVERIFY2(!content.isEmpty() && content.contains("test content"), qPrintable(content)); + + QCOMPARE(evaluateJavaScriptSync(view.page(), "navigator.language").toString(), QLocale().bcp47Name()); + + if (locale == "eu-ES") + QEXPECT_FAIL("", "Basque number formatting is somehow dependent on environment", Continue); + QCOMPARE(evaluateJavaScriptSync(view.page(), "Number(1234567890).toLocaleString()").toByteArray(), formattedNumber); + + QLocale::setDefault(QLocale("en")); +} + void tst_QWebEngineView::inputMethodsTextFormat_data() { QTest::addColumn<QString>("string"); @@ -1276,6 +1303,7 @@ void tst_QWebEngineView::inputMethodsTextFormat() evaluateJavaScriptSync(view.page(), "document.getElementById('input1').focus()"); view.show(); + QVERIFY(QTest::qWaitForWindowExposed(&view)); QFETCH(QString, string); QFETCH(int, start); @@ -1465,7 +1493,7 @@ void tst_QWebEngineView::mouseClick() QVERIFY(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString().isEmpty()); textInputCenter = elementCenter(view.page(), "input"); - QTest::mouseClick(view.focusProxy(), Qt::LeftButton, 0, textInputCenter); + QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, textInputCenter); QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("input")); QCOMPARE(selectionChangedSpy.count(), 0); QVERIFY(view.focusProxy()->inputMethodQuery(Qt::ImCurrentSelection).toString().isEmpty()); @@ -1486,7 +1514,7 @@ void tst_QWebEngineView::mouseClick() QCOMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("input")); QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCurrentSelection).toString(), QStringLiteral("Company")); - QTest::mouseClick(view.focusProxy(), Qt::LeftButton, 0, textInputCenter); + QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, textInputCenter); QVERIFY(selectionChangedSpy.wait()); QCOMPARE(selectionChangedSpy.count(), 2); QVERIFY(view.focusProxy()->inputMethodQuery(Qt::ImCurrentSelection).toString().isEmpty()); @@ -1507,7 +1535,7 @@ void tst_QWebEngineView::mouseClick() QCOMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("input")); QCOMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCurrentSelection).toString(), QStringLiteral("The Qt Company")); - QTest::mouseClick(view.focusProxy(), Qt::LeftButton, 0, textInputCenter); + QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, textInputCenter); QVERIFY(selectionChangedSpy.wait()); QCOMPARE(selectionChangedSpy.count(), 3); QVERIFY(view.focusProxy()->inputMethodQuery(Qt::ImCurrentSelection).toString().isEmpty()); @@ -1536,7 +1564,11 @@ void tst_QWebEngineView::postData() QStringList lines = QString::fromLocal8Bit(rawData).split("\r\n"); // examine request +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + QStringList request = lines[0].split(" ", Qt::SkipEmptyParts); +#else QStringList request = lines[0].split(" ", QString::SkipEmptyParts); +#endif bool requestOk = request.length() > 2 && request[2].toUpper().startsWith("HTTP/") && request[0].toUpper() == "POST" @@ -1657,6 +1689,7 @@ void tst_QWebEngineView::inputFieldOverridesShortcuts() view.setHtml(QString("<html><body>" "<button id=\"btn1\" type=\"button\">push it real good</button>" "<input id=\"input1\" type=\"text\" value=\"x\">" + "<input id=\"pass1\" type=\"password\" value=\"x\">" "</body></html>")); QVERIFY(loadFinishedSpy.wait()); @@ -1668,6 +1701,11 @@ void tst_QWebEngineView::inputFieldOverridesShortcuts() "document.getElementById('input1').value").toString(); }; + auto passwordFieldValue = [&view] () -> QString { + return evaluateJavaScriptSync(view.page(), + "document.getElementById('pass1').value").toString(); + }; + // The input form is not focused. The action is triggered on pressing Shift+Delete. action->setShortcut(Qt::SHIFT + Qt::Key_Delete); QTest::keyClick(view.windowHandle(), Qt::Key_Delete, Qt::ShiftModifier); @@ -1691,8 +1729,20 @@ void tst_QWebEngineView::inputFieldOverridesShortcuts() QTRY_COMPARE(inputFieldValue(), QString("yxx")); QVERIFY(!actionTriggered); + // The password input form is focused. The action is not triggered, and the form's text changed. + evaluateJavaScriptSync(view.page(), "document.getElementById('pass1').focus();"); + QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("pass1")); + actionTriggered = false; + QTest::keyClick(view.windowHandle(), Qt::Key_Y); + QTRY_COMPARE(passwordFieldValue(), QString("yx")); + QTest::keyClick(view.windowHandle(), Qt::Key_X); + QTRY_COMPARE(passwordFieldValue(), QString("yxx")); + QVERIFY(!actionTriggered); + // The input form is focused. Make sure we don't override all short cuts. // A Ctrl-1 action is no default Qt key binding and should be triggerable. + evaluateJavaScriptSync(view.page(), "document.getElementById('input1').focus();"); + QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("input1")); action->setShortcut(Qt::CTRL + Qt::Key_1); QTest::keyClick(view.windowHandle(), Qt::Key_1, Qt::ControlModifier); QTRY_VERIFY(actionTriggered); @@ -1800,7 +1850,7 @@ void tst_QWebEngineView::softwareInputPanel() QVERIFY(loadFinishedSpy.wait()); QPoint textInputCenter = elementCenter(view.page(), "input1"); - QTest::mouseClick(view.focusProxy(), Qt::LeftButton, 0, textInputCenter); + QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, textInputCenter); QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("input1")); // This part of the test checks if the SIP (Software Input Panel) is triggered, @@ -1819,7 +1869,7 @@ void tst_QWebEngineView::softwareInputPanel() QTRY_VERIFY(!testContext.isInputPanelVisible()); testContext.hideInputPanel(); - QTest::mouseClick(view.focusProxy(), Qt::LeftButton, 0, textInputCenter); + QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, textInputCenter); QTRY_VERIFY(testContext.isInputPanelVisible()); view.setHtml("<html><body><p id='para'>nothing to input here</p></body></html>"); @@ -1827,7 +1877,7 @@ void tst_QWebEngineView::softwareInputPanel() testContext.hideInputPanel(); QPoint paraCenter = elementCenter(view.page(), "para"); - QTest::mouseClick(view.focusProxy(), Qt::LeftButton, 0, paraCenter); + QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, paraCenter); QVERIFY(!testContext.isInputPanelVisible()); @@ -1839,7 +1889,7 @@ void tst_QWebEngineView::softwareInputPanel() QVERIFY(loadFinishedSpy.wait()); QPoint btnDivCenter = elementCenter(view.page(), "btnDiv"); - QTest::mouseClick(view.focusProxy(), Qt::LeftButton, 0, btnDivCenter); + QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, btnDivCenter); QVERIFY(!testContext.isInputPanelVisible()); } @@ -1860,11 +1910,12 @@ void tst_QWebEngineView::inputContextQueryInput() " <input type='text' id='input1' value='' size='50'/>" "</body></html>"); QTRY_COMPARE(loadFinishedSpy.count(), 1); + QVERIFY(QTest::qWaitForWindowExposed(&view)); QCOMPARE(testContext.infos.count(), 0); // Set focus on an input field. QPoint textInputCenter = elementCenter(view.page(), "input1"); - QTest::mouseClick(view.focusProxy(), Qt::LeftButton, 0, textInputCenter); + QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, textInputCenter); QTRY_COMPARE(testContext.infos.count(), 2); QCOMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("input1")); foreach (const InputMethodInfo &info, testContext.infos) { @@ -2011,9 +2062,10 @@ void tst_QWebEngineView::inputMethods() " <input type='text' id='input1' style='font-family: serif' value='' maxlength='20' size='50'/>" "</body></html>"); QTRY_COMPARE(loadFinishedSpy.size(), 1); + QVERIFY(QTest::qWaitForWindowExposed(&view)); QPoint textInputCenter = elementCenter(view.page(), "input1"); - QTest::mouseClick(view.focusProxy(), Qt::LeftButton, 0, textInputCenter); + QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, textInputCenter); QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("input1")); // ImCursorRectangle @@ -2108,13 +2160,14 @@ void tst_QWebEngineView::textSelectionInInputField() " <input type='text' id='input1' value='QtWebEngine' size='50'/>" "</body></html>"); QVERIFY(loadFinishedSpy.wait()); + QVERIFY(QTest::qWaitForWindowExposed(&view)); // Tests for Selection when the Editor is NOT in Composition mode // LEFT to RIGHT selection // Mouse click event moves the current cursor to the end of the text QPoint textInputCenter = elementCenter(view.page(), "input1"); - QTest::mouseClick(view.focusProxy(), Qt::LeftButton, 0, textInputCenter); + QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, textInputCenter); QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("input1")); QTRY_COMPARE(view.focusProxy()->inputMethodQuery(Qt::ImCursorPosition).toInt(), 11); QTRY_COMPARE(view.focusProxy()->inputMethodQuery(Qt::ImAnchorPosition).toInt(), 11); @@ -2156,7 +2209,7 @@ void tst_QWebEngineView::textSelectionInInputField() // RIGHT to LEFT selection // Deselect the selection (this moves the current cursor to the end of the text) - QTest::mouseClick(view.focusProxy(), Qt::LeftButton, 0, textInputCenter); + QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, textInputCenter); QVERIFY(selectionChangedSpy.wait()); QCOMPARE(selectionChangedSpy.count(), 2); @@ -2181,6 +2234,7 @@ void tst_QWebEngineView::textSelectionInInputField() void tst_QWebEngineView::textSelectionOutOfInputField() { QWebEngineView view; + view.settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, true); view.resize(640, 480); view.show(); @@ -2190,13 +2244,14 @@ void tst_QWebEngineView::textSelectionOutOfInputField() " This is a text" "</body></html>"); QVERIFY(loadFinishedSpy.wait()); + QVERIFY(QTest::qWaitForWindowExposed(&view)); QCOMPARE(selectionChangedSpy.count(), 0); QVERIFY(!view.hasSelection()); QVERIFY(view.page()->selectedText().isEmpty()); // Simple click should not update text selection, however it updates selection bounds in Chromium - QTest::mouseClick(view.focusProxy(), Qt::LeftButton, 0, view.geometry().center()); + QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, view.geometry().center()); QCOMPARE(selectionChangedSpy.count(), 0); QVERIFY(!view.hasSelection()); QVERIFY(view.page()->selectedText().isEmpty()); @@ -2209,7 +2264,7 @@ void tst_QWebEngineView::textSelectionOutOfInputField() QCOMPARE(view.page()->selectedText(), QString("This is a text")); // Deselect text by mouse click - QTest::mouseClick(view.focusProxy(), Qt::LeftButton, 0, view.geometry().center()); + QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, view.geometry().center()); QVERIFY(selectionChangedSpy.wait()); QCOMPARE(selectionChangedSpy.count(), 2); QVERIFY(!view.hasSelection()); @@ -2238,6 +2293,7 @@ void tst_QWebEngineView::textSelectionOutOfInputField() " <input type='text' id='input1' value='QtWebEngine' size='50'/>" "</body></html>"); QVERIFY(loadFinishedSpy.wait()); + QVERIFY(QTest::qWaitForWindowExposed(&view)); QCOMPARE(selectionChangedSpy.count(), 0); QVERIFY(!view.hasSelection()); @@ -2256,7 +2312,7 @@ void tst_QWebEngineView::textSelectionOutOfInputField() // Remove selection by clicking into an input field QPoint textInputCenter = elementCenter(view.page(), "input1"); - QTest::mouseClick(view.focusProxy(), Qt::LeftButton, 0, textInputCenter); + QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, textInputCenter); QVERIFY(selectionChangedSpy.wait()); QCOMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("input1")); QCOMPARE(selectionChangedSpy.count(), 2); @@ -2271,7 +2327,7 @@ void tst_QWebEngineView::textSelectionOutOfInputField() QCOMPARE(view.page()->selectedText(), QString("QtWebEngine")); // Deselect input field's text by mouse click - QTest::mouseClick(view.focusProxy(), Qt::LeftButton, 0, view.geometry().center()); + QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, view.geometry().center()); QVERIFY(selectionChangedSpy.wait()); QCOMPARE(selectionChangedSpy.count(), 4); QVERIFY(!view.hasSelection()); @@ -2292,14 +2348,14 @@ void tst_QWebEngineView::hiddenText() QVERIFY(loadFinishedSpy.wait()); QPoint passwordInputCenter = elementCenter(view.page(), "password1"); - QTest::mouseClick(view.focusProxy(), Qt::LeftButton, 0, passwordInputCenter); + QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, passwordInputCenter); QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("password1")); QVERIFY(!view.focusProxy()->testAttribute(Qt::WA_InputMethodEnabled)); QVERIFY(view.focusProxy()->inputMethodHints() & Qt::ImhHiddenText); QPoint textInputCenter = elementCenter(view.page(), "input1"); - QTest::mouseClick(view.focusProxy(), Qt::LeftButton, 0, textInputCenter); + QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, textInputCenter); QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("input1")); QVERIFY(!(view.focusProxy()->inputMethodHints() & Qt::ImhHiddenText)); } @@ -2317,6 +2373,7 @@ void tst_QWebEngineView::emptyInputMethodEvent() " <input type='text' id='input1' value='QtWebEngine'/>" "</body></html>"); QVERIFY(loadFinishedSpy.wait()); + QVERIFY(QTest::qWaitForWindowExposed(&view)); evaluateJavaScriptSync(view.page(), "var inputEle = document.getElementById('input1'); inputEle.focus(); inputEle.select();"); QTRY_COMPARE(selectionChangedSpy.count(), 1); @@ -2365,6 +2422,7 @@ void tst_QWebEngineView::imeComposition() " <input type='text' id='input1' value='QtWebEngine inputMethod'/>" "</body></html>"); QVERIFY(loadFinishedSpy.wait()); + QVERIFY(QTest::qWaitForWindowExposed(&view)); evaluateJavaScriptSync(view.page(), "var inputEle = document.getElementById('input1'); inputEle.focus(); inputEle.select();"); QTRY_COMPARE(selectionChangedSpy.count(), 1); @@ -2582,6 +2640,7 @@ void tst_QWebEngineView::newlineInTextarea() " <textarea rows='5' cols='1' id='input1'></textarea>" "</body></html>"); QVERIFY(loadFinishedSpy.wait()); + QVERIFY(QTest::qWaitForWindowExposed(&view)); evaluateJavaScriptSync(view.page(), "var inputEle = document.getElementById('input1'); inputEle.focus(); inputEle.select();"); QTRY_VERIFY(evaluateJavaScriptSync(view.page(), "document.getElementById('input1').value").toString().isEmpty()); @@ -2706,6 +2765,7 @@ void tst_QWebEngineView::imeJSInputEvents() " <pre id='log'></pre>" "</body></html>"); QVERIFY(loadFinishedSpy.wait()); + QVERIFY(QTest::qWaitForWindowExposed(&view)); evaluateJavaScriptSync(view.page(), "document.getElementById('input').focus()"); QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("input")); @@ -2828,6 +2888,7 @@ void tst_QWebEngineView::imeCompositionQueryEvent() " <input type='text' id='input1' />" "</body></html>"); QVERIFY(loadFinishedSpy.wait()); + QVERIFY(QTest::qWaitForWindowExposed(&view)); evaluateJavaScriptSync(view.page(), "document.getElementById('input1').focus()"); QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("input1")); @@ -2917,7 +2978,7 @@ void tst_QWebEngineView::globalMouseSelection() // Deselect the selection (this moves the current cursor to the end of the text) QPoint textInputCenter = elementCenter(view.page(), "input1"); - QTest::mouseClick(view.focusProxy(), Qt::LeftButton, 0, textInputCenter); + QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, textInputCenter); QVERIFY(selectionChangedSpy.wait()); QCOMPARE(selectionChangedSpy.count(), 2); QVERIFY(QApplication::clipboard()->text(QClipboard::Selection).isEmpty()); @@ -2958,36 +3019,55 @@ void tst_QWebEngineView::noContextMenu() void tst_QWebEngineView::contextMenu_data() { QTest::addColumn<int>("childrenCount"); + QTest::addColumn<bool>("isCustomMenu"); QTest::addColumn<Qt::ContextMenuPolicy>("contextMenuPolicy"); - QTest::newRow("defaultContextMenu") << 1 << Qt::DefaultContextMenu; - QTest::newRow("customContextMenu") << 1 << Qt::CustomContextMenu; - QTest::newRow("preventContextMenu") << 0 << Qt::PreventContextMenu; + QTest::newRow("defaultContextMenu") << 1 << false << Qt::DefaultContextMenu; + QTest::newRow("customContextMenu") << 1 << true << Qt::CustomContextMenu; + QTest::newRow("preventContextMenu") << 0 << false << Qt::PreventContextMenu; } void tst_QWebEngineView::contextMenu() { QFETCH(int, childrenCount); + QFETCH(bool, isCustomMenu); QFETCH(Qt::ContextMenuPolicy, contextMenuPolicy); QWebEngineView view; + QMenu *customMenu = nullptr; if (contextMenuPolicy == Qt::CustomContextMenu) { - connect(&view, &QWebEngineView::customContextMenuRequested, [&view](const QPoint &pt) { - QMenu* menu = new QMenu(&view); - menu->addAction("Action1"); - menu->addAction("Action2"); - menu->popup(pt); + connect(&view, &QWebEngineView::customContextMenuRequested, [&view, &customMenu] (const QPoint &pt) { + Q_ASSERT(!customMenu); + customMenu = new QMenu(&view); + customMenu->addAction("Action1"); + customMenu->addAction("Action2"); + customMenu->popup(pt); }); } view.setContextMenuPolicy(contextMenuPolicy); + + // input is supposed to be skipped before first real navigation in >= 79 + QSignalSpy loadSpy(&view, &QWebEngineView::loadFinished); + view.load(QUrl("about:blank")); view.resize(640, 480); view.show(); + QTRY_COMPARE(loadSpy.count(), 1); QVERIFY(view.findChildren<QMenu *>().isEmpty()); QTest::mouseMove(view.windowHandle(), QPoint(10,10)); QTest::mouseClick(view.windowHandle(), Qt::RightButton); - QTRY_COMPARE(view.findChildren<QMenu *>().count(), childrenCount); + + // verify for zero children will always succeed, so should be tested with at least minor timeout + if (childrenCount <= 0) { + QVERIFY(!QTest::qWaitFor([&view] () { return view.findChildren<QMenu *>().count() > 0; }, 500)); + } else { + QTRY_COMPARE(view.findChildren<QMenu *>().count(), childrenCount); + if (isCustomMenu) { + QCOMPARE(view.findChildren<QMenu *>().first(), customMenu); + } + } + QCOMPARE(!!customMenu, isCustomMenu); } void tst_QWebEngineView::mouseLeave() @@ -3050,46 +3130,50 @@ void tst_QWebEngineView::webUIURLs_data() QTest::newRow("accessibility") << QUrl("chrome://accessibility") << true; QTest::newRow("appcache-internals") << QUrl("chrome://appcache-internals") << true; QTest::newRow("apps") << QUrl("chrome://apps") << false; + QTest::newRow("autofill-internals") << QUrl("chrome://autofill-internals") << false; QTest::newRow("blob-internals") << QUrl("chrome://blob-internals") << true; QTest::newRow("bluetooth-internals") << QUrl("chrome://bluetooth-internals") << false; QTest::newRow("bookmarks") << QUrl("chrome://bookmarks") << false; - QTest::newRow("cache") << QUrl("chrome://cache") << false; - QTest::newRow("chrome") << QUrl("chrome://chrome") << false; QTest::newRow("chrome-urls") << QUrl("chrome://chrome-urls") << false; QTest::newRow("components") << QUrl("chrome://components") << false; + QTest::newRow("conversion-internals") << QUrl("chrome://conversion-internals") << true; QTest::newRow("crashes") << QUrl("chrome://crashes") << false; QTest::newRow("credits") << QUrl("chrome://credits") << false; QTest::newRow("device-log") << QUrl("chrome://device-log") << false; QTest::newRow("devices") << QUrl("chrome://devices") << false; QTest::newRow("dino") << QUrl("chrome://dino") << false; // It works but this is an error page - QTest::newRow("dns") << QUrl("chrome://dns") << false; + QTest::newRow("discards") << QUrl("chrome://discards") << false; + QTest::newRow("download-internals") << QUrl("chrome://download-internals") << false; QTest::newRow("downloads") << QUrl("chrome://downloads") << false; QTest::newRow("extensions") << QUrl("chrome://extensions") << false; QTest::newRow("flags") << QUrl("chrome://flags") << false; - QTest::newRow("flash") << QUrl("chrome://flash") << false; QTest::newRow("gcm-internals") << QUrl("chrome://gcm-internals") << false; QTest::newRow("gpu") << QUrl("chrome://gpu") << true; QTest::newRow("help") << QUrl("chrome://help") << false; QTest::newRow("histograms") << QUrl("chrome://histograms") << true; + QTest::newRow("history") << QUrl("chrome://history") << false; QTest::newRow("indexeddb-internals") << QUrl("chrome://indexeddb-internals") << true; QTest::newRow("inspect") << QUrl("chrome://inspect") << false; + QTest::newRow("interstitials") << QUrl("chrome://interstitials") << false; + QTest::newRow("interventions-internals") << QUrl("chrome://interventions-internals") << false; QTest::newRow("invalidations") << QUrl("chrome://invalidations") << false; QTest::newRow("linux-proxy-config") << QUrl("chrome://linux-proxy-config") << false; QTest::newRow("local-state") << QUrl("chrome://local-state") << false; + QTest::newRow("management") << QUrl("chrome://management") << false; + QTest::newRow("media-engagement") << QUrl("chrome://media-engagement") << false; QTest::newRow("media-internals") << QUrl("chrome://media-internals") << true; QTest::newRow("net-export") << QUrl("chrome://net-export") << false; - QTest::newRow("net-internals") << QUrl("chrome://net-internals") << false; + QTest::newRow("net-internals") << QUrl("chrome://net-internals") << true; QTest::newRow("network-error") << QUrl("chrome://network-error") << false; QTest::newRow("network-errors") << QUrl("chrome://network-errors") << true; - QTest::newRow("newtab") << QUrl("chrome://newtab") << false; QTest::newRow("ntp-tiles-internals") << QUrl("chrome://ntp-tiles-internals") << false; QTest::newRow("omnibox") << QUrl("chrome://omnibox") << false; QTest::newRow("password-manager-internals") << QUrl("chrome://password-manager-internals") << false; QTest::newRow("policy") << QUrl("chrome://policy") << false; QTest::newRow("predictors") << QUrl("chrome://predictors") << false; + QTest::newRow("prefs-internals") << QUrl("chrome://prefs-internals") << false; QTest::newRow("print") << QUrl("chrome://print") << false; QTest::newRow("process-internals") << QUrl("chrome://process-internals") << true; - QTest::newRow("profiler") << QUrl("chrome://profiler") << false; QTest::newRow("quota-internals") << QUrl("chrome://quota-internals") << true; QTest::newRow("safe-browsing") << QUrl("chrome://safe-browsing") << false; #ifdef Q_OS_LINUX @@ -3106,14 +3190,14 @@ void tst_QWebEngineView::webUIURLs_data() QTest::newRow("sync-internals") << QUrl("chrome://sync-internals") << false; QTest::newRow("system") << QUrl("chrome://system") << false; QTest::newRow("terms") << QUrl("chrome://terms") << false; - QTest::newRow("thumbnails") << QUrl("chrome://thumbnails") << false; - QTest::newRow("tracing") << QUrl("chrome://tracing") << false; + QTest::newRow("tracing") << QUrl("chrome://tracing") << true; QTest::newRow("translate-internals") << QUrl("chrome://translate-internals") << false; + QTest::newRow("ukm") << QUrl("chrome://ukm") << true; QTest::newRow("usb-internals") << QUrl("chrome://usb-internals") << false; - QTest::newRow("user-actions") << QUrl("chrome://user-actions") << false; + QTest::newRow("user-actions") << QUrl("chrome://user-actions") << true; QTest::newRow("version") << QUrl("chrome://version") << false; QTest::newRow("webrtc-internals") << QUrl("chrome://webrtc-internals") << true; - QTest::newRow("webrtc-logs") << QUrl("chrome://webrtc-logs") << false; + QTest::newRow("webrtc-logs") << QUrl("chrome://webrtc-logs") << true; } void tst_QWebEngineView::webUIURLs() @@ -3174,6 +3258,28 @@ void tst_QWebEngineView::visibilityState3() QCOMPARE(evaluateJavaScriptSync(&page2, "document.visibilityState").toString(), QStringLiteral("visible")); } +void tst_QWebEngineView::jsKeyboardEvent_data() +{ + QTest::addColumn<char>("key"); + QTest::addColumn<Qt::KeyboardModifiers>("modifiers"); + QTest::addColumn<QString>("expected"); + +#if defined(Q_OS_MACOS) + // See Qt::AA_MacDontSwapCtrlAndMeta + Qt::KeyboardModifiers controlModifier = Qt::MetaModifier; +#else + Qt::KeyboardModifiers controlModifier = Qt::ControlModifier; +#endif + + QTest::newRow("Ctrl+Shift+A") << 'A' << (controlModifier | Qt::ShiftModifier) << QStringLiteral( + "16,ShiftLeft,Shift,false,true,false;" + "17,ControlLeft,Control,true,true,false;" + "65,KeyA,A,true,true,false;"); + QTest::newRow("Ctrl+z") << 'z' << controlModifier << QStringLiteral( + "17,ControlLeft,Control,true,false,false;" + "90,KeyZ,z,true,false,false;"); +} + void tst_QWebEngineView::jsKeyboardEvent() { QWebEngineView view; @@ -3183,18 +3289,13 @@ void tst_QWebEngineView::jsKeyboardEvent() "addEventListener('keydown', (ev) => {" " log += [ev.keyCode, ev.code, ev.key, ev.ctrlKey, ev.shiftKey, ev.altKey].join(',') + ';';" "});"); + + QFETCH(char, key); + QFETCH(Qt::KeyboardModifiers, modifiers); + QFETCH(QString, expected); + // Note that this only tests the fallback code path where native scan codes are not used. -#if defined(Q_OS_MACOS) - // See Qt::AA_MacDontSwapCtrlAndMeta - QTest::keyClick(view.focusProxy(), 'A', Qt::MetaModifier | Qt::ShiftModifier); -#else - QTest::keyClick(view.focusProxy(), 'A', Qt::ControlModifier | Qt::ShiftModifier); -#endif - QString expected = QStringLiteral( - "16,ShiftLeft,Shift,false,true,false;" - "17,ControlLeft,Control,true,true,false;" - "65,KeyA,A,true,true,false;" - ); + QTest::keyClick(view.focusProxy(), key, modifiers); QTRY_VERIFY(evaluateJavaScriptSync(view.page(), "log") != QVariant(QString())); QCOMPARE(evaluateJavaScriptSync(view.page(), "log"), expected); } @@ -3213,6 +3314,26 @@ void tst_QWebEngineView::deletePage() QTRY_VERIFY(spy.count()); } +void tst_QWebEngineView::autoDeleteOnExternalPageDelete() +{ + QPointer<QWebEngineView> view = new QWebEngineView; + QPointer<QWebEnginePage> page = new QWebEnginePage; + auto sg = qScopeGuard([&] () { delete view; delete page; }); + + QSignalSpy spy(page, &QWebEnginePage::loadFinished); + view->setPage(page); + view->show(); + view->resize(320, 240); + page->load(QUrl("about:blank")); + QTRY_VERIFY(spy.count()); + QVERIFY(page->parent() != view); + + auto sc = QObject::connect(page, &QWebEnginePage::destroyed, view, &QWebEngineView::deleteLater); + QTimer::singleShot(0, page, &QObject::deleteLater); + QTRY_VERIFY(!page); + QTRY_VERIFY(!view); +} + class TestView : public QWebEngineView { Q_OBJECT public: @@ -3342,5 +3463,58 @@ void tst_QWebEngineView::closeDiscardsPage() QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Discarded); } + +void tst_QWebEngineView::loadAfterRendererCrashed() +{ + QWebEngineView view; + view.resize(640, 480); + view.show(); + QVERIFY(QTest::qWaitForWindowExposed(&view)); + + bool terminated = false; + connect(view.page(), &QWebEnginePage::renderProcessTerminated, [&] () { terminated = true; }); + view.load(QUrl("chrome://crash")); + QTRY_VERIFY_WITH_TIMEOUT(terminated, 30000); + + QSignalSpy loadSpy(&view, &QWebEngineView::loadFinished); + view.load(QUrl("qrc:///resources/dummy.html")); + QTRY_COMPARE(loadSpy.count(), 1); + QVERIFY(loadSpy.first().first().toBool()); +} + +void tst_QWebEngineView::navigateOnDrop_data() +{ + QTest::addColumn<QUrl>("url"); + QTest::newRow("file") << QUrl::fromLocalFile(QDir(TESTS_SOURCE_DIR).absoluteFilePath("qwebengineview/resources/dummy.html")); + QTest::newRow("qrc") << QUrl("qrc:///resources/dummy.html"); +} + +void tst_QWebEngineView::navigateOnDrop() +{ + QFETCH(QUrl, url); + struct WebEngineView : QWebEngineView { + QWebEngineView* createWindow(QWebEnginePage::WebWindowType /* type */) override { return this; } + } view; + view.resize(640, 480); + view.show(); + QVERIFY(QTest::qWaitForWindowExposed(&view)); + + QSignalSpy loadSpy(&view, &QWebEngineView::loadFinished); + QMimeData mimeData; + mimeData.setUrls({ url }); + + auto sendEvents = [&] () { + QDragEnterEvent dee(view.rect().center(), Qt::CopyAction, &mimeData, Qt::LeftButton, Qt::NoModifier); + QApplication::sendEvent(&view, &dee); + QDropEvent de(view.rect().center(), Qt::CopyAction, &mimeData, Qt::LeftButton, Qt::NoModifier); + QApplication::sendEvent(&view, &de); + }; + + sendEvents(); + QTRY_COMPARE(loadSpy.count(), 1); + QVERIFY(loadSpy.first().first().toBool()); + QCOMPARE(view.url(), url); +} + QTEST_MAIN(tst_QWebEngineView) #include "tst_qwebengineview.moc" diff --git a/tests/auto/widgets/qwebengineview/tst_qwebengineview.qrc b/tests/auto/widgets/qwebengineview/tst_qwebengineview.qrc index a09be0399..a0e81e242 100644 --- a/tests/auto/widgets/qwebengineview/tst_qwebengineview.qrc +++ b/tests/auto/widgets/qwebengineview/tst_qwebengineview.qrc @@ -6,5 +6,6 @@ <file>resources/scrolltest_page.html</file> <file>resources/keyboardEvents.html</file> <file>resources/image2.png</file> + <file>resources/dummy.html</file> </qresource> </RCC> diff --git a/tests/auto/widgets/spellchecking/tst_spellchecking.cpp b/tests/auto/widgets/spellchecking/tst_spellchecking.cpp index 64df05d89..801e2a76c 100644 --- a/tests/auto/widgets/spellchecking/tst_spellchecking.cpp +++ b/tests/auto/widgets/spellchecking/tst_spellchecking.cpp @@ -41,10 +41,10 @@ public: void activateMenu(QWidget *widget, const QPoint &position) { QTest::mouseMove(widget, position); - QTest::mousePress(widget, Qt::RightButton, 0, position); + QTest::mousePress(widget, Qt::RightButton, {}, position); QContextMenuEvent evcont(QContextMenuEvent::Mouse, position, mapToGlobal(position)); event(&evcont); - QTest::mouseRelease(widget, Qt::RightButton, 0, position); + QTest::mouseRelease(widget, Qt::RightButton, {}, position); } const QWebEngineContextMenuData& data() @@ -175,8 +175,8 @@ void tst_Spellchecking::spellcheck() //type text, spellchecker needs time QTest::mouseMove(m_view->focusWidget(), QPoint(20,20)); - QTest::mousePress(m_view->focusWidget(), Qt::LeftButton, 0, QPoint(20,20)); - QTest::mouseRelease(m_view->focusWidget(), Qt::LeftButton, 0, QPoint(20,20)); + QTest::mousePress(m_view->focusWidget(), Qt::LeftButton, {}, QPoint(20,20)); + QTest::mouseRelease(m_view->focusWidget(), Qt::LeftButton, {}, QPoint(20,20)); QString text("I lowe Qt ...."); for (int i = 0; i < text.length(); i++) { QTest::keyClicks(m_view->focusWidget(), text.at(i)); diff --git a/tests/auto/widgets/touchinput/BLACKLIST b/tests/auto/widgets/touchinput/BLACKLIST new file mode 100644 index 000000000..d9e06df8c --- /dev/null +++ b/tests/auto/widgets/touchinput/BLACKLIST @@ -0,0 +1,2 @@ +[touchTap] +sles-15.4 # QTBUG-106334 diff --git a/tests/auto/widgets/touchinput/touchinput.pro b/tests/auto/widgets/touchinput/touchinput.pro new file mode 100644 index 000000000..d91c0074b --- /dev/null +++ b/tests/auto/widgets/touchinput/touchinput.pro @@ -0,0 +1,2 @@ +include(../tests.pri) +QT *= gui-private diff --git a/tests/auto/widgets/touchinput/tst_touchinput.cpp b/tests/auto/widgets/touchinput/tst_touchinput.cpp new file mode 100644 index 000000000..3c7d8ccbb --- /dev/null +++ b/tests/auto/widgets/touchinput/tst_touchinput.cpp @@ -0,0 +1,349 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtWebEngine module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "../util.h" + +#include <QtGui/qpa/qwindowsysteminterface.h> +#include <QSignalSpy> +#include <QTest> +#include <QTouchDevice> +#include <QWebEngineSettings> +#include <QWebEngineView> + +static QTouchDevice* s_touchDevice = nullptr; + +class TouchInputTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + +private Q_SLOTS: + void touchTap(); + void touchTapAndHold(); + void touchTapAndHoldCancelled(); + void scrolling(); + void pinchZoom_data(); + void pinchZoom(); + void complexSequence(); + +private: + QWebEngineView view; + QSignalSpy loadSpy { &view, &QWebEngineView::loadFinished }; + QPoint notextCenter, textCenter, inputCenter; + + QString activeElement() { return evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(); } + + void gestureScroll(bool down) { + auto target = view.focusProxy(); + QPoint p(target->width() / 2, target->height() / 4 * (down ? 3 : 1)); + + QTest::touchEvent(target, s_touchDevice).press(42, p, target); + + QSignalSpy spy(view.page(), &QWebEnginePage::scrollPositionChanged); + for (int i = 0; i < 3; ++i) { + down ? p -= QPoint(5, 15) : p += QPoint(5, 15); + QTest::qWait(100); // too fast and events are recognized as fling gesture + QTest::touchEvent(target, s_touchDevice).move(42, p, target); + spy.wait(); + } + + QTest::touchEvent(target, s_touchDevice).release(42, p, target); + } + + void gesturePinch(bool zoomIn, bool tapOneByOne = false) { + auto target = view.focusProxy(); + QPoint p(target->width() / 2, target->height() / 2); + auto t1 = p - QPoint(zoomIn ? 50 : 150, 10), t2 = p + QPoint(zoomIn ? 50 : 150, 10); + + if (tapOneByOne) { + QTest::touchEvent(target, s_touchDevice).press(42, t1, target); + QTest::touchEvent(target, s_touchDevice).stationary(42).press(24, t2, target); + } else { + QTest::touchEvent(target, s_touchDevice).press(42, t1, target).press(24, t2, target); + } + + for (int i = 0; i < 3; ++i) { + if (zoomIn) { + t1 -= QPoint(25, 5); + t2 += QPoint(25, 5); + } else { + t1 += QPoint(35, 5); + t2 -= QPoint(35, 5); + } + QTest::qWait(100); // too fast and events are recognized as fling gesture + QTest::touchEvent(target, s_touchDevice).move(24, t1, target).move(42, t2, target); + } + + if (tapOneByOne) { + QTest::touchEvent(target, s_touchDevice).stationary(42).release(24, t2, target); + QTest::touchEvent(target, s_touchDevice).release(42, t1, target); + } else { + QTest::touchEvent(target, s_touchDevice).release(42, t1, target).release(24, t2, target); + } + } + + int getScrollPosition(int *position = nullptr) { + int p = evaluateJavaScriptSync(view.page(), "window.scrollY").toInt(); + return position ? (*position = p) : p; + } + + int pageScrollPosition() { + // this one is updated later in page in asynchronous way + return qRound(view.page()->scrollPosition().y()); + } + + double getScaleFactor(double *scale = nullptr) { + double s = evaluateJavaScriptSync(view.page(), "window.visualViewport.scale").toDouble(); + return scale ? (*scale = s) : s; + } +}; + +void TouchInputTest::initTestCase() +{ + s_touchDevice = QTest::createTouchDevice(); + + view.settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, false); + + view.show(); view.resize(480, 320); + QVERIFY(QTest::qWaitForWindowExposed(&view)); + + view.setHtml("<html><head><style>.rect { min-width: 240px; min-height: 120px; }</style></head><body>" + "<p id='text' style='width: 150px;'>The Qt Company</p>" + "<div id='notext' style='width: 150px; height: 100px; background-color: #f00;'></div>" + "<form><input id='input' width='150px' type='text' value='The Qt Company2' /></form>" + "<table style='width: 100%; padding: 15px; text-align: center;'>" + "<tr><td>BEFORE</td><td><div class='rect' style='background-color: #00f;'></div></td><td>AFTER</td></tr>" + "<tr><td>BEFORE</td><td><div class='rect' style='background-color: #0f0;'></div></td><td>AFTER</td></tr>" + "<tr><td>BEFORE</td><td><div class='rect' style='background-color: #f00;'></div></td><td>AFTER</td></tr></table>" + "</body></html>"); + QVERIFY(loadSpy.wait() && loadSpy.first().first().toBool()); + + notextCenter = elementCenter(view.page(), "notext"); + textCenter = elementCenter(view.page(), "text"); + inputCenter = elementCenter(view.page(), "input"); +} + +void TouchInputTest::init() +{ + QCOMPARE(activeElement(), QString()); +} + +void TouchInputTest::cleanup() +{ + evaluateJavaScriptSync(view.page(), "if (document.activeElement) document.activeElement.blur()"); + evaluateJavaScriptSync(view.page(), "window.scrollTo(0, 0)"); + QTRY_COMPARE(getScrollPosition(), 0); + QTRY_COMPARE(pageScrollPosition(), 0); +} + +void TouchInputTest::touchTap() +{ + auto singleTap = [target = view.focusProxy()] (const QPoint& tapCoords) -> void { + QTest::touchEvent(target, s_touchDevice).press(1, tapCoords, target); + QTest::touchEvent(target, s_touchDevice).stationary(1); + QTest::touchEvent(target, s_touchDevice).release(1, tapCoords, target); + }; + + // Single tap on text doesn't trigger a selection + singleTap(textCenter); + QTRY_COMPARE(activeElement(), QString()); + QTRY_VERIFY(!view.hasSelection()); + + // Single tap inside the input field focuses it without selecting the text + singleTap(inputCenter); + QTRY_COMPARE(activeElement(), QStringLiteral("input")); + QTRY_VERIFY(!view.hasSelection()); + + // Single tap on the div clears the input field focus + singleTap(notextCenter); + QTRY_COMPARE(activeElement(), QString()); + + // Double tap on text still doesn't trigger a selection + singleTap(textCenter); + singleTap(textCenter); + QTRY_COMPARE(activeElement(), QString()); + QTRY_VERIFY(!view.hasSelection()); + + // Double tap inside the input field focuses it and selects the word under it + singleTap(inputCenter); + singleTap(inputCenter); + QTRY_COMPARE(activeElement(), QStringLiteral("input")); + QTRY_COMPARE(view.selectedText(), QStringLiteral("Company2")); + + // Double tap outside the input field behaves like a single tap: clears its focus and selection + singleTap(notextCenter); + singleTap(notextCenter); + QTRY_COMPARE(activeElement(), QString()); + QTRY_VERIFY(!view.hasSelection()); +} + +void TouchInputTest::touchTapAndHold() +{ + auto tapAndHold = [target = view.focusProxy()] (const QPoint& tapCoords) -> void { + QTest::touchEvent(target, s_touchDevice).press(1, tapCoords, target); + QTest::touchEvent(target, s_touchDevice).stationary(1); + QTest::qWait(1000); + QTest::touchEvent(target, s_touchDevice).release(1, tapCoords, target); + }; + + // Tap-and-hold on text selects the word under it + tapAndHold(textCenter); + QTRY_COMPARE(activeElement(), QString()); + QTRY_COMPARE(view.selectedText(), QStringLiteral("Company")); + + // Tap-and-hold inside the input field focuses it and selects the word under it + tapAndHold(inputCenter); + QTRY_COMPARE(activeElement(), QStringLiteral("input")); + QTRY_COMPARE(view.selectedText(), QStringLiteral("Company2")); + + // Only test the page context menu on Windows, as Linux doesn't handle context menus consistently + // and other non-desktop platforms like Android may not even support context menus at all +#if defined(Q_OS_WIN) + // Tap-and-hold clears the text selection and shows the page's context menu + QVERIFY(QApplication::activePopupWidget() == nullptr); + tapAndHold(notextCenter); + QTRY_COMPARE(activeElement(), QString()); + QTRY_VERIFY(!view.hasSelection()); + QTRY_VERIFY(QApplication::activePopupWidget() != nullptr); + + QApplication::activePopupWidget()->close(); + QVERIFY(QApplication::activePopupWidget() == nullptr); +#endif +} + +void TouchInputTest::touchTapAndHoldCancelled() +{ + auto cancelledTapAndHold = [target = view.focusProxy()] (const QPoint& tapCoords) -> void { + QTest::touchEvent(target, s_touchDevice).press(1, tapCoords, target); + QTest::touchEvent(target, s_touchDevice).stationary(1); + QTest::qWait(1000); + QWindowSystemInterface::handleTouchCancelEvent(target->windowHandle(), s_touchDevice); + }; + + // A cancelled tap-and-hold should cancel text selection, but currently doesn't + cancelledTapAndHold(textCenter); + QEXPECT_FAIL("", "Incorrect Chromium selection behavior when cancelling tap-and-hold on text", Continue); + QTRY_VERIFY_WITH_TIMEOUT(!view.hasSelection(), 100); + + // A cancelled tap-and-hold should cancel input field focusing and selection, but currently doesn't + cancelledTapAndHold(inputCenter); + QEXPECT_FAIL("", "Incorrect Chromium selection behavior when cancelling tap-and-hold on input field", Continue); + QTRY_VERIFY_WITH_TIMEOUT(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString().isEmpty(), 100); + QEXPECT_FAIL("", "Incorrect Chromium focus behavior when cancelling tap-and-hold on input field", Continue); + QTRY_VERIFY_WITH_TIMEOUT(!view.hasSelection(), 100); + + // Only test the page context menu on Windows, as Linux doesn't handle context menus consistently + // and other non-desktop platforms like Android may not even support context menus at all +#if defined(Q_OS_WIN) + // A cancelled tap-and-hold cancels the context menu + QVERIFY(QApplication::activePopupWidget() == nullptr); + cancelledTapAndHold(notextCenter); + QVERIFY(QApplication::activePopupWidget() == nullptr); +#endif +} + +void TouchInputTest::scrolling() +{ + int p = getScrollPosition(); + QCOMPARE(p, 0); + + // scroll a bit down... + for (int i = 0; i < 3; ++i) { + gestureScroll(/* down = */true); + int positionBefore = p; + QTRY_VERIFY2(getScrollPosition(&p) > positionBefore, qPrintable(QString("i: %1, position: %2 -> %3").arg(i).arg(positionBefore).arg(p))); + } + + // ... and then scroll page again but in opposite direction + for (int i = 0; i < 3; ++i) { + gestureScroll(/* down = */false); + int positionBefore = p; + QTRY_VERIFY2(getScrollPosition(&p) < positionBefore, qPrintable(QString("i: %1, position: %2 -> %3").arg(i).arg(positionBefore).arg(p))); + } + + QTRY_COMPARE(getScrollPosition(), 0); +} + +void TouchInputTest::pinchZoom_data() +{ + QTest::addColumn<bool>("tapOneByOne"); + QTest::addRow("sequential") << true; + QTest::addRow("simultaneous") << false; +} + +void TouchInputTest::pinchZoom() +{ + QFETCH(bool, tapOneByOne); + double scale = getScaleFactor(); + QCOMPARE(scale, 1.0); + + for (int i = 0; i < 3; ++i) { + gesturePinch(/* zoomIn = */true, tapOneByOne); + QTRY_VERIFY2(getScaleFactor(&scale) > 1.5, qPrintable(QString("i: %1, scale: %2").arg(i).arg(scale))); + gesturePinch(/* zoomIn = */false, tapOneByOne); + QTRY_COMPARE(getScaleFactor(&scale), 1.0); + } +} + +void TouchInputTest::complexSequence() +{ + auto t = view.focusProxy(); + QPoint pc(view.width() / 2, view.height() / 2), p1 = pc - QPoint(50, 25), p2 = pc + QPoint(50, 25); + + for (int i = 0; i < 4; ++i) { + QTest::touchEvent(t, s_touchDevice).press(42, p1, t); QTest::qWait(50); + QTest::touchEvent(t, s_touchDevice).stationary(42).press(24, p2, t); QTest::qWait(50); + QTest::touchEvent(t, s_touchDevice).release(42, p1, t).release(24, p2, t); + + // for additional variablity add zooming in on even steps and zooming out on odd steps + // MEMO scroll position will always be 0 while viewport scale factor > 1.0, so do zoom in after scroll + bool zoomIn = i % 2 == 0; + + if (!zoomIn) { + gesturePinch(false); + QTRY_COMPARE(getScaleFactor(), 1.0); + } + + int p = getScrollPosition(), positionBefore = p; + gestureScroll(true); + QTRY_VERIFY2_WITH_TIMEOUT(getScrollPosition(&p) > positionBefore, qPrintable(QString("i: %1, position: %2 -> %3").arg(i).arg(positionBefore).arg(p)), 1000); + + if (zoomIn) { + double s = getScaleFactor(), scaleBefore = s; + gesturePinch(true); + QTRY_VERIFY2(getScaleFactor(&s) > scaleBefore, qPrintable(QString("i: %1, scale: %2").arg(i).arg(s))); + } + } +} + +QTEST_MAIN(TouchInputTest) +#include "tst_touchinput.moc" diff --git a/tests/auto/widgets/util.h b/tests/auto/widgets/util.h index eba974f33..461baf9ac 100644 --- a/tests/auto/widgets/util.h +++ b/tests/auto/widgets/util.h @@ -36,11 +36,23 @@ #include <QSignalSpy> #include <QTimer> #include <qwebenginepage.h> +#include <qwebengineview.h> #if !defined(TESTS_SOURCE_DIR) #define TESTS_SOURCE_DIR "" #endif +// Disconnect signal on destruction. +class ScopedConnection +{ +public: + ScopedConnection(QMetaObject::Connection connection) : m_connection(std::move(connection)) { } + ~ScopedConnection() { QObject::disconnect(m_connection); } + +private: + QMetaObject::Connection m_connection; +}; + /** * Just like QSignalSpy but facilitates sync and async * signal emission. For example if you want to verify that @@ -83,10 +95,13 @@ public: QObject::connect(&timeoutTimer, SIGNAL(timeout()), &eventLoop, SLOT(quit())); } - T waitForResult() { - if (!called) { - timeoutTimer.start(20000); + T waitForResult(int timeout = 20000) { + const int step = 1000; + int elapsed = 0; + while (elapsed < timeout && !called) { + timeoutTimer.start(step); eventLoop.exec(); + elapsed += step; } return result; } @@ -132,7 +147,7 @@ static inline QString toHtmlSync(QWebEnginePage *page) static inline bool findTextSync(QWebEnginePage *page, const QString &subString) { CallbackSpy<bool> spy; - page->findText(subString, 0, spy.ref()); + page->findText(subString, {}, spy.ref()); return spy.waitForResult(); } @@ -157,6 +172,55 @@ static inline QUrl baseUrlSync(QWebEnginePage *page) return spy.waitForResult().toUrl(); } +static inline bool loadSync(QWebEnginePage *page, const QUrl &url, bool ok = true) +{ + QSignalSpy spy(page, &QWebEnginePage::loadFinished); + page->load(url); + return (!spy.empty() || spy.wait(20000)) && (spy.front().value(0).toBool() == ok); +} + +static inline bool loadSync(QWebEngineView *view, const QUrl &url, bool ok = true) +{ + return loadSync(view->page(), url, ok); +} + +static inline QPoint elementCenter(QWebEnginePage *page, const QString &id) +{ + const QString jsCode( + "(function(){" + " var elem = document.getElementById('" + id + "');" + " var rect = elem.getBoundingClientRect();" + " return [(rect.left + rect.right) / 2, (rect.top + rect.bottom) / 2];" + "})()"); + QVariantList rectList = evaluateJavaScriptSync(page, jsCode).toList(); + + if (rectList.count() != 2) { + qWarning("elementCenter failed."); + return QPoint(); + } + + return QPoint(rectList.at(0).toInt(), rectList.at(1).toInt()); +} + +static inline QRect elementGeometry(QWebEnginePage *page, const QString &id) +{ + const QString jsCode( + "(function() {" + " var elem = document.getElementById('" + id + "');" + " var rect = elem.getBoundingClientRect();" + " return [rect.left, rect.top, rect.right, rect.bottom];" + "})()"); + QVariantList coords = evaluateJavaScriptSync(page, jsCode).toList(); + + if (coords.count() != 4) { + qWarning("elementGeometry faield."); + return QRect(); + } + + return QRect(coords[0].toInt(), coords[1].toInt(), coords[2].toInt(), coords[3].toInt()); +} + + #define W_QSKIP(a, b) QSKIP(a) #define W_QTEST_MAIN(TestObject, params) \ diff --git a/tests/auto/widgets/widgets.pro b/tests/auto/widgets/widgets.pro index 6d65eecb5..d35f875c1 100644 --- a/tests/auto/widgets/widgets.pro +++ b/tests/auto/widgets/widgets.pro @@ -1,3 +1,5 @@ +load(functions) + include($$QTWEBENGINE_OUT_ROOT/src/core/qtwebenginecore-config.pri) # workaround for QTBUG-68093 QT_FOR_CONFIG += webenginecore webenginecore-private @@ -22,6 +24,9 @@ SUBDIRS += \ qwebenginesettings \ qwebengineview +# Synthetic touch events are not supported on macOS +!macos: SUBDIRS += touchinput + qtConfig(accessibility) { SUBDIRS += accessibility } @@ -34,7 +39,7 @@ qtConfig(ssl) { SUBDIRS += certificateerror } -qtConfig(webengine-spellchecker):!cross_compile { +qtConfig(webengine-spellchecker):!cross_compile:!isUniversal() { !qtConfig(webengine-native-spellchecker) { SUBDIRS += spellchecking } else { |