Skip to content
Merged
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
26 changes: 23 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,14 +136,34 @@ jobs:
- name: Build host
run: raco rivet build
- name: Boot smoke
shell: bash
run: |
set -euo pipefail
smoke_home="$RUNNER_TEMP/taskly-smoke-home"
smoke_log="$RUNNER_TEMP/taskly-smoke.log"
mkdir -p "$smoke_home"
HOME="$smoke_home" ./.rivet/stage/RivetHost &
HOME="$smoke_home" ./.rivet/stage/RivetHost >"$smoke_log" 2>&1 &
APP_PID=$!
cleanup() {
if kill -0 "$APP_PID" 2>/dev/null; then
kill -TERM "$APP_PID" 2>/dev/null || true
for _ in 1 2 3 4 5; do
kill -0 "$APP_PID" 2>/dev/null || break
sleep 1
done
kill -KILL "$APP_PID" 2>/dev/null || true
fi
wait "$APP_PID" 2>/dev/null || true
}
trap cleanup EXIT
sleep 8
kill -0 $APP_PID
kill $APP_PID
if ! kill -0 "$APP_PID" 2>/dev/null; then
cat "$smoke_log" || true
echo "::error::RivetHost exited before the 8-second smoke window"
exit 1
fi
cleanup
trap - EXIT

windows-host:
name: Windows host (WinRT build)
Expand Down
24 changes: 18 additions & 6 deletions linux/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2666,18 +2666,29 @@ void rebuild_task_list() {
// ---------------------------------------------------------------------------

guint banner_timeout() {
if (g_state.banner_box == nullptr) return 0;
return GPOINTER_TO_UINT(
g_object_get_data(G_OBJECT(g_state.banner_box), "banner-timeout"));
}

void hide_delete_banner() {
// banner_box is the persistent child of the revealer. Remove only the
// transient card; replacing the revealer's child would destroy banner_box
// while g_state.banner_box still points at it.
if (g_state.banner_box == nullptr) return;
auto const timeout = banner_timeout();
if (timeout != 0) g_source_remove(timeout);
g_object_set_data(G_OBJECT(g_state.banner_box), "banner-timeout",
GUINT_TO_POINTER(0));
gtk_revealer_set_reveal_child(
GTK_REVEALER(g_object_get_data(G_OBJECT(g_state.banner_box),
"banner-revealer")), FALSE);
auto* revealer = GTK_REVEALER(
g_object_get_data(G_OBJECT(g_state.banner_box), "banner-revealer"));
if (revealer != nullptr) {
gtk_revealer_set_reveal_child(revealer, FALSE);
}
while (auto* child =
gtk_widget_get_first_child(GTK_WIDGET(g_state.banner_box))) {
gtk_box_remove(g_state.banner_box, child);
}
}

void refresh_after_staged_delete() {
Expand Down Expand Up @@ -2763,7 +2774,8 @@ void stage_delete_tasks(std::vector<std::int64_t> const& ids) {
commit_staged_delete();
}), nullptr);
gtk_box_append(GTK_BOX(card), close);
gtk_revealer_set_child(revealer, card);
// Keep the persistent banner_box alive; only the card is transient.
gtk_box_append(GTK_BOX(g_state.banner_box), card);
gtk_revealer_set_reveal_child(revealer, TRUE);
auto const timeout = g_timeout_add_seconds(6, on_undo_timeout, nullptr);
g_object_set_data(G_OBJECT(g_state.banner_box), "banner-timeout",
Expand Down Expand Up @@ -3015,8 +3027,8 @@ void apply_language() {
build_menubar();
gtk_label_set_text(g_state.my_lists_caption,
tr("sectionMyLists").c_str());
gtk_entry_set_placeholder_text(GTK_ENTRY(g_state.search_box),
tr("searchHint").c_str());
gtk_search_entry_set_placeholder_text(g_state.search_box,
tr("searchHint").c_str());
gtk_entry_set_placeholder_text(g_state.quick_add,
tr("taskListInputHint").c_str());
gtk_accessible_update_property(
Expand Down
67 changes: 54 additions & 13 deletions scripts/e2e/linux-atspi.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ def role(node):
return "unknown"


def name(node):
try:
return node.name or ""
except Exception:
# A row can disappear between two AT-SPI calls while GTK rebuilds it.
return ""


def text(node):
try:
value = node.queryText()
Expand All @@ -60,21 +68,21 @@ def find_application():
desktop = pyatspi.Registry.getDesktop(0)
for application in children(desktop):
for node in children(application):
if role(node) == "frame" and "taskly" in (node.name or "").casefold():
if role(node) == "frame" and "taskly" in name(node).casefold():
return application
return None


def find_named(root, name, expected_role=None):
def find_named(root, expected_name, expected_role=None):
for node in walk(root):
if (node.name or "") == name and (expected_role is None or role(node) == expected_role):
if name(node) == expected_name and (expected_role is None or role(node) == expected_role):
return node
return None


def find_editable_named(root, name):
def find_editable_named(root, expected_name):
for node in walk(root):
if (node.name or "") != name:
if name(node) != expected_name:
continue
try:
node.queryEditableText()
Expand All @@ -85,12 +93,44 @@ def find_editable_named(root, name):


def contains_text(root, expected):
return any((node.name or "") == expected or text(node) == expected for node in walk(root))
return any(name(node) == expected or text(node) == expected for node in walk(root))


def find_staged_delete_ready(root):
"""Find the Undo button and verify both staged task rows are already hidden in one tree walk."""
undo = None
found_a = False
found_b = False
for node in walk(root):
node_name = name(node)
node_text = ""
if node_name == "Undo" and role(node) == "push button":
undo = node
if node_name == "64-bit task A":
found_a = True
elif not found_a:
node_text = text(node)
if node_text == "64-bit task A":
found_a = True
if node_name == "64-bit task B":
found_b = True
elif not found_b:
if not node_text:
node_text = text(node)
if node_text == "64-bit task B":
found_b = True
if undo is not None and found_a and found_b:
return None
return undo if undo is not None and not found_a and not found_b else None


def matching_child_indexes(container, expected):
result = []
for index in range(container.childCount):
try:
count = container.childCount
except Exception:
return result
for index in range(count):
try:
child = container.getChildAtIndex(index)
except Exception:
Expand Down Expand Up @@ -142,7 +182,7 @@ def select_named_children(root, expected_names):


def snapshot(node, depth=0):
result = {"name": node.name or "", "role": role(node)}
result = {"name": name(node), "role": role(node)}
value = text(node)
if value:
result["text"] = value
Expand Down Expand Up @@ -210,12 +250,13 @@ def main():
lambda: select_named_children(window, ["64-bit task A", "64-bit task B"]),
)
press(delete)
wait_for(
"staged tasks hidden",
lambda: not contains_text(window, "64-bit task A")
and not contains_text(window, "64-bit task B"),
# The product's undo window is six seconds. Find the banner and confirm
# both rows are hidden in one AT-SPI traversal, then undo immediately.
undo = wait_for(
"staged deletion hidden with undo banner",
lambda: find_staged_delete_ready(window),
timeout=4.0,
)
undo = wait_for("undo banner", lambda: find_named(window, "Undo", "push button"))
press(undo)
wait_for(
"identity-preserving undo",
Expand Down
Loading