Files
status-desktop/test/e2e/driver/objects_access.py
Mag. b8a9afda17 test(ActivityCenter): add QML component tests for notification controls (#20267)
* test(ActivityCenter): add QML component tests for notification controls

Add component test coverage for Activity Center notification controls:
- tst_NotificationAvatar.qml: avatar rendering, signals, density scaling, badge
- tst_NotificationCard.qml: card states, signals, quick actions, timestamps
- tst_NotificationContentBlock.qml: text, images, attachments, links
- tst_NotificationContextRow.qml: text properties, colours, separators
- tst_NotificationHeaderRow.qml: title, trust indicators, contact states
- tst_ActivityCenterPanel.qml: panel lifecycle, filters, mark-all-read
- tst_PrimaryNavSidebar.qml: section buttons, navigation, market/swap

Related: #18908

* refactor(ActivityCenter): address review feedback on NotificationCard architecture

Move logical state out of NotificationCard UI component per review:
- Remove showQuickActions property; derive quick action visibility from actionId
- Change declineRequested/acceptRequested signals to parameterless
- Resolve model context (avatarId, actionId) in ActivityCenterPanel handlers
- Expand QML test coverage: placeholder state, ListView count, icon state, news settings
- Add objectNames for listView, placeholderLoader, quickActions (test access)
- Update storybook and e2e references for the property change

* fix(e2e): update Squish object map after objectName added to listView/quickActions

The second PR commit added objectName: "listView" and objectName: "quickActions"
to ActivityCenterPanel.qml, but the object map entries still used unnamed: 1
(meaning "no objectName"). In Squish, unnamed: 1 stops matching once an object
has an objectName set, breaking the container chain used to find NotificationCard
items. This caused find_contact_request_in_list to always return [] and timeout.

Switch both entries from id/unnamed to objectName to match the QML.
2026-04-03 21:50:21 +01:00

169 lines
5.4 KiB
Python

import logging
import time
import object
import squish
import configs
LOG = logging.getLogger(__name__)
def describe_notification_card_for_log(card) -> str:
try:
title = getattr(card, 'title', None)
chat_key = getattr(card, 'chatKey', None)
return f'title={title!r} chatKey={chat_key!r}'
except (RuntimeError, AttributeError, LookupError):
return repr(card)
def describe_button_for_log(btn) -> str:
try:
return (
f'objectName={getattr(btn, "objectName", None)!r} '
f'visible={getattr(btn, "visible", None)} enabled={getattr(btn, "enabled", None)}'
)
except (RuntimeError, AttributeError, LookupError):
return repr(btn)
def walk_children(parent, depth: int = 1000):
for child in object.children(parent):
yield child
if depth:
yield from walk_children(child, depth - 1)
def find_descendant_by_object_name(parent, object_name: str, max_depth: int = 1000):
"""Walk QML tree from parent; Squish often fails waitForObject({container: parentRef, objectName: ...})."""
if parent is None or not object_name:
return None
for child in walk_children(parent, max_depth):
oname = getattr(child, 'objectName', None)
if oname is not None and str(oname) == object_name:
return child
return None
def is_descendant_of(ancestor, item) -> bool:
if ancestor is None or item is None:
return False
cur = item
for _ in range(96):
try:
if cur is ancestor:
return True
except Exception:
pass
cur = getattr(cur, 'parent', None)
if cur is None:
return False
return False
def find_notification_button_on_card(card, object_name: str, max_depth: int = 1000):
"""Find StatusButton by objectName on a specific NotificationCard.
``object.children`` from a QQuick ``Control`` often does not include ``contentItem`` children,
so tree-walk from the card misses Accept/Decline. Fallback: ``findAllObjects`` + ancestor or bounds.
"""
if card is None or not object_name:
return None
found = find_descendant_by_object_name(card, object_name, max_depth)
if found is not None:
LOG.info(
'find_notification_button_on_card: %r via tree walk on card %s',
object_name,
describe_notification_card_for_log(card),
)
return found
all_named = list(squish.findAllObjects({'objectName': object_name}))
for btn in all_named:
if not item_is_visible(btn):
continue
if is_descendant_of(card, btn):
LOG.info(
'find_notification_button_on_card: %r via ancestor chain on card %s; %s',
object_name,
describe_notification_card_for_log(card),
describe_button_for_log(btn),
)
return btn
try:
outer = object.globalBounds(card)
except (RuntimeError, LookupError, AttributeError):
outer = None
if outer is None or outer.width <= 0 or outer.height <= 0:
return None
for btn in all_named:
if not item_is_visible(btn):
continue
try:
inner = object.globalBounds(btn)
except (RuntimeError, LookupError, AttributeError):
continue
cx = inner.x + inner.width / 2
cy = inner.y + inner.height / 2
if outer.x <= cx <= outer.x + outer.width and outer.y <= cy <= outer.y + outer.height:
LOG.info(
'find_notification_button_on_card: %r via bounds on card %s; %s',
object_name,
describe_notification_card_for_log(card),
describe_button_for_log(btn),
)
return btn
LOG.info(
'find_notification_button_on_card: no %r on card %s (%d global name matches)',
object_name,
describe_notification_card_for_log(card),
len(all_named),
)
return None
def item_is_visible(item) -> bool:
"""True only if this item and QQuick ancestors are visible.
Quick-action StatusButtons stay ``visible: true`` while parent ``RowLayout`` (quickActions)
is hidden via ``visible: root.actionId !== ""`` — checking only the button lies.
"""
try:
cur = item
for _ in range(64):
if cur is None:
return True
if not bool(getattr(cur, 'visible', True)):
return False
try:
op = getattr(cur, 'opacity', None)
if op is not None and float(op) <= 0.0:
return False
except (TypeError, ValueError):
pass
cur = getattr(cur, 'parent', None)
return True
except (RuntimeError, AttributeError, LookupError):
return False
def wait_for_template(
real_name_template: dict, value: str, attr_name: str, timeout_sec: int = configs.timeouts.UI_LOAD_TIMEOUT_SEC):
started_at = time.monotonic()
while True:
for obj in squish.findAllObjects(real_name_template):
values = []
if hasattr(obj, attr_name):
current_value = str(getattr(obj, attr_name))
if current_value == value:
return obj
values.append(current_value)
if time.monotonic() - started_at > timeout_sec:
raise RuntimeError(f'Value not found in: {values}')
time.sleep(1)