/* * Copyright © 2025 Khalid Abu Shawarib * * SPDX-License-Identifier: GPL-3.0-or-later */ #define G_LOG_DOMAIN "test-files-view" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define G_LOG_DOMAIN "test-files-view" static gboolean ptr_arrays_equal_unordered (GPtrArray *a, GPtrArray *b) { if (a->len != b->len) { return FALSE; } for (guint i = 0; i < a->len; i++) { if (!g_ptr_array_find (b, a->pdata[i], NULL)) { return FALSE; } } return TRUE; } static gboolean ptr_array_is_subset (GPtrArray *subset, GPtrArray *set) { if (subset->len > set->len) { return FALSE; } for (guint i = 0; i < subset->len; i++) { if (!g_ptr_array_find (set, subset->pdata[i], NULL)) { return FALSE; } } return TRUE; } static void set_true (gboolean *data) { *data = TRUE; } static void collect_renamed_files (NautilusFile *file, GFile *result_location, GError *error, gpointer callback_data) { GPtrArray *renamed_files_arr = callback_data; g_ptr_array_add (renamed_files_arr, nautilus_file_ref (file)); } static void collect_changed_files (NautilusFilesView *view, NautilusFile *file, NautilusDirectory *directory, GPtrArray *data_files) { /* Ignore duplicate change emissions originating from nautilus-directory */ if (!g_ptr_array_find (data_files, file, NULL)) { g_ptr_array_add (data_files, nautilus_file_ref (file)); } } static void file_changes_done (NautilusFilesView *view, gpointer user_data) { gboolean *end_of_changes = user_data; *end_of_changes = TRUE; } static NautilusFilesView * create_view_with_slot (NautilusMode mode, uint view_id, NautilusWindowSlot **slot_out) { NautilusWindowSlot *slot = nautilus_window_slot_new (mode); g_autoptr (GFile) location = g_file_new_for_path (g_get_home_dir ()); /* Open the location to trigger view creation */ nautilus_window_slot_open_location_full (slot, location, NULL); /* The slot creates its content view after asynchronous loading */ ITER_CONTEXT_WHILE (nautilus_window_slot_get_current_view (slot) == NULL); NautilusFilesView *view = nautilus_window_slot_get_current_view (slot); if (nautilus_files_view_get_view_id (view) != view_id) { nautilus_files_view_change (view, view_id); } *slot_out = slot; return g_object_ref (view); } static void run_view_zoom_testcase (GSettings *view_setting, const gchar *zoom_setting_key, uint default_zoom_level, uint view_id) { g_settings_set_enum (view_setting, zoom_setting_key, default_zoom_level); g_autoptr (NautilusWindowSlot) slot = NULL; g_autoptr (NautilusFilesView) files_view = create_view_with_slot (NAUTILUS_MODE_BROWSE, view_id, &slot); g_autoptr (GFile) tmp_location = g_file_new_for_path (test_get_tmp_dir ()); NautilusListBase *list_base = nautilus_files_view_get_private_list_base (files_view); NautilusViewInfo view_info = nautilus_list_base_get_view_info (list_base); const guint file_count = 10; create_multiple_files ("zoom_test", file_count); nautilus_files_view_set_location (files_view, tmp_location); ITER_CONTEXT_WHILE (nautilus_files_view_get_loading (files_view)); GActionGroup *group = nautilus_files_view_get_private_action_group (files_view); GAction *action_zoom_in = g_action_map_lookup_action (G_ACTION_MAP (group), "zoom-in"); GAction *action_zoom_out = g_action_map_lookup_action (G_ACTION_MAP (group), "zoom-out"); GAction *action_zoom_standard = g_action_map_lookup_action (G_ACTION_MAP (group), "zoom-standard"); /* Zoom in until we reach the max (action becomes disabled) */ while (g_action_get_enabled (action_zoom_in)) { gtk_widget_activate_action (GTK_WIDGET (files_view), "view.zoom-in", NULL); g_assert_true (g_action_get_enabled (action_zoom_out)); } g_assert_true (g_action_get_enabled (action_zoom_standard)); g_assert_cmpint (g_settings_get_enum (view_setting, zoom_setting_key), ==, view_info.zoom_level_max); /* Zoom out until we reach the max (action becomes disabled) */ while (g_action_get_enabled (action_zoom_out)) { gtk_widget_activate_action (GTK_WIDGET (files_view), "view.zoom-out", NULL); g_assert_true (g_action_get_enabled (action_zoom_in)); } g_assert_true (g_action_get_enabled (action_zoom_standard)); g_assert_cmpint (g_settings_get_enum (view_setting, zoom_setting_key), ==, view_info.zoom_level_min); /* Now set to standard and ensure the standard action becomes disabled */ gtk_widget_activate_action (GTK_WIDGET (files_view), "view.zoom-standard", NULL); g_assert_true (g_action_get_enabled (action_zoom_in)); g_assert_true (g_action_get_enabled (action_zoom_out)); g_assert_false (g_action_get_enabled (action_zoom_standard)); g_assert_cmpint (g_settings_get_enum (view_setting, zoom_setting_key), ==, view_info.zoom_level_standard); test_clear_tmp_dir (); } static void test_zoom_actions (void) { struct { GSettings *view_setting; const gchar *zoom_setting_key; uint default_zoom_level; uint view_id; } testcases[] = { { nautilus_list_view_preferences, NAUTILUS_PREFERENCES_LIST_VIEW_DEFAULT_ZOOM_LEVEL, NAUTILUS_LIST_ZOOM_LEVEL_MEDIUM, NAUTILUS_VIEW_LIST_ID, }, { nautilus_icon_view_preferences, NAUTILUS_PREFERENCES_ICON_VIEW_DEFAULT_ZOOM_LEVEL, NAUTILUS_GRID_ZOOM_LEVEL_MEDIUM, NAUTILUS_VIEW_GRID_ID, }, }; for (uint i = 0; i < G_N_ELEMENTS (testcases); i++) { run_view_zoom_testcase (testcases[i].view_setting, testcases[i].zoom_setting_key, testcases[i].default_zoom_level, testcases[i].view_id); } } const GStrv hidden_files_hierarchy = (char *[]) { "my_file", "my.file", "my_file.", "~my_file", "hidden_temp_file~", ".my_hidden_file", ".very_hidden_file~", "my_indirectly_hidden_file", "my_indirectly_hidden_tmp_file~", ".hidden", NULL }; typedef struct { gboolean success; GMainLoop *loop; } ExtractionTestData; static ExtractionTestData * extraction_test_data_new (void) { ExtractionTestData *data = g_new0 (ExtractionTestData, 1); data->loop = g_main_loop_new (NULL, FALSE); return data; } static void extraction_test_data_free (ExtractionTestData *data) { g_clear_pointer (&data->loop, g_main_loop_unref); g_free (data); } G_DEFINE_AUTOPTR_CLEANUP_FUNC (ExtractionTestData, extraction_test_data_free) static void compress_callback (GFile *new_file, gboolean success, gpointer user_data) { ExtractionTestData *data = user_data; data->success = success; g_main_loop_quit (data->loop); } static void test_extract_action (void) { g_autoptr (NautilusWindowSlot) slot = NULL; g_autoptr (NautilusFilesView) files_view = create_view_with_slot (NAUTILUS_MODE_BROWSE, NAUTILUS_VIEW_GRID_ID, &slot); NautilusViewModel *model = nautilus_files_view_get_private_model (files_view); g_autoptr (GFile) tmp_location = g_file_new_for_path (test_get_tmp_dir ()); g_autoptr (GError) error = NULL; /* Create a file that will be compressed into an archive */ g_autoptr (GFile) document_location = g_file_get_child (tmp_location, "document"); g_autoptr (GFileOutputStream) output_stream = g_file_create (document_location, G_FILE_CREATE_NONE, NULL, NULL); g_autoptr (GFile) archive_file = g_file_get_child (tmp_location, "document.zip"); g_autoptr (ExtractionTestData) compress_data = extraction_test_data_new (); g_assert_nonnull (output_stream); g_output_stream_close (G_OUTPUT_STREAM (output_stream), NULL, &error); g_assert_no_error (error); /* Compress it into a .zip archive */ nautilus_file_operations_compress (&(GList){ .data = document_location }, archive_file, AUTOAR_FORMAT_ZIP, AUTOAR_FILTER_NONE, NULL, NULL, NULL, compress_callback, compress_data); g_main_loop_run (compress_data->loop); g_assert_true (compress_data->success); g_assert_true (g_file_query_exists (archive_file, NULL)); /* Delete the inner file so we can verify extraction recreates it */ g_assert_true (g_file_delete (document_location, NULL, NULL)); g_assert_false (g_file_query_exists (document_location, NULL)); nautilus_files_view_set_location (files_view, tmp_location); ITER_CONTEXT_WHILE (nautilus_files_view_get_loading (files_view)); GActionGroup *group = nautilus_files_view_get_private_action_group (files_view); GAction *action_extract_here = g_action_map_lookup_action (G_ACTION_MAP (group), "extract-here"); g_assert_nonnull (action_extract_here); /* With no selection, action should be disabled */ g_assert_false (g_action_get_enabled (action_extract_here)); /* Select the archive file and extract it with the action */ g_autoptr (NautilusFile) archive_nautilus_file = nautilus_file_get (archive_file); g_autoptr (NautilusFile) document_file = NULL; g_autolist (NautilusFile) out_selection = NULL; nautilus_files_view_set_selection (files_view, &(GList) { .data = archive_nautilus_file}, NAUTILUS_SELECTION_SOURCE_MANUAL); /* It will switch between default activation and "extract-here" depending * on whether Files is the default app for zip archives. */ if (g_action_get_enabled (action_extract_here)) { gtk_widget_activate_action (GTK_WIDGET (files_view), "view.extract-here", NULL); } else { GAction *action_default_activate = g_action_map_lookup_action (G_ACTION_MAP (group), "open-with-default-application"); g_assert_true (g_action_get_enabled (action_default_activate)); gtk_widget_activate_action (GTK_WIDGET (files_view), "view.open-with-default-application", NULL); } document_file = nautilus_file_get (document_location); ITER_CONTEXT_WHILE (nautilus_view_model_get_item_for_file (model, document_file) == NULL); g_assert_true (g_file_query_exists (document_location, NULL)); out_selection = nautilus_files_view_get_selection (files_view); g_assert_nonnull (out_selection); g_assert_cmpint (g_list_length (out_selection), ==, 1); g_assert_cmpint (nautilus_files_view_get_selection_source (files_view), ==, NAUTILUS_SELECTION_SOURCE_OP_DONE); /* With text file selected, action should be disabled */ g_assert_false (g_action_get_enabled (action_extract_here)); test_clear_tmp_dir (); } static void test_selection_actions (void) { g_autoptr (NautilusWindowSlot) slot = NULL; g_autoptr (NautilusFilesView) files_view = create_view_with_slot (NAUTILUS_MODE_BROWSE, NAUTILUS_VIEW_GRID_ID, &slot); NautilusViewModel *model = nautilus_files_view_get_private_model (files_view); g_autoptr (GFile) tmp_location = g_file_new_for_path (test_get_tmp_dir ()); const guint file_count = 10, selected_count = 4; g_autoptr (NautilusFileList) selection_set = NULL; NautilusFileList *got_selection = NULL; guint got_count; /* Need to subtract one since it creates an extra directory. */ create_multiple_files ("select_test", file_count - 1); nautilus_files_view_set_location (files_view, tmp_location); ITER_CONTEXT_WHILE (nautilus_files_view_get_loading (files_view)); for (guint i = 0; i < selected_count; i++) { g_autofree gchar *name = g_strdup_printf ("select_test_file_%u", i); g_autoptr (GFile) location = g_file_get_child (tmp_location, name); g_autoptr (NautilusFile) file = nautilus_file_get (location); selection_set = g_list_append (selection_set, nautilus_file_ref (file)); } /* Set the selection on the view */ nautilus_files_view_set_selection (files_view, selection_set, NAUTILUS_SELECTION_SOURCE_AUTO); /* Retrieve selection and verify it matches what we set */ got_selection = nautilus_files_view_get_selection (files_view); got_count = g_list_length (got_selection); g_assert_cmpint (nautilus_files_view_get_selection_source (files_view), ==, NAUTILUS_SELECTION_SOURCE_AUTO); g_assert_cmpint (got_count, ==, selected_count); for (NautilusFileList *l = got_selection; l != NULL; l = l->next) { NautilusFile *file = l->data; NautilusViewItem *item = nautilus_view_model_get_item_for_file (model, file); g_assert_nonnull (item); g_assert_nonnull (g_list_find (selection_set, file)); } g_clear_pointer (&got_selection, nautilus_file_list_free); /* Invert selection */ gtk_widget_activate_action (GTK_WIDGET (files_view), "view.invert-selection", NULL); got_selection = nautilus_files_view_get_selection (files_view); got_count = g_list_length (got_selection); g_assert_cmpint (nautilus_files_view_get_selection_source (files_view), ==, NAUTILUS_SELECTION_SOURCE_MANUAL); g_assert_cmpint (got_count, ==, file_count - selected_count); for (NautilusFileList *l = got_selection; l != NULL; l = l->next) { NautilusFile *file = l->data; NautilusViewItem *item = nautilus_view_model_get_item_for_file (model, file); g_assert_nonnull (item); g_assert_null (g_list_find (selection_set, file)); } g_clear_pointer (&got_selection, nautilus_file_list_free); /* Select all */ gtk_widget_activate_action (GTK_WIDGET (files_view), "view.select-all", NULL); got_selection = nautilus_files_view_get_selection (files_view); got_count = g_list_length (got_selection); g_assert_cmpint (nautilus_files_view_get_selection_source (files_view), ==, NAUTILUS_SELECTION_SOURCE_MANUAL); g_assert_cmpint (got_count, ==, file_count); g_assert_cmpint (got_count, ==, g_list_model_get_n_items (G_LIST_MODEL (model))); g_clear_pointer (&got_selection, nautilus_file_list_free); /* Invert selection to clear selection. */ gtk_widget_activate_action (GTK_WIDGET (files_view), "view.invert-selection", NULL); got_selection = nautilus_files_view_get_selection (files_view); g_assert_cmpint (nautilus_files_view_get_selection_source (files_view), ==, NAUTILUS_SELECTION_SOURCE_MANUAL); g_assert_null (got_selection); g_clear_pointer (&got_selection, nautilus_file_list_free); test_clear_tmp_dir (); } static void test_selection_source_in_search (void) { g_autoptr (NautilusWindowSlot) slot = NULL; g_autoptr (NautilusFilesView) files_view = create_view_with_slot (NAUTILUS_MODE_BROWSE, NAUTILUS_VIEW_GRID_ID, &slot); NautilusViewModel *model = nautilus_files_view_get_private_model (files_view); g_autoptr (GFile) tmp_location = g_file_new_for_path (test_get_tmp_dir ()); const guint file_count = 10; g_autoptr (NautilusQuery) search_query = nautilus_query_new (); NautilusFileList *got_selection = NULL; /* Need to subtract one since it creates an extra directory. */ create_multiple_files ("select_test", file_count - 1); nautilus_files_view_set_location (files_view, tmp_location); ITER_CONTEXT_WHILE (nautilus_files_view_get_loading (files_view)); g_assert_cmpint (g_list_model_get_n_items (G_LIST_MODEL (model)), ==, file_count); nautilus_query_set_text (search_query, "2"); nautilus_query_set_location (search_query, tmp_location); nautilus_files_view_set_search_query (files_view, search_query); ITER_CONTEXT_WHILE (nautilus_files_view_get_loading (files_view)); g_assert_cmpint (nautilus_files_view_get_selection_source (files_view), ==, NAUTILUS_SELECTION_SOURCE_IN_SEARCH); got_selection = nautilus_files_view_get_selection (files_view); g_assert_cmpint (g_list_length (got_selection), ==, 1); g_clear_pointer (&got_selection, nautilus_file_list_free); /* Cannot check for NAUTILUS_SELECTION_SOURCE_AFTER_SEARCH because it is * set through selection restoration through the slot. */ test_clear_tmp_dir (); } static void test_open_item_location_in_search (void) { const GStrv test_hierarchy = (char *[]) { "dir/", "dir/my_nested_file", NULL }; g_autoptr (NautilusWindowSlot) slot = NULL; g_autoptr (NautilusFilesView) files_view = create_view_with_slot (NAUTILUS_MODE_BROWSE, NAUTILUS_VIEW_GRID_ID, &slot); NautilusViewModel *model = nautilus_files_view_get_private_model (files_view); g_autoptr (GFile) tmp_location = g_file_new_for_path (test_get_tmp_dir ()); g_autoptr (NautilusQuery) search_query = nautilus_query_new (); g_autoptr (GFile) dir_location = g_file_get_child (tmp_location, "dir"); g_autoptr (GFile) nested_file_location = g_file_get_child (dir_location, "my_nested_file"); g_autolist (NautilusFile) out_selection = NULL; g_autoptr (GFile) out_location = NULL; file_hierarchy_create (test_hierarchy, ""); nautilus_files_view_set_location (files_view, tmp_location); ITER_CONTEXT_WHILE (nautilus_files_view_get_loading (files_view)); /* The model should contain only the nested directory at this level. */ g_assert_cmpint (g_list_model_get_n_items (G_LIST_MODEL (model)), ==, 1); /* Search for the nested file. */ nautilus_query_set_text (search_query, "my_nested_file"); nautilus_query_set_location (search_query, tmp_location); nautilus_files_view_set_search_query (files_view, search_query); ITER_CONTEXT_WHILE (nautilus_files_view_get_loading (files_view)); g_assert_cmpint (nautilus_files_view_get_selection_source (files_view), ==, NAUTILUS_SELECTION_SOURCE_IN_SEARCH); /* Trigger the open-item-location action while searching. */ gtk_widget_activate_action (GTK_WIDGET (files_view), "view.open-item-location", NULL); ITER_CONTEXT_WHILE (!g_file_equal (dir_location, nautilus_files_view_get_location (files_view))); out_selection = nautilus_files_view_get_selection (files_view); g_assert_cmpint (g_list_length (out_selection), ==, 1); out_location = nautilus_file_get_location (out_selection->data); g_assert_true (g_file_equal (out_location, nested_file_location)); /* FIXME: This doesn't work since the slot doesn't store pending selection * source. * g_assert_cmpint (nautilus_files_view_get_selection_source (files_view), * ==, * NAUTILUS_SELECTION_SOURCE_AFTER_SEARCH); */ test_clear_tmp_dir (); } static void test_selection_source_operation (void) { GStrv files_hierarchy = (char *[]) { "file_1", "file_2", "file_3", "file_4", NULL }; g_autoptr (NautilusWindowSlot) slot = NULL; g_autoptr (NautilusFilesView) files_view = create_view_with_slot (NAUTILUS_MODE_BROWSE, NAUTILUS_VIEW_GRID_ID, &slot); NautilusViewModel *model = nautilus_files_view_get_private_model (files_view); g_autoptr (GFile) tmp_location = g_file_new_for_path (test_get_tmp_dir ()); const guint file_count = 4; const guint selected_count = 2; g_autolist (GFile) file_list = NULL; g_autoptr (NautilusFileList) first_selection = NULL; g_autoptr (NautilusFileList) second_selection = NULL; NautilusFileList *got_selection = NULL; file_hierarchy_create (files_hierarchy, ""); nautilus_files_view_set_location (files_view, tmp_location); ITER_CONTEXT_WHILE (nautilus_files_view_get_loading (files_view)); g_assert_cmpint (g_list_model_get_n_items (G_LIST_MODEL (model)), ==, file_count); /* Build two disjoint selection sets. */ file_list = file_hierarchy_get_files_list (files_hierarchy, "", TRUE); for (guint i = 0; i < selected_count; i++) { GFile *location = g_list_nth_data (file_list, i); g_autoptr (NautilusFile) file = nautilus_file_get (location); first_selection = g_list_append (first_selection, g_steal_pointer (&file)); } for (guint i = selected_count; i < file_count; i++) { GFile *location = g_list_nth_data (file_list, i); g_autoptr (NautilusFile) file = nautilus_file_get (location); second_selection = g_list_append (second_selection, g_steal_pointer (&file)); } /* Set a selection with MANUAL source, then start a copy operation, then * wait for the operation to complete, verifying the selection source * transitions correctly at each step. */ /* * #1: No manual selection before the operation ends */ nautilus_files_view_set_selection (files_view, first_selection, NAUTILUS_SELECTION_SOURCE_MANUAL); g_assert_cmpint (nautilus_files_view_get_selection_source (files_view), ==, NAUTILUS_SELECTION_SOURCE_MANUAL); got_selection = nautilus_files_view_get_selection (files_view); g_assert_cmpint (g_list_length (got_selection), ==, selected_count); g_clear_pointer (&got_selection, nautilus_file_list_free); gtk_widget_activate_action (GTK_WIDGET (files_view), "view.copy", NULL); gtk_widget_activate_action (GTK_WIDGET (files_view), "view.paste", NULL); got_selection = nautilus_files_view_get_selection (files_view); g_assert_cmpint (g_list_length (got_selection), ==, selected_count); g_assert_cmpint (nautilus_files_view_get_selection_source (files_view), ==, NAUTILUS_SELECTION_SOURCE_OP_START); g_clear_pointer (&got_selection, nautilus_file_list_free); ITER_CONTEXT_WHILE (nautilus_files_view_get_selection_source (files_view) != NAUTILUS_SELECTION_SOURCE_OP_DONE); got_selection = nautilus_files_view_get_selection (files_view); g_assert_cmpint (g_list_length (got_selection), ==, selected_count); g_clear_pointer (&got_selection, nautilus_file_list_free); /* * #2: Manual selection before the operation ends */ nautilus_files_view_set_selection (files_view, first_selection, NAUTILUS_SELECTION_SOURCE_MANUAL); g_assert_cmpint (nautilus_files_view_get_selection_source (files_view), ==, NAUTILUS_SELECTION_SOURCE_MANUAL); got_selection = nautilus_files_view_get_selection (files_view); g_assert_cmpint (g_list_length (got_selection), ==, selected_count); g_clear_pointer (&got_selection, nautilus_file_list_free); gtk_widget_activate_action (GTK_WIDGET (files_view), "view.copy", NULL); gtk_widget_activate_action (GTK_WIDGET (files_view), "view.paste", NULL); got_selection = nautilus_files_view_get_selection (files_view); g_assert_cmpint (g_list_length (got_selection), ==, selected_count); g_assert_cmpint (nautilus_files_view_get_selection_source (files_view), ==, NAUTILUS_SELECTION_SOURCE_OP_START); g_clear_pointer (&got_selection, nautilus_file_list_free); nautilus_files_view_set_selection (files_view, second_selection, NAUTILUS_SELECTION_SOURCE_MANUAL); g_assert_cmpint (nautilus_files_view_get_selection_source (files_view), ==, NAUTILUS_SELECTION_SOURCE_MANUAL); ITER_CONTEXT_WHILE (nautilus_file_undo_manager_get_state () == NAUTILUS_FILE_UNDO_MANAGER_STATE_NONE); got_selection = nautilus_files_view_get_selection (files_view); g_assert_cmpint (g_list_length (got_selection), ==, selected_count); g_assert_cmpint (nautilus_files_view_get_selection_source (files_view), ==, NAUTILUS_SELECTION_SOURCE_MANUAL); g_clear_pointer (&got_selection, nautilus_file_list_free); test_clear_tmp_dir (); } static void create_hidden_files (void) { g_autoptr (GFile) hidden_list_file = g_file_new_build_filename (test_get_tmp_dir (), ".hidden", NULL); const gchar *content = "my_indirectly_hidden_file\n" "my_indirectly_hidden_tmp_file~\n"; g_autoptr (GError) error = NULL; file_hierarchy_create (hidden_files_hierarchy, ""); g_file_replace_contents (hidden_list_file, content, strlen (content), NULL, FALSE, G_FILE_CREATE_NONE, NULL, NULL, &error); g_assert_no_error (error); } static void test_hidden_files_renamed (void) { g_settings_set_boolean (gtk_filechooser_preferences, NAUTILUS_PREFERENCES_SHOW_HIDDEN_FILES, FALSE); g_autoptr (NautilusWindowSlot) slot = NULL; g_autoptr (NautilusFilesView) files_view = create_view_with_slot (NAUTILUS_MODE_BROWSE, NAUTILUS_VIEW_GRID_ID, &slot); NautilusViewModel *model = nautilus_files_view_get_private_model (files_view); g_autoptr (GFile) tmp_location = g_file_new_for_path (test_get_tmp_dir ()); const guint renamed_file_count = 4; g_autoptr (GPtrArray) renamed_files_arr = g_ptr_array_new_full (renamed_file_count, (GDestroyNotify) nautilus_file_unref); g_autoptr (GPtrArray) changed_files_arr = g_ptr_array_new_full (renamed_file_count, (GDestroyNotify) nautilus_file_unref); create_hidden_files (); /* Load the directory with hidden files not shown. */ nautilus_files_view_set_location (NAUTILUS_FILES_VIEW (files_view), tmp_location); ITER_CONTEXT_WHILE (nautilus_files_view_get_loading (files_view)); /* Rename a few hidden files to visible names and verify they appear. */ g_signal_connect (files_view, "file-changed", G_CALLBACK (collect_changed_files), changed_files_arr); for (guint i = 0, renamed = 0; hidden_files_hierarchy[i] != NULL && renamed < renamed_file_count; i++) { const gchar *orig_name = hidden_files_hierarchy[i]; /* Only pick hidden entries */ if (strstr (orig_name, "hidden") == NULL) { continue; } g_autoptr (GFile) location = g_file_new_build_filename (test_get_tmp_dir (), orig_name, NULL); g_autoptr (NautilusFile) file = nautilus_file_get (location); NautilusViewItem *item = nautilus_view_model_get_item_for_file (model, file); g_autofree gchar *new_name = g_strdup_printf ("renamed_visible_%u", renamed); /* Ensure hidden files are not visible initially. */ g_assert_true (nautilus_file_is_hidden_file (file)); g_assert_null (item); nautilus_file_rename (file, new_name, collect_renamed_files, renamed_files_arr); renamed++; } ITER_CONTEXT_WHILE (changed_files_arr->len < renamed_file_count || renamed_files_arr->len < renamed_file_count || !ptr_array_is_subset (renamed_files_arr, changed_files_arr)); /* The renamed files should now be present in the view model. */ for (guint i = 0; i < renamed_files_arr->len; i++) { NautilusFile *file = renamed_files_arr->pdata[i]; NautilusViewItem *item = nautilus_view_model_get_item_for_file (model, file); g_assert_false (nautilus_file_is_hidden_file (file)); g_assert_nonnull (item); } test_clear_tmp_dir (); } static void test_hidden_files_change (void) { g_settings_set_boolean (gtk_filechooser_preferences, NAUTILUS_PREFERENCES_SHOW_HIDDEN_FILES, FALSE); g_autoptr (NautilusWindowSlot) slot = NULL; g_autoptr (NautilusFilesView) files_view = create_view_with_slot (NAUTILUS_MODE_BROWSE, NAUTILUS_VIEW_GRID_ID, &slot); NautilusViewModel *model = nautilus_files_view_get_private_model (files_view); g_autoptr (GFile) tmp_location = g_file_new_for_path (test_get_tmp_dir ()); create_hidden_files (); /* Test visibility when hidden files are not shown. */ nautilus_files_view_set_location (NAUTILUS_FILES_VIEW (files_view), tmp_location); ITER_CONTEXT_WHILE (nautilus_files_view_get_loading (files_view)); for (gchar **filename = hidden_files_hierarchy; *filename != NULL; filename++) { g_autoptr (GFile) location = g_file_new_build_filename (test_get_tmp_dir (), *filename, NULL); g_autoptr (NautilusFile) file = nautilus_file_get (location); NautilusViewItem *item = nautilus_view_model_get_item_for_file (model, file); if (strstr (*filename, "hidden")) { g_assert_true (nautilus_file_is_hidden_file (file)); g_assert_null (item); } else { g_assert_false (nautilus_file_is_hidden_file (file)); g_assert_nonnull (item); } } /* Test visibility when hidden files are shown. */ gtk_widget_activate_action (GTK_WIDGET (files_view), "view.show-hidden-files", NULL); ITER_CONTEXT_WHILE (nautilus_files_view_get_loading (files_view)); for (gchar **filename = hidden_files_hierarchy; *filename != NULL; filename++) { g_autoptr (GFile) location = g_file_new_build_filename (test_get_tmp_dir (), *filename, NULL); g_autoptr (NautilusFile) file = nautilus_file_get (location); NautilusViewItem *item = nautilus_view_model_get_item_for_file (model, file); g_assert_nonnull (item); if (strstr (*filename, "hidden")) { g_assert_true (nautilus_file_is_hidden_file (file)); } else { g_assert_false (nautilus_file_is_hidden_file (file)); } } test_clear_tmp_dir (); } static void replace_cb (GObject *object, GAsyncResult *result, gpointer user_data) { GFile *file = G_FILE (object); g_autoptr (GError) error = NULL; GPtrArray *replaced_files = user_data; g_file_replace_contents_finish (file, result, NULL, &error); g_assert_no_error (error); g_ptr_array_add (replaced_files, g_object_ref (file)); } static void test_replace_files (void) { /* This is not reproducible using files created in tmpfs. */ g_test_bug ("https://gitlab.gnome.org/GNOME/nautilus/-/issues/3959"); g_settings_set_boolean (gtk_filechooser_preferences, NAUTILUS_PREFERENCES_SHOW_HIDDEN_FILES, FALSE); g_autoptr (NautilusWindowSlot) slot = NULL; g_autoptr (NautilusFilesView) files_view = create_view_with_slot (NAUTILUS_MODE_BROWSE, NAUTILUS_VIEW_GRID_ID, &slot); NautilusViewModel *model = nautilus_files_view_get_private_model (files_view); g_autoptr (GFile) tmp_location = g_file_new_for_path (test_get_tmp_dir ()); const gsize file_count = 10, replaced_file_count = 5; g_autoptr (GPtrArray) file_arr = g_ptr_array_new_full (file_count, (GDestroyNotify) nautilus_file_unref); g_autoptr (GPtrArray) replacment_files_arr = g_ptr_array_new_full (replaced_file_count, (GDestroyNotify) g_object_unref); g_autoptr (GPtrArray) changed_files_arr = g_ptr_array_new_full (replaced_file_count, (GDestroyNotify) nautilus_file_unref); gboolean end_of_changes = FALSE; /* Create the files before loading the view and keep them in an array. */ for (gsize i = 0; i < file_count; i++) { g_autofree gchar *file_name = g_strdup_printf ("test_file_%zu", i); g_autoptr (GFile) file = g_file_get_child (tmp_location, file_name); g_autofree gchar *text = g_uuid_string_random (); g_autoptr (GError) error = NULL; g_autoptr (GFileOutputStream) out = g_file_create (file, G_FILE_CREATE_NONE, NULL, &error); g_assert_nonnull (out); g_assert_no_error (error); g_output_stream_write_all (G_OUTPUT_STREAM (out), text, strlen (text), NULL, NULL, &error); g_assert_no_error (error); g_ptr_array_add (file_arr, nautilus_file_get (file)); } nautilus_files_view_set_location (files_view, tmp_location); ITER_CONTEXT_WHILE (nautilus_files_view_get_loading (files_view)); g_assert_cmpint (g_list_model_get_n_items (G_LIST_MODEL (model)), ==, file_count); /* Replace only some of the files and verify that changes are emitted. */ g_signal_connect (files_view, "file-changed", G_CALLBACK (collect_changed_files), changed_files_arr); g_signal_connect (files_view, "end-file-changes", G_CALLBACK (file_changes_done), &end_of_changes); for (gsize i = 0; i < replaced_file_count; i++) { NautilusFile *file = file_arr->pdata[i]; g_autoptr (GFile) location = nautilus_file_get_location (file); g_autofree gchar *text = g_uuid_string_random (); gsize text_len = strlen (text); g_autoptr (GBytes) bytes = g_bytes_new_take (g_steal_pointer (&text), text_len); g_autoptr (GError) error = NULL; g_file_replace_contents_bytes_async (location, bytes, NULL, FALSE, G_FILE_CREATE_REPLACE_DESTINATION, NULL, replace_cb, replacment_files_arr); g_assert_no_error (error); } ITER_CONTEXT_WHILE (!end_of_changes || replacment_files_arr->len < replaced_file_count || changed_files_arr->len < replaced_file_count); g_assert_cmpint (g_list_model_get_n_items (G_LIST_MODEL (model)), ==, file_count); for (gsize i = 0; i < file_arr->len; i++) { NautilusFile *file = file_arr->pdata[i]; NautilusViewItem *item = nautilus_view_model_get_item_for_file (model, file); g_autoptr (GFile) location = nautilus_file_get_location (file); /* Ignore replaced files as they are not guaranteed to stay the same */ if (!g_ptr_array_find_with_equal_func (replacment_files_arr, location, (GEqualFunc) g_file_equal, NULL)) { g_assert_nonnull (item); } } for (gsize i = 0; i < replacment_files_arr->len; i++) { GFile *location = replacment_files_arr->pdata[i]; g_autoptr (NautilusFile) file = nautilus_file_get (location); NautilusViewItem *item = nautilus_view_model_get_item_for_file (model, file); /* GIO might use a temperarly hidden file for writing that starts with * a dot like ".goutputstream-XXXXXX". Ensure the file is visible after * the rename to the intended file. */ g_assert_nonnull (item); } for (gsize i = 0; i < changed_files_arr->len; i++) { NautilusFile *file = changed_files_arr->pdata[i]; NautilusViewItem *item = nautilus_view_model_get_item_for_file (model, file); g_assert_nonnull (item); } test_clear_tmp_dir (); } static void test_rename_files (void) { g_autoptr (NautilusWindowSlot) slot = NULL; g_autoptr (NautilusFilesView) files_view = create_view_with_slot (NAUTILUS_MODE_BROWSE, NAUTILUS_VIEW_GRID_ID, &slot); NautilusViewModel *model = nautilus_files_view_get_private_model (files_view); g_autoptr (GFile) tmp_location = g_file_new_for_path (test_get_tmp_dir ()); const guint file_count = 10, renamed_file_count = 5; g_autoptr (GPtrArray) file_arr = g_ptr_array_new_full (file_count, (GDestroyNotify) nautilus_file_unref); g_autoptr (GPtrArray) renamed_files_arr = g_ptr_array_new_full (renamed_file_count, (GDestroyNotify) nautilus_file_unref); g_autoptr (GPtrArray) changed_files_arr = g_ptr_array_new_full (renamed_file_count, (GDestroyNotify) nautilus_file_unref); /* Create the files before loading the view and keep them in an array. */ for (guint i = 0; i < file_count; i++) { g_autofree gchar *file_name = g_strdup_printf ("test_file_%i", i); g_autoptr (GFile) file = g_file_get_child (tmp_location, file_name); g_autoptr (GFileOutputStream) out = g_file_create (file, G_FILE_CREATE_NONE, NULL, NULL); g_assert_nonnull (out); g_ptr_array_add (file_arr, nautilus_file_get (file)); } nautilus_files_view_set_location (files_view, tmp_location); ITER_CONTEXT_WHILE (nautilus_files_view_get_loading (files_view)); g_assert_cmpint (g_list_model_get_n_items (G_LIST_MODEL (model)), ==, file_count); /* Rename only some of the files and verify that changes are emitted. */ g_signal_connect (files_view, "file-changed", G_CALLBACK (collect_changed_files), changed_files_arr); for (guint i = 0; i < renamed_file_count; i++) { NautilusFile *file = file_arr->pdata[i]; g_autoptr (GFile) location = nautilus_file_get_location (file); g_autofree gchar *file_name = g_strdup_printf ("test_file_%i.txt", i); nautilus_file_rename (file, file_name, collect_renamed_files, renamed_files_arr); } ITER_CONTEXT_WHILE (changed_files_arr->len < renamed_file_count || renamed_files_arr->len != renamed_file_count || !ptr_array_is_subset (renamed_files_arr, changed_files_arr)); g_assert_cmpint (g_list_model_get_n_items (G_LIST_MODEL (model)), ==, file_count); /* Both renamed and non-renamed nautilus file pointers from before renaming * should still point to the same files in the view. Renaming should not * make the old pointers invalid since we're using nautilus_file_rename(). */ for (guint i = 0; i < file_arr->len; i++) { NautilusFile *file = file_arr->pdata[i]; NautilusViewItem *item = nautilus_view_model_get_item_for_file (model, file); g_assert_nonnull (item); } for (guint i = 0; i < renamed_files_arr->len; i++) { NautilusFile *file = renamed_files_arr->pdata[i]; NautilusViewItem *item = nautilus_view_model_get_item_for_file (model, file); g_assert_nonnull (item); } test_clear_tmp_dir (); } static void collect_removed_files_cb (NautilusFilesView *view, GList *removed_files, NautilusDirectory *directory, GPtrArray *data_files) { for (GList *link = removed_files; link != NULL; link = link->next) { g_ptr_array_add (data_files, nautilus_file_ref (NAUTILUS_FILE (link->data))); } } static void test_remove_files (void) { g_autoptr (NautilusWindowSlot) slot = NULL; g_autoptr (NautilusFilesView) files_view = create_view_with_slot (NAUTILUS_MODE_BROWSE, NAUTILUS_VIEW_GRID_ID, &slot); NautilusViewModel *model = nautilus_files_view_get_private_model (files_view); g_autoptr (GFile) tmp_location = g_file_new_for_path (test_get_tmp_dir ()); g_autofree gchar *uri = g_file_get_uri (tmp_location); const guint file_count = 10, deleted_file_count = 5; g_autoptr (GPtrArray) file_arr = g_ptr_array_new_full (file_count, (GDestroyNotify) nautilus_file_unref); g_autoptr (GPtrArray) deleted_files_arr = g_ptr_array_new (); g_autoptr (GPtrArray) callback_arr = g_ptr_array_new_full (file_count, (GDestroyNotify) nautilus_file_unref); /* Create the files before loading the view and keep them in an array. */ for (guint i = 0; i < file_count; i++) { g_autofree gchar *file_name = g_strdup_printf ("test_file_%i", i); g_autoptr (GFile) file = g_file_get_child (tmp_location, file_name); g_autoptr (GFileOutputStream) out = g_file_create (file, G_FILE_CREATE_NONE, NULL, NULL); g_assert_nonnull (out); g_ptr_array_add (file_arr, nautilus_file_get (file)); } nautilus_files_view_set_location (files_view, tmp_location); ITER_CONTEXT_WHILE (nautilus_files_view_get_loading (files_view)); g_assert_true (g_file_equal (tmp_location, nautilus_files_view_get_location (files_view))); g_assert_cmpint (g_list_model_get_n_items (G_LIST_MODEL (model)), ==, file_count); /* Delete only some of the files and verify that they are emitted */ g_signal_connect (files_view, "remove-files", G_CALLBACK (collect_removed_files_cb), callback_arr); for (guint i = 0; i < deleted_file_count; i++) { g_autoptr (GFile) location = nautilus_file_get_location (file_arr->pdata[i]); g_autoptr (GError) error = NULL; g_file_delete (location, NULL, &error); g_assert_null (error); g_ptr_array_add (deleted_files_arr, file_arr->pdata[i]); } ITER_CONTEXT_WHILE (!ptr_arrays_equal_unordered (deleted_files_arr, callback_arr)); g_assert_cmpint (g_list_model_get_n_items (G_LIST_MODEL (model)), ==, file_count - deleted_file_count); for (guint i = 0; i < file_arr->len; i++) { NautilusFile *file = file_arr->pdata[i]; NautilusViewItem *item = nautilus_view_model_get_item_for_file (model, file); if (g_ptr_array_find (deleted_files_arr, file, NULL)) { g_assert_null (item); } else { g_assert_nonnull (item); } } test_clear_tmp_dir (); } static void collect_added_files_cb (NautilusFilesView *view, GList *added_files, GPtrArray *data_files) { for (GList *link = added_files; link != NULL; link = link->next) { g_ptr_array_add (data_files, nautilus_file_ref (NAUTILUS_FILE (link->data))); } } static void test_add_files (void) { g_autoptr (NautilusWindowSlot) slot = NULL; g_autoptr (NautilusFilesView) files_view = create_view_with_slot (NAUTILUS_MODE_BROWSE, NAUTILUS_VIEW_GRID_ID, &slot); NautilusViewModel *model = nautilus_files_view_get_private_model (files_view); g_autoptr (GFile) tmp_location = g_file_new_for_path (test_get_tmp_dir ()); g_autofree gchar *uri = g_file_get_uri (tmp_location); const guint file_count = 10; g_autoptr (GPtrArray) file_arr = g_ptr_array_new_full (file_count, (GDestroyNotify) nautilus_file_unref); g_autoptr (GPtrArray) callback_arr = g_ptr_array_new_full (file_count, (GDestroyNotify) nautilus_file_unref); nautilus_files_view_set_location (files_view, tmp_location); ITER_CONTEXT_WHILE (nautilus_files_view_get_loading (files_view)); g_assert_true (g_file_equal (tmp_location, nautilus_files_view_get_location (files_view))); g_assert_cmpint (g_list_model_get_n_items (G_LIST_MODEL (model)), ==, 0); /* Keep a list of NautilusFiles */ for (guint i = 0; i < file_count; i++) { g_autofree gchar *file_name = g_strdup_printf ("test_file_%i", i); g_autoptr (GFile) location = g_file_get_child (tmp_location, file_name); g_ptr_array_add (file_arr, nautilus_file_get (location)); } /* Create files and verify that the view added the correct files */ g_signal_connect (files_view, "add-files", G_CALLBACK (collect_added_files_cb), callback_arr); for (guint i = 0; i < file_arr->len; i++) { g_autoptr (GFile) location = nautilus_file_get_location (file_arr->pdata[i]); g_autoptr (GFileOutputStream) out = g_file_create (location, G_FILE_CREATE_NONE, NULL, NULL); g_assert_nonnull (out); } ITER_CONTEXT_WHILE (!ptr_arrays_equal_unordered (file_arr, callback_arr)); g_assert_cmpint (g_list_model_get_n_items (G_LIST_MODEL (model)), ==, file_count); for (guint i = 0; i < file_arr->len; i++) { NautilusFile *file = file_arr->pdata[i]; g_assert_nonnull (nautilus_view_model_get_item_for_file (model, file)); } test_clear_tmp_dir (); } static void test_load_dir (void) { g_autoptr (NautilusWindowSlot) slot = NULL; g_autoptr (NautilusFilesView) files_view = create_view_with_slot (NAUTILUS_MODE_BROWSE, NAUTILUS_VIEW_GRID_ID, &slot); NautilusViewModel *model = nautilus_files_view_get_private_model (files_view); g_autoptr (GFile) tmp_location = g_file_new_for_path (test_get_tmp_dir ()); g_autofree gchar *uri = g_file_get_uri (tmp_location); gboolean loading_started = FALSE, loading_ended = FALSE; const guint file_count = 10; create_multiple_files ("view_test", file_count); /* Verify loading signals and location */ nautilus_files_view_set_location (files_view, tmp_location); g_signal_connect_swapped (files_view, "begin-loading", G_CALLBACK (set_true), &loading_started); g_signal_connect_swapped (files_view, "end-loading", G_CALLBACK (set_true), &loading_ended); g_assert_true (nautilus_files_view_get_loading (files_view)); ITER_CONTEXT_WHILE (nautilus_files_view_get_loading (files_view)); GFile *view_location = nautilus_files_view_get_location (files_view); g_autofree gchar *view_uri = g_file_get_uri (view_location); g_assert_true (g_file_equal (tmp_location, view_location)); g_assert_cmpstr (view_uri, ==, uri); g_assert_true (loading_started); g_assert_true (loading_ended); /* Verify that files exist exist in the model */ g_autoptr (GFileEnumerator) enumerator = NULL; g_autoptr (GError) error = NULL; GFile *child; guint counter = 0; enumerator = g_file_enumerate_children (tmp_location, G_FILE_ATTRIBUTE_STANDARD_NAME, G_TYPE_FILE_QUERY_INFO_FLAGS, NULL, &error); g_assert_no_error (error); /* Account for the extra folder */ g_assert_cmpint (g_list_model_get_n_items (G_LIST_MODEL (model)), ==, file_count + 1); while (g_file_enumerator_iterate (enumerator, NULL, &child, NULL, NULL) && child != NULL) { g_autoptr (NautilusFile) file = nautilus_file_get (child); g_assert_nonnull (nautilus_view_model_get_item_for_file (model, file)); counter++; } g_assert_cmpint (counter, ==, file_count + 1); test_clear_tmp_dir (); } int main (int argc, char *argv[]) { g_autoptr (NautilusTagManager) tag_manager = NULL; gtk_test_init (&argc, &argv, NULL); if (nautilus_application_is_sandboxed ()) { g_message ("files-view tests don't work inside a sandbox."); return 77; } nautilus_register_resource (); nautilus_ensure_extension_points (); nautilus_global_preferences_init (); tag_manager = nautilus_tag_manager_new_dummy (); test_init_config_dir (); g_autoptr (NautilusApplication) app = nautilus_application_new (); g_test_add_func ("/view/load_dir", test_load_dir); g_test_add_func ("/view/add_files", test_add_files); g_test_add_func ("/view/remove_files", test_remove_files); g_test_add_func ("/view/change_files/rename", test_rename_files); g_test_add_func ("/view/change_files/replace", test_replace_files); g_test_add_func ("/view/hidden_files/change", test_hidden_files_change); g_test_add_func ("/view/hidden_files/rename_files", test_hidden_files_renamed); g_test_add_func ("/view/selection/actions", test_selection_actions); g_test_add_func ("/view/selection/in_search", test_selection_source_in_search); g_test_add_func ("/view/selection/operation", test_selection_source_operation); g_test_add_func ("/view/actions/extract", test_extract_action); g_test_add_func ("/view/actions/open_item_location/search", test_open_item_location_in_search); g_test_add_func ("/view/actions/zoom", test_zoom_actions); return g_test_run (); }