summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-07-22 11:04:05 +0300
committerPaul Buetow <paul@buetow.org>2026-07-22 11:04:05 +0300
commit9af69a3409f67cbe3807538ccfb56fd0d3f43070 (patch)
tree912c18c43cfaecde242d15a0f60db88e40f2be78 /tests
parentd0a824c685db1567facaf08fcae19ee0636496da (diff)
Integrate navigator marks into window and grid UI (M8)
Wire the navigator mark API into the window actions, grid badges, and keyboard/pointer controls: - Navigator: add a path-based range anchor (p_last_mark) set on `v` toggle-on and cleared on unmark-of-anchor / clear_marks / remove / mark_removed / rescan-prune; expose navigator_get_last_mark(). navigator_mark_removed now clears the removed file's mark (decision Q / task du0) and the anchor if it was it, without touching unrelated marks, and emits "changed" whenever the removed set or marks actually changed so grid badges and the header never go stale. - Window: add win.mark-range (`V`: range from the last-mark anchor to the current/grid-selected image), register it, wire Shift+V, and document it in the shortcuts overlay. Trash/delete clear marks via mark_removed. - Grid: middle-click on a cell selects it and dispatches win.mark (pointer-accessible marks). - Tests: unit test_marks_anchor_and_remove and test_marks_anchor_pruned_on_rescan; integration test_marks.c covering toggle, all+clear, range, persistence across view switch, clear-on-trash, and the middle-click gesture wiring; updated test_shortcut.c action table + count. clang-format clean; full suite + ASan leak check green.
Diffstat (limited to 'tests')
-rw-r--r--tests/meson.build8
-rw-r--r--tests/test_marks.c424
-rw-r--r--tests/test_navigator.c108
-rw-r--r--tests/test_shortcut.c20
4 files changed, 550 insertions, 10 deletions
diff --git a/tests/meson.build b/tests/meson.build
index e895210..37d1233 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -263,4 +263,12 @@ test_clipboard = executable(
test('clipboard', test_clipboard,
suite : 'integration', env : fixtures_env)
+test_marks = executable(
+ 'test_marks', ['test_marks.c', ggaze_conf_h],
+ include_directories : [inc, src_inc],
+ dependencies : ggaze_deps,
+ link_with : ggaze_lib, install : false,
+)
+test('marks', test_marks, suite : 'integration', env : fixtures_env)
+
subdir('integration')
diff --git a/tests/test_marks.c b/tests/test_marks.c
new file mode 100644
index 0000000..a2350b1
--- /dev/null
+++ b/tests/test_marks.c
@@ -0,0 +1,424 @@
+/*:*
+ * ggaze — marks integration test
+ *
+ * Exercises the mark UI wiring in the window + grid: `v` toggle, `V` range,
+ * `Ctrl+a` mark-all, contextual `Esc` clear, persistence across a large/grid
+ * view switch, and mark clearing on a successful trash (decision Q / du0).
+ * Verifies through the public UI surface: the header title carries "N marked"
+ * and the grid cells carry the "ggaze-marked" badge in sync. Needs a display
+ * (integration suite; CI uses xvfb).
+ *
+ * Copyright (c) 2026 ggaze contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *:*/
+
+#include "gridview.h"
+#include "viewer.h"
+#include "window.h"
+
+#include <gdk/gdk.h>
+#include <gio/gio.h>
+#include <glib.h>
+#include <gtk/gtk.h>
+
+/* --- helpers ------------------------------------------------------------ */
+
+static GgazeWindow *
+new_window(void) {
+ return (GGAZE_WINDOW(g_object_new(GGAZE_TYPE_WINDOW, NULL)));
+}
+
+static GdkTexture *
+viewer_texture(GgazeWindow *p_win) {
+ GtkStack *p_stack = ggaze_window_get_stack(p_win);
+ GtkWidget *p_large = gtk_stack_get_child_by_name(p_stack, "large");
+ return (ggaze_viewer_get_texture(GGAZE_VIEWER(p_large)));
+}
+
+static void
+drain_main(guint u_ms) {
+ for (guint u = 0; u < u_ms; u++) {
+ g_main_context_iteration(g_main_context_default(), FALSE);
+ g_usleep(1000);
+ }
+}
+
+static void
+copy_fixture(const char *c_dir, const char *c_name) {
+ const gchar *c_fx = g_getenv("GGAZE_FIXTURES_DIR");
+ g_assert_nonnull(c_fx);
+ char *c_src = g_build_filename(c_fx, c_name, NULL);
+ char *c_dst = g_build_filename(c_dir, c_name, NULL);
+ GFile *p_src = g_file_new_for_path(c_src);
+ GFile *p_dst = g_file_new_for_path(c_dst);
+ GError *p_err = NULL;
+ g_assert_true(g_file_copy(p_src, p_dst, G_FILE_COPY_OVERWRITE, NULL, NULL,
+ NULL, &p_err));
+ g_assert_no_error(p_err);
+ g_object_unref(p_src);
+ g_object_unref(p_dst);
+ g_free(c_src);
+ g_free(c_dst);
+}
+
+static void
+cleanup_temp_dir(char *c_dir) {
+ GFile *p_dir = g_file_new_for_path(c_dir);
+ GFileEnumerator *p_e =
+ g_file_enumerate_children(p_dir, "standard::name,standard::type",
+ G_FILE_QUERY_INFO_NONE, NULL, NULL);
+ if (p_e != NULL) {
+ GFileInfo *p_info;
+ while ((p_info = g_file_enumerator_next_file(p_e, NULL, NULL)) != NULL) {
+ GFile *p_child = g_file_get_child(p_dir, g_file_info_get_name(p_info));
+ if (g_file_info_get_file_type(p_info) == G_FILE_TYPE_DIRECTORY) {
+ GFileEnumerator *p_e2 = g_file_enumerate_children(
+ p_child, "standard::name", G_FILE_QUERY_INFO_NONE, NULL, NULL);
+ if (p_e2 != NULL) {
+ GFileInfo *p_i2;
+ while ((p_i2 = g_file_enumerator_next_file(p_e2, NULL, NULL)) !=
+ NULL) {
+ GFile *p_c2 =
+ g_file_get_child(p_child, g_file_info_get_name(p_i2));
+ g_file_delete(p_c2, NULL, NULL);
+ g_object_unref(p_c2);
+ g_object_unref(p_i2);
+ }
+ g_object_unref(p_e2);
+ }
+ }
+ g_file_delete(p_child, NULL, NULL);
+ g_object_unref(p_child);
+ g_object_unref(p_info);
+ }
+ g_object_unref(p_e);
+ }
+ g_file_delete(p_dir, NULL, NULL);
+ g_object_unref(p_dir);
+ g_free(c_dir);
+}
+
+/* Fire a win.* action on the window. */
+static void
+fire(GgazeWindow *p_win, const char *c_action) {
+ gtk_widget_activate_action(GTK_WIDGET(p_win), c_action, NULL);
+}
+
+/* The grid built on open (the stack's "grid" child), even when not visible. */
+static GgazeGrid *
+window_grid(GgazeWindow *p_win) {
+ GtkStack *p_stack = ggaze_window_get_stack(p_win);
+ return (GGAZE_GRID(gtk_stack_get_child_by_name(p_stack, "grid")));
+}
+
+/* Find the first GtkFlowBox descendant of p_root (the grid's flowbox lives
+ * under the scrolled window, possibly via an internal GtkViewport). */
+static GtkWidget *
+find_flowbox(GtkWidget *p_root) {
+ if (GTK_IS_FLOW_BOX(p_root)) {
+ return (p_root);
+ }
+ GtkWidget *p_child = gtk_widget_get_first_child(p_root);
+ while (p_child != NULL) {
+ GtkWidget *p_found = find_flowbox(p_child);
+ if (p_found != NULL) {
+ return (p_found);
+ }
+ p_child = gtk_widget_get_next_sibling(p_child);
+ }
+ return (NULL);
+}
+
+/* Count grid cells currently showing the "ggaze-marked" badge. Walks the
+ * GtkFlowBox (grid -> scrolled -> flowbox) and checks each cell's box. */
+static guint
+grid_marked_count(GgazeGrid *p_grid) {
+ GtkWidget *p_flow = find_flowbox(GTK_WIDGET(p_grid));
+ g_assert_nonnull(p_flow);
+ guint u_count = 0;
+ GtkWidget *p_child = gtk_widget_get_first_child(p_flow);
+ while (p_child != NULL) {
+ GtkWidget *p_box =
+ gtk_flow_box_child_get_child(GTK_FLOW_BOX_CHILD(p_child));
+ if (p_box != NULL && gtk_widget_has_css_class(p_box, "ggaze-marked")) {
+ u_count++;
+ }
+ p_child = gtk_widget_get_next_sibling(p_child);
+ }
+ return (u_count);
+}
+
+/* The mark count shown in the window title ("N marked"), or 0 if the title
+ * carries no "marked" suffix (which is how 0 marks is rendered). */
+static guint
+title_mark_count(GgazeWindow *p_win) {
+ const gchar *c_title = gtk_window_get_title(GTK_WINDOW(p_win));
+ if (c_title == NULL) {
+ return (0);
+ }
+ const char *c_p = g_strstr_len(c_title, -1, " marked");
+ if (c_p == NULL) {
+ return (0);
+ }
+ /* walk back over the digits immediately preceding " marked" */
+ const char *c_num = c_p;
+ while (c_num > c_title && g_ascii_isdigit(*(c_num - 1))) {
+ c_num--;
+ }
+ if (c_num == c_p) {
+ return (0);
+ }
+ return ((guint)g_ascii_strtoull(c_num, NULL, 10));
+}
+
+static void
+assert_marks(GgazeWindow *p_win, guint u_expect) {
+ drain_main(100);
+ g_assert_cmpint(title_mark_count(p_win), ==, u_expect);
+ g_assert_cmpint(grid_marked_count(window_grid(p_win)), ==, u_expect);
+}
+
+static char *
+setup_dir_with_three(GFile **p_plain_out) {
+ GError *p_err = NULL;
+ char *c_dir = g_dir_make_tmp("ggaze-marks-XXXXXX", &p_err);
+ g_assert_no_error(p_err);
+ copy_fixture(c_dir, "plain.jpg");
+ copy_fixture(c_dir, "rot6.jpg");
+ copy_fixture(c_dir, "small.png");
+ char *c_path = g_build_filename(c_dir, "plain.jpg", NULL);
+ GFile *p_plain = g_file_new_for_path(c_path);
+ g_free(c_path);
+ *p_plain_out = p_plain;
+ return (c_dir);
+}
+
+static void
+wait_for_load(GgazeWindow *p_win) {
+ for (guint u = 0; u < 3000 && viewer_texture(p_win) == NULL; u++) {
+ g_main_context_iteration(g_main_context_default(), FALSE);
+ g_usleep(1000);
+ }
+ g_assert_nonnull(viewer_texture(p_win));
+ drain_main(200);
+}
+
+/* --- subtests ----------------------------------------------------------- */
+
+/* `v` toggles a mark on the current (large-view) image; the header shows
+ * "1 marked" and the grid badge appears on exactly one cell. `v` again
+ * untoggles it. */
+static void
+test_mark_toggle(void) {
+ GFile *p_plain = NULL;
+ char *c_dir = setup_dir_with_three(&p_plain);
+
+ GgazeWindow *p_win = new_window();
+ ggaze_window_open(p_win, p_plain);
+ wait_for_load(p_win);
+
+ g_assert_cmpint(ggaze_grid_get_count(window_grid(p_win)), ==, 3);
+ assert_marks(p_win, 0);
+
+ fire(p_win, "win.mark");
+ assert_marks(p_win, 1);
+
+ fire(p_win, "win.mark"); /* untoggle */
+ assert_marks(p_win, 0);
+
+ g_object_unref(p_plain);
+ g_object_unref(p_win);
+ drain_main(300);
+ cleanup_temp_dir(c_dir);
+}
+
+/* `Ctrl+a` marks every image; `Esc` (win.back) clears all marks contextually
+ * (no fullscreen / no large->grid back-step happens while marks exist). */
+static void
+test_mark_all_and_clear(void) {
+ GFile *p_plain = NULL;
+ char *c_dir = setup_dir_with_three(&p_plain);
+
+ GgazeWindow *p_win = new_window();
+ ggaze_window_open(p_win, p_plain);
+ wait_for_load(p_win);
+
+ fire(p_win, "win.mark-all");
+ assert_marks(p_win, 3);
+
+ /* Esc clears marks first (contextual back), rather than leaving large view.
+ */
+ GtkStack *p_stack = ggaze_window_get_stack(p_win);
+ g_assert_cmpstr(gtk_stack_get_visible_child_name(p_stack), ==, "large");
+ fire(p_win, "win.back");
+ assert_marks(p_win, 0);
+ /* Still in large view: Esc only cleared marks, did not back out. */
+ g_assert_cmpstr(gtk_stack_get_visible_child_name(p_stack), ==, "large");
+
+ g_object_unref(p_plain);
+ g_object_unref(p_win);
+ drain_main(300);
+ cleanup_temp_dir(c_dir);
+}
+
+/* `V` range-marks from the last `v`-toggled anchor to the current image.
+ * Mark a (anchor), advance to b, range -> 2 marked; advance to c, range
+ * -> 3 marked. */
+static void
+test_mark_range(void) {
+ GFile *p_plain = NULL;
+ char *c_dir = setup_dir_with_three(&p_plain);
+
+ GgazeWindow *p_win = new_window();
+ ggaze_window_open(p_win, p_plain);
+ wait_for_load(p_win);
+
+ /* current = plain.jpg (idx 0). Mark it -> anchor. */
+ fire(p_win, "win.mark");
+ assert_marks(p_win, 1);
+
+ /* advance to rot6.jpg (idx 1), range from anchor -> a, b marked. */
+ fire(p_win, "win.next");
+ drain_main(100);
+ fire(p_win, "win.mark-range");
+ assert_marks(p_win, 2);
+
+ /* advance to small.png (idx 2), range from anchor -> a, b, c marked. */
+ fire(p_win, "win.next");
+ drain_main(100);
+ fire(p_win, "win.mark-range");
+ assert_marks(p_win, 3);
+
+ g_object_unref(p_plain);
+ g_object_unref(p_win);
+ drain_main(300);
+ cleanup_temp_dir(c_dir);
+}
+
+/* Marks persist across a large > grid > large view switch: the grid shows
+ * the same badges and the header keeps the same count. */
+static void
+test_marks_persist_across_view_switch(void) {
+ GFile *p_plain = NULL;
+ char *c_dir = setup_dir_with_three(&p_plain);
+
+ GgazeWindow *p_win = new_window();
+ ggaze_window_open(p_win, p_plain);
+ wait_for_load(p_win);
+
+ fire(p_win, "win.mark");
+ fire(p_win, "win.next");
+ drain_main(100);
+ fire(p_win, "win.mark"); /* mark 2 images */
+ assert_marks(p_win, 2);
+
+ /* large -> grid: badges reflect the 2 marks. */
+ GtkStack *p_stack = ggaze_window_get_stack(p_win);
+ fire(p_win, "win.toggle-view");
+ drain_main(200);
+ g_assert_cmpstr(gtk_stack_get_visible_child_name(p_stack), ==, "grid");
+ g_assert_cmpint(grid_marked_count(window_grid(p_win)), ==, 2);
+ g_assert_cmpint(title_mark_count(p_win), ==, 2);
+
+ /* grid -> large: marks still present. */
+ fire(p_win, "win.toggle-view");
+ drain_main(200);
+ g_assert_cmpstr(gtk_stack_get_visible_child_name(p_stack), ==, "large");
+ assert_marks(p_win, 2);
+
+ g_object_unref(p_plain);
+ g_object_unref(p_win);
+ drain_main(300);
+ cleanup_temp_dir(c_dir);
+}
+
+/* Trashing the current image clears its mark (decision Q / du0): mark all 3,
+ * trash the current, the mark count drops to 2 and the grid badges drop one. */
+static void
+test_marks_clear_on_trash(void) {
+ GFile *p_plain = NULL;
+ char *c_dir = setup_dir_with_three(&p_plain);
+
+ GgazeWindow *p_win = new_window();
+ ggaze_window_open(p_win, p_plain);
+ wait_for_load(p_win);
+
+ fire(p_win, "win.mark-all");
+ assert_marks(p_win, 3);
+
+ fire(p_win, "win.trash");
+ drain_main(300);
+ /* The trashed file's mark is cleared; 2 marks remain. */
+ g_assert_cmpint(title_mark_count(p_win), ==, 2);
+ g_assert_cmpint(grid_marked_count(window_grid(p_win)), ==, 2);
+
+ /* .Trash was created with the binned file. */
+ char *c_trashpath = g_build_filename(c_dir, ".Trash", NULL);
+ GFile *p_trash = g_file_new_for_path(c_trashpath);
+ g_assert_true(g_file_query_exists(p_trash, NULL));
+ g_object_unref(p_trash);
+ g_free(c_trashpath);
+
+ g_object_unref(p_plain);
+ g_object_unref(p_win);
+ drain_main(300);
+ cleanup_temp_dir(c_dir);
+}
+
+/* A middle-click GtkGestureClick is attached to the grid flowbox, wired to
+ * toggle the mark on the clicked cell (pointer-accessible marks). GTK 4.22
+ * exposes no public API to synthesize a button event, so this is a structural
+ * check that the gesture is registered with button == middle, analogous to the
+ * shortcut-controller scope check in test_shortcut.c. */
+static void
+test_pointer_middle_click_wired(void) {
+ GFile *p_plain = NULL;
+ char *c_dir = setup_dir_with_three(&p_plain);
+
+ GgazeWindow *p_win = new_window();
+ ggaze_window_open(p_win, p_plain);
+ wait_for_load(p_win);
+
+ GgazeGrid *p_grid = window_grid(p_win);
+ GtkWidget *p_flow = find_flowbox(GTK_WIDGET(p_grid));
+ g_assert_nonnull(p_flow);
+ GListModel *p_ctrls = gtk_widget_observe_controllers(p_flow);
+ gboolean b_found = FALSE;
+ guint u_n = g_list_model_get_n_items(p_ctrls);
+ for (guint i = 0; i < u_n; i++) {
+ GObject *p_obj = g_list_model_get_item(p_ctrls, i);
+ if (GTK_IS_GESTURE_CLICK(p_obj)) {
+ if (gtk_gesture_single_get_button(GTK_GESTURE_SINGLE(p_obj)) ==
+ GDK_BUTTON_MIDDLE) {
+ b_found = TRUE;
+ }
+ }
+ g_object_unref(p_obj);
+ }
+ g_object_unref(p_ctrls);
+ g_assert_true(b_found);
+
+ g_object_unref(p_plain);
+ g_object_unref(p_win);
+ drain_main(300);
+ cleanup_temp_dir(c_dir);
+}
+
+int
+main(int i_argc, char **c_argv) {
+ g_test_init(&i_argc, &c_argv, NULL);
+ g_log_set_always_fatal(G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL);
+ if (!gtk_init_check()) {
+ g_test_skip("no display available (run under xvfb)");
+ return (g_test_run());
+ }
+ g_test_add_func("/marks/toggle", test_mark_toggle);
+ g_test_add_func("/marks/all_and_clear", test_mark_all_and_clear);
+ g_test_add_func("/marks/range", test_mark_range);
+ g_test_add_func("/marks/persist_across_view_switch",
+ test_marks_persist_across_view_switch);
+ g_test_add_func("/marks/clear_on_trash", test_marks_clear_on_trash);
+ g_test_add_func("/marks/pointer_middle_click_wired",
+ test_pointer_middle_click_wired);
+ return (g_test_run());
+} \ No newline at end of file
diff --git a/tests/test_navigator.c b/tests/test_navigator.c
index e8528b1..9cc86f2 100644
--- a/tests/test_navigator.c
+++ b/tests/test_navigator.c
@@ -304,6 +304,110 @@ test_marks(void) {
cleanup_temp_dir(c_dir);
}
+/* `v` toggle-on records the anchor used by `V` range-mark; unmarking the
+ * anchor drops it. navigator_mark_removed clears the mark (decision Q) and
+ * the anchor if it was the removed file, without touching unrelated marks. */
+static void
+test_marks_anchor_and_remove(void) {
+ char *c_dir = make_temp_dir();
+ write_file(c_dir, "a.jpg", "x", 1);
+ write_file(c_dir, "b.jpg", "x", 1);
+ write_file(c_dir, "c.jpg", "x", 1);
+ GFile *p_a = file_ref(c_dir, "a.jpg");
+ GFile *p_b = file_ref(c_dir, "b.jpg");
+ GFile *p_c = file_ref(c_dir, "c.jpg");
+
+ GFile *p_dirf = g_file_new_for_path(c_dir);
+ Navigator *p_nav = navigator_new(p_dirf, GGAZE_SORT_NAME, TRUE, TRUE);
+
+ /* No anchor until a mark is toggled on. */
+ g_assert_null(navigator_get_last_mark(p_nav));
+ navigator_toggle_mark(p_nav, p_a); /* mark a, anchor = a */
+ g_assert_true(g_file_equal(navigator_get_last_mark(p_nav), p_a));
+ g_assert_cmpint(navigator_get_mark_count(p_nav), ==, 1);
+
+ /* Range from anchor a to current c marks a, b, c. */
+ navigator_set_current_file(p_nav, p_c);
+ navigator_mark_range(p_nav, navigator_get_last_mark(p_nav), p_c);
+ g_assert_cmpint(navigator_get_mark_count(p_nav), ==, 3);
+
+ /* Unmarking the anchor drops it; the other marks remain. */
+ navigator_toggle_mark(p_nav, p_a); /* unmark a (was the anchor) */
+ g_assert_null(navigator_get_last_mark(p_nav));
+ g_assert_cmpint(navigator_get_mark_count(p_nav), ==, 2);
+ g_assert_true(navigator_is_marked(p_nav, p_b));
+ g_assert_true(navigator_is_marked(p_nav, p_c));
+
+ /* Re-marking a sets the anchor again; clearing marks drops it. */
+ navigator_toggle_mark(p_nav, p_a); /* mark a, anchor = a */
+ g_assert_true(g_file_equal(navigator_get_last_mark(p_nav), p_a));
+ navigator_clear_marks(p_nav);
+ g_assert_null(navigator_get_last_mark(p_nav));
+ g_assert_cmpint(navigator_get_mark_count(p_nav), ==, 0);
+
+ /* mark_removed clears that file's mark but leaves unrelated marks, and
+ * drops the anchor when the removed file was the anchor. */
+ navigator_toggle_mark(p_nav, p_b); /* mark b, anchor = b */
+ navigator_toggle_mark(p_nav, p_c); /* mark c, anchor = c */
+ g_assert_cmpint(navigator_get_mark_count(p_nav), ==, 2);
+ navigator_mark_removed(p_nav, p_c);
+ g_assert_false(navigator_is_marked(p_nav, p_c));
+ g_assert_true(navigator_is_marked(p_nav, p_b));
+ g_assert_cmpint(navigator_get_mark_count(p_nav), ==, 1);
+ g_assert_null(navigator_get_last_mark(p_nav)); /* anchor was c -> cleared */
+ /* mark_removed on the remaining (non-anchor) mark clears it too. */
+ navigator_mark_removed(p_nav, p_b);
+ g_assert_cmpint(navigator_get_mark_count(p_nav), ==, 0);
+
+ navigator_delete(p_nav);
+ g_object_unref(p_a);
+ g_object_unref(p_b);
+ g_object_unref(p_c);
+ g_object_unref(p_dirf);
+ cleanup_temp_dir(c_dir);
+}
+
+/* The range anchor is dropped when its file leaves the listing via an
+ * external delete + rescan (the monitor path), so `V` no-ops instead of
+ * range-marking from a stale path. */
+static void
+test_marks_anchor_pruned_on_rescan(void) {
+ char *c_dir = make_temp_dir();
+ write_file(c_dir, "a.jpg", "x", 1);
+ write_file(c_dir, "b.jpg", "x", 1);
+ write_file(c_dir, "c.jpg", "x", 1);
+ GFile *p_a = file_ref(c_dir, "a.jpg");
+ GFile *p_b = file_ref(c_dir, "b.jpg");
+ GFile *p_c = file_ref(c_dir, "c.jpg");
+
+ GFile *p_dirf = g_file_new_for_path(c_dir);
+ Navigator *p_nav = navigator_new(p_dirf, GGAZE_SORT_NAME, TRUE, TRUE);
+
+ navigator_toggle_mark(p_nav, p_a); /* anchor = a */
+ navigator_toggle_mark(p_nav, p_b);
+ g_assert_cmpint(navigator_get_mark_count(p_nav), ==, 2);
+ g_assert_true(g_file_equal(navigator_get_last_mark(p_nav), p_b));
+
+ /* Externally delete a (the anchor's mark prunes on rescan). */
+ g_assert_true(g_file_delete(p_a, NULL, NULL));
+ navigator_rescan(p_nav);
+ g_assert_cmpint(navigator_get_mark_count(p_nav), ==, 1); /* b remains */
+ g_assert_false(navigator_is_marked(p_nav, p_a));
+
+ /* Externally delete b: the anchor's file is gone, so the anchor drops. */
+ g_assert_true(g_file_delete(p_b, NULL, NULL));
+ navigator_rescan(p_nav);
+ g_assert_cmpint(navigator_get_mark_count(p_nav), ==, 0);
+ g_assert_null(navigator_get_last_mark(p_nav)); /* anchor pruned */
+
+ navigator_delete(p_nav);
+ g_object_unref(p_a);
+ g_object_unref(p_b);
+ g_object_unref(p_c);
+ g_object_unref(p_dirf);
+ cleanup_temp_dir(c_dir);
+}
+
static void
test_rescan_and_nearest_fallback(void) {
char *c_dir = make_temp_dir();
@@ -471,6 +575,10 @@ main(int i_argc, char **c_argv) {
g_test_add_func("/navigator/prev_next_wrap", test_prev_next_wrap);
g_test_add_func("/navigator/set_current_file", test_set_current_file);
g_test_add_func("/navigator/marks", test_marks);
+ g_test_add_func("/navigator/marks_anchor_and_remove",
+ test_marks_anchor_and_remove);
+ g_test_add_func("/navigator/marks_anchor_pruned_on_rescan",
+ test_marks_anchor_pruned_on_rescan);
g_test_add_func("/navigator/rescan_fallback",
test_rescan_and_nearest_fallback);
g_test_add_func("/navigator/remove", test_remove_clears_mark_and_falls_back);
diff --git a/tests/test_shortcut.c b/tests/test_shortcut.c
index 96a5b74..7365ef0 100644
--- a/tests/test_shortcut.c
+++ b/tests/test_shortcut.c
@@ -328,14 +328,14 @@ test_shortcut_keypath_toggle_and_back(void) {
static void
test_shortcut_full_table_registered(void) {
static const char *ACTIONS[] = {
- "win.prev", "win.next", "win.first", "win.last",
- "win.open", "win.quit", "win.trash", "win.delete",
- "win.undo", "win.toggle-view", "win.mark", "win.mark-all",
- "win.shortcuts", "win.zoom-in", "win.zoom-out", "win.fullscreen",
- "win.slideshow", "win.info", "win.back", "win.enhance",
- "win.enhance-save", "win.enhance-1", "win.enhance-2", "win.enhance-3",
- "win.enhance-4", "win.enhance-5", "win.enhance-6", "win.enhance-7",
- "win.enhance-8",
+ "win.prev", "win.next", "win.first", "win.last",
+ "win.open", "win.quit", "win.trash", "win.delete",
+ "win.undo", "win.toggle-view", "win.mark", "win.mark-all",
+ "win.mark-range", "win.shortcuts", "win.zoom-in", "win.zoom-out",
+ "win.fullscreen", "win.slideshow", "win.info", "win.back",
+ "win.enhance", "win.enhance-save", "win.enhance-1", "win.enhance-2",
+ "win.enhance-3", "win.enhance-4", "win.enhance-5", "win.enhance-6",
+ "win.enhance-7", "win.enhance-8",
};
GgazeWindow *p_win = new_window();
GtkShortcutController *p_sc = find_shortcut_controller(GTK_WIDGET(p_win));
@@ -345,9 +345,9 @@ test_shortcut_full_table_registered(void) {
g_assert_nonnull(p_s);
g_object_unref(p_s);
}
- /* The SHORTCUTS[] table has 25 rows now (some actions appear twice, e.g.
+ /* The SHORTCUTS[] table has 34 rows now (some actions appear twice, e.g.
* win.prev for h and Left; win.zoom-in for plus and equal). */
- g_assert_cmpint(g_list_model_get_n_items(G_LIST_MODEL(p_sc)), ==, 33);
+ g_assert_cmpint(g_list_model_get_n_items(G_LIST_MODEL(p_sc)), ==, 34);
g_object_unref(p_sc);
g_object_unref(p_win);
drain_main(200);