Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/gui/activity/qml/ActivitiesWindow.qml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ WizardStyledWindow {
minimumHeight: Style.wizardStandaloneWindowMinimumHeight

function resetActivityList() {
activityList.positionAtTop()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reset already-visible activity windows on open

This reset only fires from Component.onCompleted or when visible changes. In the cached-window path of Systray::showActivitiesWindow(), the tray action calls show(), raise(), and requestActivate() on the existing QQuickWindow; when that window is already visible but behind another window, visible does not change, so the list keeps its old scroll offset and new items at the top can still be missed. Please trigger resetActivityList() from the existing-window open path or another activation hook rather than relying only on visibleChanged.

Useful? React with 👍 / 👎.

newActivitiesButtonLoader.active = false
}

Expand Down Expand Up @@ -84,6 +85,7 @@ WizardStyledWindow {

ActivityList {
id: activityList
objectName: "activityList"

anchors.fill: parent
activeFocusOnTab: true
Expand All @@ -109,6 +111,7 @@ WizardStyledWindow {

Loader {
id: newActivitiesButtonLoader
objectName: "newActivitiesButtonLoader"

anchors.top: activityList.top
anchors.topMargin: Style.smallSpacing
Expand Down
6 changes: 6 additions & 0 deletions src/gui/activity/qml/ActivityList.qml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ ScrollView {
scrollingToTop = true
}

function positionAtTop() {
scrollingToTop = false
activityList.positionViewAtBeginning()
}

signal openFile(string filePath)
signal activityItemClicked(int index)

Expand All @@ -52,6 +57,7 @@ ScrollView {

ListView {
id: activityList
objectName: "activityListView"

Accessible.role: Accessible.List
Accessible.name: qsTr("Activity list")
Expand Down
1 change: 1 addition & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ nextcloud_add_test(TrayAccountMenuPolicy)
nextcloud_add_test(UnifiedSearchListmodel)
nextcloud_add_test(ActivityListModel)
nextcloud_add_test(SortedActivityListModel)
nextcloud_add_test(ActivitiesWindow)
nextcloud_add_test(ActivityData)
add_test(NAME ActivityFileMenuQmlTest
COMMAND Qt6::qmltestrunner
Expand Down
132 changes: 132 additions & 0 deletions test/testactivitieswindow.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: CC0-1.0
*
* This software is in the public domain, furnished "as is", without technical
* support, and with no warranty, express or implied, as to its usefulness for
* any purpose.
*/

#include "account.h"

Check failure on line 10 in test/testactivitieswindow.cpp

View workflow job for this annotation

GitHub Actions / build

test/testactivitieswindow.cpp:10:10 [clang-diagnostic-error]

'account.h' file not found
Comment on lines +1 to +10

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's avoid writing C++ QTests for QML components. The window should be testable using a QtQuick Test

https://doc.qt.io/qt-6/qtquicktest-index.html

#include "accountmanager.h"
#include "accountstate.h"
#include "activity/activitylistmodel.h"
#include "activity/sortedactivitylistmodel.h"
#include "activity/syncstatussummary.h"
#include "activitylistmodeltestutils.h"
#include "systray.h"
#include "syncenginetestutils.h"
#include "wheelhandler.h"

#include <QQmlApplicationEngine>
#include <QQmlComponent>
#include <QPointer>
#include <QQuickWindow>
#include <QScopedPointer>
#include <QStandardPaths>
#include <QTest>

using namespace ActivityListModelTestUtils;

class TestActivitiesWindow : public QObject

Check warning on line 31 in test/testactivitieswindow.cpp

View workflow job for this annotation

GitHub Actions / build

test/testactivitieswindow.cpp:31:7 [cppcoreguidelines-pro-type-member-init]

constructor does not initialize these fields: account, accountState
{
Q_OBJECT

public:
TestActivitiesWindow()
{
Q_INIT_RESOURCE(resources);
Q_INIT_RESOURCE(theme);
}

~TestActivitiesWindow() override
{
if (accountState) {
OCC::AccountManager::instance()->deleteAccount(accountState.data());
}
}

private slots:
void initTestCase()
{
QStandardPaths::setTestModeEnabled(true);

qmlRegisterType<OCC::SyncStatusSummary>("com.nextcloud.desktopclient", 1, 0, "SyncStatusSummary");
qmlRegisterType<OCC::SortedActivityListModel>("com.nextcloud.desktopclient", 1, 0, "SortedActivityListModel");
qmlRegisterType<WheelHandler>("com.nextcloud.desktopclient", 1, 0, "WheelHandler");
qmlRegisterSingletonInstance("com.nextcloud.desktopclient", 1, 0, "Systray", OCC::Systray::instance());

fakeQnam.reset(new FakeQNAM({}));
account = OCC::Account::create();
account->setCredentials(new FakeCredentials{fakeQnam.data()});
account->setUrl(QUrl(QStringLiteral("https://example.com")));
accountState = OCC::AccountManager::instance()->addAccount(account);
QVERIFY(accountState);
}

void testOpeningResetsViewportWithoutInterruptingLiveUpdates()
{
TestingALM activityModel;
activityModel.setAccountState(accountState.data());

const auto accountName = account->displayName();
for (auto id = 1; id <= 40; ++id) {
const auto activity = exampleNotificationActivity(accountName, id);
activityModel.addNotificationToActivityList(activity);
}

QQmlApplicationEngine engine;
engine.addImportPath(QStringLiteral("qrc:/qml/theme"));

QQmlComponent component(&engine, QUrl(QStringLiteral("qrc:/qml/src/gui/activity/qml/ActivitiesWindow.qml")));
QVERIFY2(component.isReady(), qPrintable(component.errorString()));

QScopedPointer<QObject> object(component.createWithInitialProperties({
{QStringLiteral("activityModel"), QVariant::fromValue(&activityModel)},
}));
QVERIFY2(object, qPrintable(component.errorString()));

const auto window = qobject_cast<QQuickWindow *>(object.data());
QVERIFY(window);

window->show();
QTRY_VERIFY(window->isVisible());

const auto activityList = window->findChild<QObject *>(QStringLiteral("activityList"));
const auto activityListView = window->findChild<QObject *>(QStringLiteral("activityListView"));
const auto newActivitiesButtonLoader = window->findChild<QObject *>(QStringLiteral("newActivitiesButtonLoader"));
QVERIFY(activityList);
QVERIFY(activityListView);
QVERIFY(newActivitiesButtonLoader);
QTRY_VERIFY(activityListView->property("contentHeight").toReal() > activityListView->property("height").toReal());

QVERIFY(QMetaObject::invokeMethod(activityListView, "positionViewAtEnd"));
QTRY_VERIFY(!activityList->property("atYBeginning").toBool());

window->hide();
QTRY_VERIFY(!window->isVisible());
window->show();
QTRY_VERIFY(window->isVisible());
QTRY_VERIFY(activityList->property("atYBeginning").toBool());

QVERIFY(QMetaObject::invokeMethod(activityListView, "positionViewAtEnd"));
QTRY_VERIFY(!activityList->property("atYBeginning").toBool());

auto interactiveActivity = exampleNotificationActivity(accountName, 41);
OCC::ActivityLink action;
action._verb = QByteArrayLiteral("POST");
interactiveActivity._links.push_back(action);
activityModel.addNotificationToActivityList(interactiveActivity);

QTRY_VERIFY(!activityList->property("atYBeginning").toBool());
QTRY_VERIFY(newActivitiesButtonLoader->property("active").toBool());
}

private:
QScopedPointer<FakeQNAM> fakeQnam;
OCC::AccountPtr account;
QPointer<OCC::AccountState> accountState;
};

QTEST_MAIN(TestActivitiesWindow)

Check warning on line 131 in test/testactivitieswindow.cpp

View workflow job for this annotation

GitHub Actions / build

test/testactivitieswindow.cpp:131:12 [cppcoreguidelines-avoid-non-const-global-variables]

variable 'TestActivitiesWindow' is non-const and globally accessible, consider making it const
#include "testactivitieswindow.moc"
Loading